chore: Move to prettier standard double quotes (#1309)
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
// @flow
|
||||
import { DataTypes, sequelize } from '../sequelize';
|
||||
import randomstring from 'randomstring';
|
||||
import { DataTypes, sequelize } from "../sequelize";
|
||||
import randomstring from "randomstring";
|
||||
|
||||
const ApiKey = sequelize.define(
|
||||
'apiKeys',
|
||||
"apiKeys",
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
@@ -17,12 +17,12 @@ const ApiKey = sequelize.define(
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'users',
|
||||
model: "users",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
tableName: 'apiKeys',
|
||||
tableName: "apiKeys",
|
||||
paranoid: true,
|
||||
hooks: {
|
||||
beforeValidate: key => {
|
||||
@@ -34,8 +34,8 @@ const ApiKey = sequelize.define(
|
||||
|
||||
ApiKey.associate = models => {
|
||||
ApiKey.belongsTo(models.User, {
|
||||
as: 'user',
|
||||
foreignKey: 'userId',
|
||||
as: "user",
|
||||
foreignKey: "userId",
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// @flow
|
||||
import path from 'path';
|
||||
import { DataTypes, sequelize } from '../sequelize';
|
||||
import { deleteFromS3 } from '../utils/s3';
|
||||
import path from "path";
|
||||
import { DataTypes, sequelize } from "../sequelize";
|
||||
import { deleteFromS3 } from "../utils/s3";
|
||||
|
||||
const Attachment = sequelize.define(
|
||||
'attachment',
|
||||
"attachment",
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
@@ -30,9 +30,9 @@ const Attachment = sequelize.define(
|
||||
acl: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: 'public-read',
|
||||
defaultValue: "public-read",
|
||||
validate: {
|
||||
isIn: [['private', 'public-read']],
|
||||
isIn: [["private", "public-read"]],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -45,7 +45,7 @@ const Attachment = sequelize.define(
|
||||
return `/api/attachments.redirect?id=${this.id}`;
|
||||
},
|
||||
isPrivate: function() {
|
||||
return this.acl === 'private';
|
||||
return this.acl === "private";
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
import { DataTypes, sequelize, encryptedFields } from '../sequelize';
|
||||
import { DataTypes, sequelize, encryptedFields } from "../sequelize";
|
||||
|
||||
const Authentication = sequelize.define('authentication', {
|
||||
const Authentication = sequelize.define("authentication", {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
@@ -9,17 +9,17 @@ const Authentication = sequelize.define('authentication', {
|
||||
},
|
||||
service: DataTypes.STRING,
|
||||
scopes: DataTypes.ARRAY(DataTypes.STRING),
|
||||
token: encryptedFields.vault('token'),
|
||||
token: encryptedFields.vault("token"),
|
||||
});
|
||||
|
||||
Authentication.associate = models => {
|
||||
Authentication.belongsTo(models.User, {
|
||||
as: 'user',
|
||||
foreignKey: 'userId',
|
||||
as: "user",
|
||||
foreignKey: "userId",
|
||||
});
|
||||
Authentication.belongsTo(models.Team, {
|
||||
as: 'team',
|
||||
foreignKey: 'teamId',
|
||||
as: "team",
|
||||
foreignKey: "teamId",
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
import { DataTypes, sequelize } from '../sequelize';
|
||||
import { DataTypes, sequelize } from "../sequelize";
|
||||
|
||||
const Backlink = sequelize.define('backlink', {
|
||||
const Backlink = sequelize.define("backlink", {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
@@ -11,16 +11,16 @@ const Backlink = sequelize.define('backlink', {
|
||||
|
||||
Backlink.associate = models => {
|
||||
Backlink.belongsTo(models.Document, {
|
||||
as: 'document',
|
||||
foreignKey: 'documentId',
|
||||
as: "document",
|
||||
foreignKey: "documentId",
|
||||
});
|
||||
Backlink.belongsTo(models.Document, {
|
||||
as: 'reverseDocument',
|
||||
foreignKey: 'reverseDocumentId',
|
||||
as: "reverseDocument",
|
||||
foreignKey: "reverseDocumentId",
|
||||
});
|
||||
Backlink.belongsTo(models.User, {
|
||||
as: 'user',
|
||||
foreignKey: 'userId',
|
||||
as: "user",
|
||||
foreignKey: "userId",
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
// @flow
|
||||
import { find, concat, remove, uniq } from 'lodash';
|
||||
import slug from 'slug';
|
||||
import randomstring from 'randomstring';
|
||||
import { DataTypes, sequelize } from '../sequelize';
|
||||
import Document from './Document';
|
||||
import CollectionUser from './CollectionUser';
|
||||
import { find, concat, remove, uniq } from "lodash";
|
||||
import slug from "slug";
|
||||
import randomstring from "randomstring";
|
||||
import { DataTypes, sequelize } from "../sequelize";
|
||||
import Document from "./Document";
|
||||
import CollectionUser from "./CollectionUser";
|
||||
|
||||
slug.defaults.mode = 'rfc3986';
|
||||
slug.defaults.mode = "rfc3986";
|
||||
|
||||
const Collection = sequelize.define(
|
||||
'collection',
|
||||
"collection",
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
@@ -25,14 +25,14 @@ const Collection = sequelize.define(
|
||||
maintainerApprovalRequired: DataTypes.BOOLEAN,
|
||||
type: {
|
||||
type: DataTypes.STRING,
|
||||
validate: { isIn: [['atlas', 'journal']] },
|
||||
validate: { isIn: [["atlas", "journal"]] },
|
||||
},
|
||||
|
||||
/* type: atlas */
|
||||
documentStructure: DataTypes.JSONB,
|
||||
},
|
||||
{
|
||||
tableName: 'collections',
|
||||
tableName: "collections",
|
||||
paranoid: true,
|
||||
hooks: {
|
||||
beforeValidate: (collection: Collection) => {
|
||||
@@ -47,8 +47,8 @@ const Collection = sequelize.define(
|
||||
}
|
||||
);
|
||||
|
||||
Collection.addHook('beforeSave', async model => {
|
||||
if (model.icon === 'collection') {
|
||||
Collection.addHook("beforeSave", async model => {
|
||||
if (model.icon === "collection") {
|
||||
model.icon = null;
|
||||
}
|
||||
});
|
||||
@@ -57,48 +57,48 @@ Collection.addHook('beforeSave', async model => {
|
||||
|
||||
Collection.associate = models => {
|
||||
Collection.hasMany(models.Document, {
|
||||
as: 'documents',
|
||||
foreignKey: 'collectionId',
|
||||
onDelete: 'cascade',
|
||||
as: "documents",
|
||||
foreignKey: "collectionId",
|
||||
onDelete: "cascade",
|
||||
});
|
||||
Collection.hasMany(models.CollectionUser, {
|
||||
as: 'memberships',
|
||||
foreignKey: 'collectionId',
|
||||
onDelete: 'cascade',
|
||||
as: "memberships",
|
||||
foreignKey: "collectionId",
|
||||
onDelete: "cascade",
|
||||
});
|
||||
Collection.hasMany(models.CollectionGroup, {
|
||||
as: 'collectionGroupMemberships',
|
||||
foreignKey: 'collectionId',
|
||||
onDelete: 'cascade',
|
||||
as: "collectionGroupMemberships",
|
||||
foreignKey: "collectionId",
|
||||
onDelete: "cascade",
|
||||
});
|
||||
Collection.belongsToMany(models.User, {
|
||||
as: 'users',
|
||||
as: "users",
|
||||
through: models.CollectionUser,
|
||||
foreignKey: 'collectionId',
|
||||
foreignKey: "collectionId",
|
||||
});
|
||||
Collection.belongsToMany(models.Group, {
|
||||
as: 'groups',
|
||||
as: "groups",
|
||||
through: models.CollectionGroup,
|
||||
foreignKey: 'collectionId',
|
||||
foreignKey: "collectionId",
|
||||
});
|
||||
Collection.belongsTo(models.User, {
|
||||
as: 'user',
|
||||
foreignKey: 'creatorId',
|
||||
as: "user",
|
||||
foreignKey: "creatorId",
|
||||
});
|
||||
Collection.belongsTo(models.Team, {
|
||||
as: 'team',
|
||||
as: "team",
|
||||
});
|
||||
Collection.addScope('withMembership', userId => ({
|
||||
Collection.addScope("withMembership", userId => ({
|
||||
include: [
|
||||
{
|
||||
model: models.CollectionUser,
|
||||
as: 'memberships',
|
||||
as: "memberships",
|
||||
where: { userId },
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
model: models.CollectionGroup,
|
||||
as: 'collectionGroupMemberships',
|
||||
as: "collectionGroupMemberships",
|
||||
required: false,
|
||||
|
||||
// use of "separate" property: sequelize breaks when there are
|
||||
@@ -111,11 +111,11 @@ Collection.associate = models => {
|
||||
// CollectionGroup [inner join] Group [inner join] GroupUser [where] userId
|
||||
include: {
|
||||
model: models.Group,
|
||||
as: 'group',
|
||||
as: "group",
|
||||
required: true,
|
||||
include: {
|
||||
model: models.GroupUser,
|
||||
as: 'groupMemberships',
|
||||
as: "groupMemberships",
|
||||
required: true,
|
||||
where: { userId },
|
||||
},
|
||||
@@ -123,16 +123,16 @@ Collection.associate = models => {
|
||||
},
|
||||
],
|
||||
}));
|
||||
Collection.addScope('withAllMemberships', {
|
||||
Collection.addScope("withAllMemberships", {
|
||||
include: [
|
||||
{
|
||||
model: models.CollectionUser,
|
||||
as: 'memberships',
|
||||
as: "memberships",
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
model: models.CollectionGroup,
|
||||
as: 'collectionGroupMemberships',
|
||||
as: "collectionGroupMemberships",
|
||||
required: false,
|
||||
|
||||
// use of "separate" property: sequelize breaks when there are
|
||||
@@ -145,11 +145,11 @@ Collection.associate = models => {
|
||||
// CollectionGroup [inner join] Group [inner join] GroupUser [where] userId
|
||||
include: {
|
||||
model: models.Group,
|
||||
as: 'group',
|
||||
as: "group",
|
||||
required: true,
|
||||
include: {
|
||||
model: models.GroupUser,
|
||||
as: 'groupMemberships',
|
||||
as: "groupMemberships",
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
@@ -158,7 +158,7 @@ Collection.associate = models => {
|
||||
});
|
||||
};
|
||||
|
||||
Collection.addHook('afterDestroy', async (model: Collection) => {
|
||||
Collection.addHook("afterDestroy", async (model: Collection) => {
|
||||
await Document.destroy({
|
||||
where: {
|
||||
collectionId: model.id,
|
||||
@@ -166,7 +166,7 @@ Collection.addHook('afterDestroy', async (model: Collection) => {
|
||||
});
|
||||
});
|
||||
|
||||
Collection.addHook('afterCreate', (model: Collection, options) => {
|
||||
Collection.addHook("afterCreate", (model: Collection, options) => {
|
||||
if (model.private) {
|
||||
return CollectionUser.findOrCreate({
|
||||
where: {
|
||||
@@ -174,7 +174,7 @@ Collection.addHook('afterCreate', (model: Collection, options) => {
|
||||
userId: model.creatorId,
|
||||
},
|
||||
defaults: {
|
||||
permission: 'read_write',
|
||||
permission: "read_write",
|
||||
createdById: model.creatorId,
|
||||
},
|
||||
transaction: options.transaction,
|
||||
@@ -186,7 +186,7 @@ Collection.addHook('afterCreate', (model: Collection, options) => {
|
||||
|
||||
// get all the membership relationshps a user could have with the collection
|
||||
Collection.membershipUserIds = async (collectionId: string) => {
|
||||
const collection = await Collection.scope('withAllMemberships').findByPk(
|
||||
const collection = await Collection.scope("withAllMemberships").findByPk(
|
||||
collectionId
|
||||
);
|
||||
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
/* eslint-disable flowtype/require-valid-file-annotation */
|
||||
import { flushdb, seed } from '../test/support';
|
||||
import { Collection, Document } from '../models';
|
||||
import { flushdb, seed } from "../test/support";
|
||||
import { Collection, Document } from "../models";
|
||||
import {
|
||||
buildUser,
|
||||
buildGroup,
|
||||
buildCollection,
|
||||
buildTeam,
|
||||
} from '../test/factories';
|
||||
import uuid from 'uuid';
|
||||
} from "../test/factories";
|
||||
import uuid from "uuid";
|
||||
|
||||
beforeEach(flushdb);
|
||||
beforeEach(jest.resetAllMocks);
|
||||
|
||||
describe('#url', () => {
|
||||
test('should return correct url for the collection', () => {
|
||||
const collection = new Collection({ id: '1234' });
|
||||
expect(collection.url).toBe('/collections/1234');
|
||||
describe("#url", () => {
|
||||
test("should return correct url for the collection", () => {
|
||||
const collection = new Collection({ id: "1234" });
|
||||
expect(collection.url).toBe("/collections/1234");
|
||||
});
|
||||
});
|
||||
|
||||
describe('#addDocumentToStructure', async () => {
|
||||
test('should add as last element without index', async () => {
|
||||
describe("#addDocumentToStructure", async () => {
|
||||
test("should add as last element without index", async () => {
|
||||
const { collection } = await seed();
|
||||
const id = uuid.v4();
|
||||
const newDocument = new Document({
|
||||
id,
|
||||
title: 'New end node',
|
||||
title: "New end node",
|
||||
parentDocumentId: null,
|
||||
});
|
||||
|
||||
@@ -34,12 +34,12 @@ describe('#addDocumentToStructure', async () => {
|
||||
expect(collection.documentStructure[1].id).toBe(id);
|
||||
});
|
||||
|
||||
test('should add with an index', async () => {
|
||||
test("should add with an index", async () => {
|
||||
const { collection } = await seed();
|
||||
const id = uuid.v4();
|
||||
const newDocument = new Document({
|
||||
id,
|
||||
title: 'New end node',
|
||||
title: "New end node",
|
||||
parentDocumentId: null,
|
||||
});
|
||||
|
||||
@@ -48,12 +48,12 @@ describe('#addDocumentToStructure', async () => {
|
||||
expect(collection.documentStructure[1].id).toBe(id);
|
||||
});
|
||||
|
||||
test('should add as a child if with parent', async () => {
|
||||
test("should add as a child if with parent", async () => {
|
||||
const { collection, document } = await seed();
|
||||
const id = uuid.v4();
|
||||
const newDocument = new Document({
|
||||
id,
|
||||
title: 'New end node',
|
||||
title: "New end node",
|
||||
parentDocumentId: document.id,
|
||||
});
|
||||
|
||||
@@ -64,17 +64,17 @@ describe('#addDocumentToStructure', async () => {
|
||||
expect(collection.documentStructure[0].children[0].id).toBe(id);
|
||||
});
|
||||
|
||||
test('should add as a child if with parent with index', async () => {
|
||||
test("should add as a child if with parent with index", async () => {
|
||||
const { collection, document } = await seed();
|
||||
const newDocument = new Document({
|
||||
id: uuid.v4(),
|
||||
title: 'node',
|
||||
title: "node",
|
||||
parentDocumentId: document.id,
|
||||
});
|
||||
const id = uuid.v4();
|
||||
const secondDocument = new Document({
|
||||
id,
|
||||
title: 'New start node',
|
||||
title: "New start node",
|
||||
parentDocumentId: document.id,
|
||||
});
|
||||
|
||||
@@ -86,13 +86,13 @@ describe('#addDocumentToStructure', async () => {
|
||||
expect(collection.documentStructure[0].children[0].id).toBe(id);
|
||||
});
|
||||
|
||||
describe('options: documentJson', async () => {
|
||||
describe("options: documentJson", async () => {
|
||||
test("should append supplied json over document's own", async () => {
|
||||
const { collection } = await seed();
|
||||
const id = uuid.v4();
|
||||
const newDocument = new Document({
|
||||
id: uuid.v4(),
|
||||
title: 'New end node',
|
||||
title: "New end node",
|
||||
parentDocumentId: null,
|
||||
});
|
||||
|
||||
@@ -101,7 +101,7 @@ describe('#addDocumentToStructure', async () => {
|
||||
children: [
|
||||
{
|
||||
id,
|
||||
title: 'Totally fake',
|
||||
title: "Totally fake",
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
@@ -113,16 +113,16 @@ describe('#addDocumentToStructure', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#updateDocument', () => {
|
||||
describe("#updateDocument", () => {
|
||||
test("should update root document's data", async () => {
|
||||
const { collection, document } = await seed();
|
||||
|
||||
document.title = 'Updated title';
|
||||
document.title = "Updated title";
|
||||
|
||||
await document.save();
|
||||
await collection.updateDocument(document);
|
||||
|
||||
expect(collection.documentStructure[0].title).toBe('Updated title');
|
||||
expect(collection.documentStructure[0].title).toBe("Updated title");
|
||||
});
|
||||
|
||||
test("should update child document's data", async () => {
|
||||
@@ -134,32 +134,32 @@ describe('#updateDocument', () => {
|
||||
userId: collection.creatorId,
|
||||
lastModifiedById: collection.creatorId,
|
||||
createdById: collection.creatorId,
|
||||
title: 'Child document',
|
||||
text: 'content',
|
||||
title: "Child document",
|
||||
text: "content",
|
||||
});
|
||||
await collection.addDocumentToStructure(newDocument);
|
||||
|
||||
newDocument.title = 'Updated title';
|
||||
newDocument.title = "Updated title";
|
||||
await newDocument.save();
|
||||
|
||||
await collection.updateDocument(newDocument);
|
||||
|
||||
expect(collection.documentStructure[0].children[0].title).toBe(
|
||||
'Updated title'
|
||||
"Updated title"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#removeDocument', () => {
|
||||
test('should save if removing', async () => {
|
||||
describe("#removeDocument", () => {
|
||||
test("should save if removing", async () => {
|
||||
const { collection, document } = await seed();
|
||||
jest.spyOn(collection, 'save');
|
||||
jest.spyOn(collection, "save");
|
||||
|
||||
await collection.deleteDocument(document);
|
||||
expect(collection.save).toBeCalled();
|
||||
});
|
||||
|
||||
test('should remove documents from root', async () => {
|
||||
test("should remove documents from root", async () => {
|
||||
const { collection, document } = await seed();
|
||||
|
||||
await collection.deleteDocument(document);
|
||||
@@ -174,7 +174,7 @@ describe('#removeDocument', () => {
|
||||
expect(collectionDocuments.count).toBe(0);
|
||||
});
|
||||
|
||||
test('should remove a document with child documents', async () => {
|
||||
test("should remove a document with child documents", async () => {
|
||||
const { collection, document } = await seed();
|
||||
|
||||
// Add a child for testing
|
||||
@@ -185,8 +185,8 @@ describe('#removeDocument', () => {
|
||||
userId: collection.creatorId,
|
||||
lastModifiedById: collection.creatorId,
|
||||
createdById: collection.creatorId,
|
||||
title: 'Child document',
|
||||
text: 'content',
|
||||
title: "Child document",
|
||||
text: "content",
|
||||
});
|
||||
await collection.addDocumentToStructure(newDocument);
|
||||
expect(collection.documentStructure[0].children.length).toBe(1);
|
||||
@@ -202,7 +202,7 @@ describe('#removeDocument', () => {
|
||||
expect(collectionDocuments.count).toBe(0);
|
||||
});
|
||||
|
||||
test('should remove a child document', async () => {
|
||||
test("should remove a child document", async () => {
|
||||
const { collection, document } = await seed();
|
||||
|
||||
// Add a child for testing
|
||||
@@ -214,8 +214,8 @@ describe('#removeDocument', () => {
|
||||
lastModifiedById: collection.creatorId,
|
||||
createdById: collection.creatorId,
|
||||
publishedAt: new Date(),
|
||||
title: 'Child document',
|
||||
text: 'content',
|
||||
title: "Child document",
|
||||
text: "content",
|
||||
});
|
||||
await collection.addDocumentToStructure(newDocument);
|
||||
expect(collection.documentStructure.length).toBe(1);
|
||||
@@ -236,8 +236,8 @@ describe('#removeDocument', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#membershipUserIds', () => {
|
||||
test('should return collection and group memberships', async () => {
|
||||
describe("#membershipUserIds", () => {
|
||||
test("should return collection and group memberships", async () => {
|
||||
const team = await buildTeam();
|
||||
const teamId = team.id;
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// @flow
|
||||
import { DataTypes, sequelize } from '../sequelize';
|
||||
import { DataTypes, sequelize } from "../sequelize";
|
||||
|
||||
const CollectionGroup = sequelize.define(
|
||||
'collection_group',
|
||||
"collection_group",
|
||||
{
|
||||
permission: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: 'read_write',
|
||||
defaultValue: "read_write",
|
||||
validate: {
|
||||
isIn: [['read', 'read_write', 'maintainer']],
|
||||
isIn: [["read", "read_write", "maintainer"]],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -20,18 +20,18 @@ const CollectionGroup = sequelize.define(
|
||||
|
||||
CollectionGroup.associate = models => {
|
||||
CollectionGroup.belongsTo(models.Collection, {
|
||||
as: 'collection',
|
||||
foreignKey: 'collectionId',
|
||||
as: "collection",
|
||||
foreignKey: "collectionId",
|
||||
primary: true,
|
||||
});
|
||||
CollectionGroup.belongsTo(models.Group, {
|
||||
as: 'group',
|
||||
foreignKey: 'groupId',
|
||||
as: "group",
|
||||
foreignKey: "groupId",
|
||||
primary: true,
|
||||
});
|
||||
CollectionGroup.belongsTo(models.User, {
|
||||
as: 'createdBy',
|
||||
foreignKey: 'createdById',
|
||||
as: "createdBy",
|
||||
foreignKey: "createdById",
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// @flow
|
||||
import { DataTypes, sequelize } from '../sequelize';
|
||||
import { DataTypes, sequelize } from "../sequelize";
|
||||
|
||||
const CollectionUser = sequelize.define(
|
||||
'collection_user',
|
||||
"collection_user",
|
||||
{
|
||||
permission: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: 'read_write',
|
||||
defaultValue: "read_write",
|
||||
validate: {
|
||||
isIn: [['read', 'read_write', 'maintainer']],
|
||||
isIn: [["read", "read_write", "maintainer"]],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -19,16 +19,16 @@ const CollectionUser = sequelize.define(
|
||||
|
||||
CollectionUser.associate = models => {
|
||||
CollectionUser.belongsTo(models.Collection, {
|
||||
as: 'collection',
|
||||
foreignKey: 'collectionId',
|
||||
as: "collection",
|
||||
foreignKey: "collectionId",
|
||||
});
|
||||
CollectionUser.belongsTo(models.User, {
|
||||
as: 'user',
|
||||
foreignKey: 'userId',
|
||||
as: "user",
|
||||
foreignKey: "userId",
|
||||
});
|
||||
CollectionUser.belongsTo(models.User, {
|
||||
as: 'createdBy',
|
||||
foreignKey: 'createdById',
|
||||
as: "createdBy",
|
||||
foreignKey: "createdById",
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
// @flow
|
||||
import { map, find, compact, uniq } from 'lodash';
|
||||
import MarkdownSerializer from 'slate-md-serializer';
|
||||
import randomstring from 'randomstring';
|
||||
import Sequelize, { type Transaction } from 'sequelize';
|
||||
import removeMarkdown from '@tommoor/remove-markdown';
|
||||
import { map, find, compact, uniq } from "lodash";
|
||||
import MarkdownSerializer from "slate-md-serializer";
|
||||
import randomstring from "randomstring";
|
||||
import Sequelize, { type Transaction } from "sequelize";
|
||||
import removeMarkdown from "@tommoor/remove-markdown";
|
||||
|
||||
import isUUID from 'validator/lib/isUUID';
|
||||
import { Collection, User } from '../models';
|
||||
import { DataTypes, sequelize } from '../sequelize';
|
||||
import parseTitle from '../../shared/utils/parseTitle';
|
||||
import unescape from '../../shared/utils/unescape';
|
||||
import slugify from '../utils/slugify';
|
||||
import Revision from './Revision';
|
||||
import isUUID from "validator/lib/isUUID";
|
||||
import { Collection, User } from "../models";
|
||||
import { DataTypes, sequelize } from "../sequelize";
|
||||
import parseTitle from "../../shared/utils/parseTitle";
|
||||
import unescape from "../../shared/utils/unescape";
|
||||
import slugify from "../utils/slugify";
|
||||
import Revision from "./Revision";
|
||||
|
||||
const Op = Sequelize.Op;
|
||||
const URL_REGEX = /^[0-9a-zA-Z-_~]*-([a-zA-Z0-9]{10,15})$/;
|
||||
@@ -25,8 +25,8 @@ const createRevision = (doc, options = {}) => {
|
||||
|
||||
// we don't create revisions if identical to previous
|
||||
if (
|
||||
doc.text === doc.previous('text') &&
|
||||
doc.title === doc.previous('title')
|
||||
doc.text === doc.previous("text") &&
|
||||
doc.title === doc.previous("title")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -64,7 +64,7 @@ const beforeSave = async doc => {
|
||||
doc.emoji = emoji;
|
||||
|
||||
// ensure documents have a title
|
||||
doc.title = doc.title || '';
|
||||
doc.title = doc.title || "";
|
||||
|
||||
// add the current user as a collaborator on this doc
|
||||
if (!doc.collaboratorIds) doc.collaboratorIds = [];
|
||||
@@ -77,7 +77,7 @@ const beforeSave = async doc => {
|
||||
};
|
||||
|
||||
const Document = sequelize.define(
|
||||
'document',
|
||||
"document",
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
@@ -93,7 +93,7 @@ const Document = sequelize.define(
|
||||
validate: {
|
||||
len: {
|
||||
args: [0, 100],
|
||||
msg: 'Document title must be less than 100 characters',
|
||||
msg: "Document title must be less than 100 characters",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -134,45 +134,45 @@ const Document = sequelize.define(
|
||||
|
||||
Document.associate = models => {
|
||||
Document.belongsTo(models.Collection, {
|
||||
as: 'collection',
|
||||
foreignKey: 'collectionId',
|
||||
onDelete: 'cascade',
|
||||
as: "collection",
|
||||
foreignKey: "collectionId",
|
||||
onDelete: "cascade",
|
||||
});
|
||||
Document.belongsTo(models.Team, {
|
||||
as: 'team',
|
||||
foreignKey: 'teamId',
|
||||
as: "team",
|
||||
foreignKey: "teamId",
|
||||
});
|
||||
Document.belongsTo(models.User, {
|
||||
as: 'createdBy',
|
||||
foreignKey: 'createdById',
|
||||
as: "createdBy",
|
||||
foreignKey: "createdById",
|
||||
});
|
||||
Document.belongsTo(models.User, {
|
||||
as: 'updatedBy',
|
||||
foreignKey: 'lastModifiedById',
|
||||
as: "updatedBy",
|
||||
foreignKey: "lastModifiedById",
|
||||
});
|
||||
Document.belongsTo(models.User, {
|
||||
as: 'pinnedBy',
|
||||
foreignKey: 'pinnedById',
|
||||
as: "pinnedBy",
|
||||
foreignKey: "pinnedById",
|
||||
});
|
||||
Document.hasMany(models.Revision, {
|
||||
as: 'revisions',
|
||||
onDelete: 'cascade',
|
||||
as: "revisions",
|
||||
onDelete: "cascade",
|
||||
});
|
||||
Document.hasMany(models.Backlink, {
|
||||
as: 'backlinks',
|
||||
onDelete: 'cascade',
|
||||
as: "backlinks",
|
||||
onDelete: "cascade",
|
||||
});
|
||||
Document.hasMany(models.Star, {
|
||||
as: 'starred',
|
||||
onDelete: 'cascade',
|
||||
as: "starred",
|
||||
onDelete: "cascade",
|
||||
});
|
||||
Document.hasMany(models.View, {
|
||||
as: 'views',
|
||||
as: "views",
|
||||
});
|
||||
Document.addScope('defaultScope', {
|
||||
Document.addScope("defaultScope", {
|
||||
include: [
|
||||
{ model: models.User, as: 'createdBy', paranoid: false },
|
||||
{ model: models.User, as: 'updatedBy', paranoid: false },
|
||||
{ model: models.User, as: "createdBy", paranoid: false },
|
||||
{ model: models.User, as: "updatedBy", paranoid: false },
|
||||
],
|
||||
where: {
|
||||
publishedAt: {
|
||||
@@ -180,38 +180,38 @@ Document.associate = models => {
|
||||
},
|
||||
},
|
||||
});
|
||||
Document.addScope('withCollection', userId => {
|
||||
Document.addScope("withCollection", userId => {
|
||||
if (userId) {
|
||||
return {
|
||||
include: [
|
||||
{
|
||||
model: models.Collection.scope({
|
||||
method: ['withMembership', userId],
|
||||
method: ["withMembership", userId],
|
||||
}),
|
||||
as: 'collection',
|
||||
as: "collection",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
include: [{ model: models.Collection, as: 'collection' }],
|
||||
include: [{ model: models.Collection, as: "collection" }],
|
||||
};
|
||||
});
|
||||
Document.addScope('withUnpublished', {
|
||||
Document.addScope("withUnpublished", {
|
||||
include: [
|
||||
{ model: models.User, as: 'createdBy', paranoid: false },
|
||||
{ model: models.User, as: 'updatedBy', paranoid: false },
|
||||
{ model: models.User, as: "createdBy", paranoid: false },
|
||||
{ model: models.User, as: "updatedBy", paranoid: false },
|
||||
],
|
||||
});
|
||||
Document.addScope('withViews', userId => ({
|
||||
Document.addScope("withViews", userId => ({
|
||||
include: [
|
||||
{ model: models.View, as: 'views', where: { userId }, required: false },
|
||||
{ model: models.View, as: "views", where: { userId }, required: false },
|
||||
],
|
||||
}));
|
||||
Document.addScope('withStarred', userId => ({
|
||||
Document.addScope("withStarred", userId => ({
|
||||
include: [
|
||||
{ model: models.Star, as: 'starred', where: { userId }, required: false },
|
||||
{ model: models.Star, as: "starred", where: { userId }, required: false },
|
||||
],
|
||||
}));
|
||||
};
|
||||
@@ -219,8 +219,8 @@ Document.associate = models => {
|
||||
Document.findByPk = async function(id, options = {}) {
|
||||
// allow default preloading of collection membership if `userId` is passed in find options
|
||||
// almost every endpoint needs the collection membership to determine policy permissions.
|
||||
const scope = this.scope('withUnpublished', {
|
||||
method: ['withCollection', options.userId],
|
||||
const scope = this.scope("withUnpublished", {
|
||||
method: ["withCollection", options.userId],
|
||||
});
|
||||
|
||||
if (isUUID(id)) {
|
||||
@@ -248,7 +248,7 @@ type SearchOptions = {
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
collectionId?: string,
|
||||
dateFilter?: 'day' | 'week' | 'month' | 'year',
|
||||
dateFilter?: "day" | "week" | "month" | "year",
|
||||
collaboratorIds?: string[],
|
||||
includeArchived?: boolean,
|
||||
includeDrafts?: boolean,
|
||||
@@ -257,7 +257,7 @@ type SearchOptions = {
|
||||
function escape(query: string): string {
|
||||
// replace "\" with escaped "\\" because sequelize.escape doesn't do it
|
||||
// https://github.com/sequelize/sequelize/issues/2950
|
||||
return sequelize.escape(query).replace('\\', '\\\\');
|
||||
return sequelize.escape(query).replace("\\", "\\\\");
|
||||
}
|
||||
|
||||
Document.searchForTeam = async (
|
||||
@@ -308,12 +308,12 @@ Document.searchForTeam = async (
|
||||
// Final query to get associated document data
|
||||
const documents = await Document.findAll({
|
||||
where: {
|
||||
id: map(results, 'id'),
|
||||
id: map(results, "id"),
|
||||
},
|
||||
include: [
|
||||
{ model: Collection, as: 'collection' },
|
||||
{ model: User, as: 'createdBy', paranoid: false },
|
||||
{ model: User, as: 'updatedBy', paranoid: false },
|
||||
{ model: Collection, as: "collection" },
|
||||
{ model: User, as: "createdBy", paranoid: false },
|
||||
{ model: User, as: "updatedBy", paranoid: false },
|
||||
],
|
||||
});
|
||||
|
||||
@@ -366,14 +366,14 @@ Document.searchForUser = async (
|
||||
"teamId" = :teamId AND
|
||||
"collectionId" IN(:collectionIds) AND
|
||||
${
|
||||
options.dateFilter ? '"updatedAt" > now() - interval :dateFilter AND' : ''
|
||||
options.dateFilter ? '"updatedAt" > now() - interval :dateFilter AND' : ""
|
||||
}
|
||||
${
|
||||
options.collaboratorIds
|
||||
? '"collaboratorIds" @> ARRAY[:collaboratorIds]::uuid[] AND'
|
||||
: ''
|
||||
: ""
|
||||
}
|
||||
${options.includeArchived ? '' : '"archivedAt" IS NULL AND'}
|
||||
${options.includeArchived ? "" : '"archivedAt" IS NULL AND'}
|
||||
"deletedAt" IS NULL AND
|
||||
${
|
||||
options.includeDrafts
|
||||
@@ -404,18 +404,18 @@ Document.searchForUser = async (
|
||||
// Final query to get associated document data
|
||||
const documents = await Document.scope(
|
||||
{
|
||||
method: ['withViews', user.id],
|
||||
method: ["withViews", user.id],
|
||||
},
|
||||
{
|
||||
method: ['withCollection', user.id],
|
||||
method: ["withCollection", user.id],
|
||||
}
|
||||
).findAll({
|
||||
where: {
|
||||
id: map(results, 'id'),
|
||||
id: map(results, "id"),
|
||||
},
|
||||
include: [
|
||||
{ model: User, as: 'createdBy', paranoid: false },
|
||||
{ model: User, as: 'updatedBy', paranoid: false },
|
||||
{ model: User, as: "createdBy", paranoid: false },
|
||||
{ model: User, as: "updatedBy", paranoid: false },
|
||||
],
|
||||
});
|
||||
|
||||
@@ -430,21 +430,21 @@ Document.searchForUser = async (
|
||||
|
||||
// Hooks
|
||||
|
||||
Document.addHook('beforeSave', async model => {
|
||||
Document.addHook("beforeSave", async model => {
|
||||
if (!model.publishedAt) return;
|
||||
|
||||
const collection = await Collection.findByPk(model.collectionId);
|
||||
if (!collection || collection.type !== 'atlas') return;
|
||||
if (!collection || collection.type !== "atlas") return;
|
||||
|
||||
await collection.updateDocument(model);
|
||||
model.collection = collection;
|
||||
});
|
||||
|
||||
Document.addHook('afterCreate', async model => {
|
||||
Document.addHook("afterCreate", async model => {
|
||||
if (!model.publishedAt) return;
|
||||
|
||||
const collection = await Collection.findByPk(model.collectionId);
|
||||
if (!collection || collection.type !== 'atlas') return;
|
||||
if (!collection || collection.type !== "atlas") return;
|
||||
|
||||
await collection.addDocumentToStructure(model);
|
||||
model.collection = collection;
|
||||
@@ -470,7 +470,7 @@ Document.prototype.migrateVersion = function() {
|
||||
// migrate from document version 0 -> 1
|
||||
if (!this.version) {
|
||||
// removing the title from the document text attribute
|
||||
this.text = this.text.replace(/^#\s(.*)\n/, '');
|
||||
this.text = this.text.replace(/^#\s(.*)\n/, "");
|
||||
this.version = 1;
|
||||
migrated = true;
|
||||
}
|
||||
@@ -535,7 +535,7 @@ Document.prototype.publish = async function(options) {
|
||||
if (this.publishedAt) return this.save(options);
|
||||
|
||||
const collection = await Collection.findByPk(this.collectionId);
|
||||
if (collection.type !== 'atlas') return this.save(options);
|
||||
if (collection.type !== "atlas") return this.save(options);
|
||||
|
||||
await collection.addDocumentToStructure(this);
|
||||
|
||||
@@ -618,14 +618,14 @@ Document.prototype.getSummary = function() {
|
||||
const plain = removeMarkdown(unescape(this.text), {
|
||||
stripHTML: false,
|
||||
});
|
||||
const lines = compact(plain.split('\n'));
|
||||
const lines = compact(plain.split("\n"));
|
||||
const notEmpty = lines.length >= 1;
|
||||
|
||||
if (this.version) {
|
||||
return notEmpty ? lines[0] : '';
|
||||
return notEmpty ? lines[0] : "";
|
||||
}
|
||||
|
||||
return notEmpty ? lines[1] : '';
|
||||
return notEmpty ? lines[1] : "";
|
||||
};
|
||||
|
||||
Document.prototype.toJSON = function() {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/* eslint-disable flowtype/require-valid-file-annotation */
|
||||
import { flushdb } from '../test/support';
|
||||
import { Document } from '../models';
|
||||
import { buildDocument, buildCollection, buildTeam } from '../test/factories';
|
||||
import { flushdb } from "../test/support";
|
||||
import { Document } from "../models";
|
||||
import { buildDocument, buildCollection, buildTeam } from "../test/factories";
|
||||
|
||||
beforeEach(flushdb);
|
||||
beforeEach(jest.resetAllMocks);
|
||||
|
||||
describe('#getSummary', () => {
|
||||
test('should strip markdown', async () => {
|
||||
describe("#getSummary", () => {
|
||||
test("should strip markdown", async () => {
|
||||
const document = await buildDocument({
|
||||
version: 1,
|
||||
text: `*paragraph*
|
||||
@@ -15,10 +15,10 @@ describe('#getSummary', () => {
|
||||
paragraph 2`,
|
||||
});
|
||||
|
||||
expect(document.getSummary()).toBe('paragraph');
|
||||
expect(document.getSummary()).toBe("paragraph");
|
||||
});
|
||||
|
||||
test('should strip title when no version', async () => {
|
||||
test("should strip title when no version", async () => {
|
||||
const document = await buildDocument({
|
||||
version: null,
|
||||
text: `# Heading
|
||||
@@ -26,12 +26,12 @@ paragraph 2`,
|
||||
*paragraph*`,
|
||||
});
|
||||
|
||||
expect(document.getSummary()).toBe('paragraph');
|
||||
expect(document.getSummary()).toBe("paragraph");
|
||||
});
|
||||
});
|
||||
|
||||
describe('#migrateVersion', () => {
|
||||
test('should maintain empty paragraph under headings', async () => {
|
||||
describe("#migrateVersion", () => {
|
||||
test("should maintain empty paragraph under headings", async () => {
|
||||
const document = await buildDocument({
|
||||
version: 1,
|
||||
text: `# Heading
|
||||
@@ -44,7 +44,7 @@ paragraph`,
|
||||
paragraph`);
|
||||
});
|
||||
|
||||
test('should add breaks under headings with extra paragraphs', async () => {
|
||||
test("should add breaks under headings with extra paragraphs", async () => {
|
||||
const document = await buildDocument({
|
||||
version: 1,
|
||||
text: `# Heading
|
||||
@@ -60,7 +60,7 @@ paragraph`,
|
||||
paragraph`);
|
||||
});
|
||||
|
||||
test('should add breaks between paragraphs', async () => {
|
||||
test("should add breaks between paragraphs", async () => {
|
||||
const document = await buildDocument({
|
||||
version: 1,
|
||||
text: `paragraph
|
||||
@@ -74,7 +74,7 @@ paragraph`,
|
||||
paragraph`);
|
||||
});
|
||||
|
||||
test('should add breaks for multiple empty paragraphs', async () => {
|
||||
test("should add breaks for multiple empty paragraphs", async () => {
|
||||
const document = await buildDocument({
|
||||
version: 1,
|
||||
text: `paragraph
|
||||
@@ -90,7 +90,7 @@ paragraph`,
|
||||
paragraph`);
|
||||
});
|
||||
|
||||
test('should add breaks with non-latin characters', async () => {
|
||||
test("should add breaks with non-latin characters", async () => {
|
||||
const document = await buildDocument({
|
||||
version: 1,
|
||||
text: `除。
|
||||
@@ -104,7 +104,7 @@ paragraph`);
|
||||
通`);
|
||||
});
|
||||
|
||||
test('should update task list formatting', async () => {
|
||||
test("should update task list formatting", async () => {
|
||||
const document = await buildDocument({
|
||||
version: 1,
|
||||
text: `[ ] list item
|
||||
@@ -115,7 +115,7 @@ paragraph`);
|
||||
`);
|
||||
});
|
||||
|
||||
test('should update task list with multiple items', async () => {
|
||||
test("should update task list with multiple items", async () => {
|
||||
const document = await buildDocument({
|
||||
version: 1,
|
||||
text: `[ ] list item
|
||||
@@ -128,7 +128,7 @@ paragraph`);
|
||||
`);
|
||||
});
|
||||
|
||||
test('should update checked task list formatting', async () => {
|
||||
test("should update checked task list formatting", async () => {
|
||||
const document = await buildDocument({
|
||||
version: 1,
|
||||
text: `[x] list item
|
||||
@@ -139,7 +139,7 @@ paragraph`);
|
||||
`);
|
||||
});
|
||||
|
||||
test('should update nested task list formatting', async () => {
|
||||
test("should update nested task list formatting", async () => {
|
||||
const document = await buildDocument({
|
||||
version: 1,
|
||||
text: `[x] list item
|
||||
@@ -155,22 +155,22 @@ paragraph`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#searchForTeam', () => {
|
||||
test('should return search results from public collections', async () => {
|
||||
describe("#searchForTeam", () => {
|
||||
test("should return search results from public collections", async () => {
|
||||
const team = await buildTeam();
|
||||
const collection = await buildCollection({ teamId: team.id });
|
||||
const document = await buildDocument({
|
||||
teamId: team.id,
|
||||
collectionId: collection.id,
|
||||
title: 'test',
|
||||
title: "test",
|
||||
});
|
||||
|
||||
const results = await Document.searchForTeam(team, 'test');
|
||||
const results = await Document.searchForTeam(team, "test");
|
||||
expect(results.length).toBe(1);
|
||||
expect(results[0].document.id).toBe(document.id);
|
||||
});
|
||||
|
||||
test('should not return search results from private collections', async () => {
|
||||
test("should not return search results from private collections", async () => {
|
||||
const team = await buildTeam();
|
||||
const collection = await buildCollection({
|
||||
private: true,
|
||||
@@ -179,16 +179,16 @@ describe('#searchForTeam', () => {
|
||||
await buildDocument({
|
||||
teamId: team.id,
|
||||
collectionId: collection.id,
|
||||
title: 'test',
|
||||
title: "test",
|
||||
});
|
||||
|
||||
const results = await Document.searchForTeam(team, 'test');
|
||||
const results = await Document.searchForTeam(team, "test");
|
||||
expect(results.length).toBe(0);
|
||||
});
|
||||
|
||||
test('should handle no collections', async () => {
|
||||
test("should handle no collections", async () => {
|
||||
const team = await buildTeam();
|
||||
const results = await Document.searchForTeam(team, 'test');
|
||||
const results = await Document.searchForTeam(team, "test");
|
||||
expect(results.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// @flow
|
||||
import { DataTypes, sequelize } from '../sequelize';
|
||||
import events from '../events';
|
||||
import { DataTypes, sequelize } from "../sequelize";
|
||||
import events from "../events";
|
||||
|
||||
const Event = sequelize.define('event', {
|
||||
const Event = sequelize.define("event", {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
@@ -16,31 +16,31 @@ const Event = sequelize.define('event', {
|
||||
|
||||
Event.associate = models => {
|
||||
Event.belongsTo(models.User, {
|
||||
as: 'user',
|
||||
foreignKey: 'userId',
|
||||
as: "user",
|
||||
foreignKey: "userId",
|
||||
});
|
||||
Event.belongsTo(models.User, {
|
||||
as: 'actor',
|
||||
foreignKey: 'actorId',
|
||||
as: "actor",
|
||||
foreignKey: "actorId",
|
||||
});
|
||||
Event.belongsTo(models.Collection, {
|
||||
as: 'collection',
|
||||
foreignKey: 'collectionId',
|
||||
as: "collection",
|
||||
foreignKey: "collectionId",
|
||||
});
|
||||
Event.belongsTo(models.Collection, {
|
||||
as: 'document',
|
||||
foreignKey: 'documentId',
|
||||
as: "document",
|
||||
foreignKey: "documentId",
|
||||
});
|
||||
Event.belongsTo(models.Team, {
|
||||
as: 'team',
|
||||
foreignKey: 'teamId',
|
||||
as: "team",
|
||||
foreignKey: "teamId",
|
||||
});
|
||||
};
|
||||
|
||||
Event.beforeCreate(event => {
|
||||
if (event.ip) {
|
||||
// cleanup IPV6 representations of IPV4 addresses
|
||||
event.ip = event.ip.replace(/^::ffff:/, '');
|
||||
event.ip = event.ip.replace(/^::ffff:/, "");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -49,48 +49,48 @@ Event.afterCreate(event => {
|
||||
});
|
||||
|
||||
Event.ACTIVITY_EVENTS = [
|
||||
'users.create',
|
||||
'documents.publish',
|
||||
'documents.archive',
|
||||
'documents.unarchive',
|
||||
'documents.pin',
|
||||
'documents.unpin',
|
||||
'documents.delete',
|
||||
'documents.restore',
|
||||
'collections.create',
|
||||
'collections.delete',
|
||||
"users.create",
|
||||
"documents.publish",
|
||||
"documents.archive",
|
||||
"documents.unarchive",
|
||||
"documents.pin",
|
||||
"documents.unpin",
|
||||
"documents.delete",
|
||||
"documents.restore",
|
||||
"collections.create",
|
||||
"collections.delete",
|
||||
];
|
||||
|
||||
Event.AUDIT_EVENTS = [
|
||||
'api_keys.create',
|
||||
'api_keys.delete',
|
||||
'users.create',
|
||||
'users.promote',
|
||||
'users.demote',
|
||||
'users.invite',
|
||||
'users.suspend',
|
||||
'users.activate',
|
||||
'users.delete',
|
||||
'documents.publish',
|
||||
'documents.update',
|
||||
'documents.archive',
|
||||
'documents.unarchive',
|
||||
'documents.pin',
|
||||
'documents.unpin',
|
||||
'documents.move',
|
||||
'documents.delete',
|
||||
'shares.create',
|
||||
'shares.revoke',
|
||||
'groups.create',
|
||||
'groups.update',
|
||||
'groups.delete',
|
||||
'collections.create',
|
||||
'collections.update',
|
||||
'collections.add_user',
|
||||
'collections.remove_user',
|
||||
'collections.add_group',
|
||||
'collections.remove_group',
|
||||
'collections.delete',
|
||||
"api_keys.create",
|
||||
"api_keys.delete",
|
||||
"users.create",
|
||||
"users.promote",
|
||||
"users.demote",
|
||||
"users.invite",
|
||||
"users.suspend",
|
||||
"users.activate",
|
||||
"users.delete",
|
||||
"documents.publish",
|
||||
"documents.update",
|
||||
"documents.archive",
|
||||
"documents.unarchive",
|
||||
"documents.pin",
|
||||
"documents.unpin",
|
||||
"documents.move",
|
||||
"documents.delete",
|
||||
"shares.create",
|
||||
"shares.revoke",
|
||||
"groups.create",
|
||||
"groups.update",
|
||||
"groups.delete",
|
||||
"collections.create",
|
||||
"collections.update",
|
||||
"collections.add_user",
|
||||
"collections.remove_user",
|
||||
"collections.add_group",
|
||||
"collections.remove_group",
|
||||
"collections.delete",
|
||||
];
|
||||
|
||||
export default Event;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// @flow
|
||||
import { Op, DataTypes, sequelize } from '../sequelize';
|
||||
import { CollectionGroup, GroupUser } from '../models';
|
||||
import { Op, DataTypes, sequelize } from "../sequelize";
|
||||
import { CollectionGroup, GroupUser } from "../models";
|
||||
|
||||
const Group = sequelize.define(
|
||||
'group',
|
||||
"group",
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
@@ -32,7 +32,7 @@ const Group = sequelize.define(
|
||||
},
|
||||
});
|
||||
if (foundItem) {
|
||||
throw new Error('The name of this group is already in use');
|
||||
throw new Error("The name of this group is already in use");
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -41,39 +41,39 @@ const Group = sequelize.define(
|
||||
|
||||
Group.associate = models => {
|
||||
Group.hasMany(models.GroupUser, {
|
||||
as: 'groupMemberships',
|
||||
foreignKey: 'groupId',
|
||||
as: "groupMemberships",
|
||||
foreignKey: "groupId",
|
||||
});
|
||||
Group.hasMany(models.CollectionGroup, {
|
||||
as: 'collectionGroupMemberships',
|
||||
foreignKey: 'groupId',
|
||||
as: "collectionGroupMemberships",
|
||||
foreignKey: "groupId",
|
||||
});
|
||||
Group.belongsTo(models.Team, {
|
||||
as: 'team',
|
||||
foreignKey: 'teamId',
|
||||
as: "team",
|
||||
foreignKey: "teamId",
|
||||
});
|
||||
Group.belongsTo(models.User, {
|
||||
as: 'createdBy',
|
||||
foreignKey: 'createdById',
|
||||
as: "createdBy",
|
||||
foreignKey: "createdById",
|
||||
});
|
||||
Group.belongsToMany(models.User, {
|
||||
as: 'users',
|
||||
as: "users",
|
||||
through: models.GroupUser,
|
||||
foreignKey: 'groupId',
|
||||
foreignKey: "groupId",
|
||||
});
|
||||
Group.addScope('defaultScope', {
|
||||
Group.addScope("defaultScope", {
|
||||
include: [
|
||||
{
|
||||
association: 'groupMemberships',
|
||||
association: "groupMemberships",
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
order: [['name', 'ASC']],
|
||||
order: [["name", "ASC"]],
|
||||
});
|
||||
};
|
||||
|
||||
// Cascade deletes to group and collection relations
|
||||
Group.addHook('afterDestroy', async (group, options) => {
|
||||
Group.addHook("afterDestroy", async (group, options) => {
|
||||
if (!group.deletedAt) return;
|
||||
|
||||
await GroupUser.destroy({ where: { groupId: group.id } });
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/* eslint-disable flowtype/require-valid-file-annotation */
|
||||
import { flushdb } from '../test/support';
|
||||
import { CollectionGroup, GroupUser } from '../models';
|
||||
import { buildUser, buildGroup, buildCollection } from '../test/factories';
|
||||
import { flushdb } from "../test/support";
|
||||
import { CollectionGroup, GroupUser } from "../models";
|
||||
import { buildUser, buildGroup, buildCollection } from "../test/factories";
|
||||
|
||||
beforeEach(flushdb);
|
||||
beforeEach(jest.resetAllMocks);
|
||||
|
||||
describe('afterDestroy hook', () => {
|
||||
test('should destroy associated group and collection join relations', async () => {
|
||||
describe("afterDestroy hook", () => {
|
||||
test("should destroy associated group and collection join relations", async () => {
|
||||
const group = await buildGroup();
|
||||
const teamId = group.teamId;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// @flow
|
||||
import { sequelize } from '../sequelize';
|
||||
import { sequelize } from "../sequelize";
|
||||
|
||||
const GroupUser = sequelize.define(
|
||||
'group_user',
|
||||
"group_user",
|
||||
{},
|
||||
{
|
||||
timestamps: true,
|
||||
@@ -12,21 +12,21 @@ const GroupUser = sequelize.define(
|
||||
|
||||
GroupUser.associate = models => {
|
||||
GroupUser.belongsTo(models.Group, {
|
||||
as: 'group',
|
||||
foreignKey: 'groupId',
|
||||
as: "group",
|
||||
foreignKey: "groupId",
|
||||
primary: true,
|
||||
});
|
||||
GroupUser.belongsTo(models.User, {
|
||||
as: 'user',
|
||||
foreignKey: 'userId',
|
||||
as: "user",
|
||||
foreignKey: "userId",
|
||||
primary: true,
|
||||
});
|
||||
GroupUser.belongsTo(models.User, {
|
||||
as: 'createdBy',
|
||||
foreignKey: 'createdById',
|
||||
as: "createdBy",
|
||||
foreignKey: "createdById",
|
||||
});
|
||||
GroupUser.addScope('defaultScope', {
|
||||
include: [{ association: 'user' }],
|
||||
GroupUser.addScope("defaultScope", {
|
||||
include: [{ association: "user" }],
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
import { DataTypes, sequelize } from '../sequelize';
|
||||
import { DataTypes, sequelize } from "../sequelize";
|
||||
|
||||
const Integration = sequelize.define('integration', {
|
||||
const Integration = sequelize.define("integration", {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
@@ -15,20 +15,20 @@ const Integration = sequelize.define('integration', {
|
||||
|
||||
Integration.associate = models => {
|
||||
Integration.belongsTo(models.User, {
|
||||
as: 'user',
|
||||
foreignKey: 'userId',
|
||||
as: "user",
|
||||
foreignKey: "userId",
|
||||
});
|
||||
Integration.belongsTo(models.Team, {
|
||||
as: 'team',
|
||||
foreignKey: 'teamId',
|
||||
as: "team",
|
||||
foreignKey: "teamId",
|
||||
});
|
||||
Integration.belongsTo(models.Collection, {
|
||||
as: 'collection',
|
||||
foreignKey: 'collectionId',
|
||||
as: "collection",
|
||||
foreignKey: "collectionId",
|
||||
});
|
||||
Integration.belongsTo(models.Authentication, {
|
||||
as: 'authentication',
|
||||
foreignKey: 'authenticationId',
|
||||
as: "authentication",
|
||||
foreignKey: "authenticationId",
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// @flow
|
||||
import { DataTypes, sequelize } from '../sequelize';
|
||||
import { DataTypes, sequelize } from "../sequelize";
|
||||
|
||||
const Notification = sequelize.define(
|
||||
'notification',
|
||||
"notification",
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
@@ -24,12 +24,12 @@ const Notification = sequelize.define(
|
||||
|
||||
Notification.associate = models => {
|
||||
Notification.belongsTo(models.User, {
|
||||
as: 'actor',
|
||||
foreignKey: 'actorId',
|
||||
as: "actor",
|
||||
foreignKey: "actorId",
|
||||
});
|
||||
Notification.belongsTo(models.User, {
|
||||
as: 'user',
|
||||
foreignKey: 'userId',
|
||||
as: "user",
|
||||
foreignKey: "userId",
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// @flow
|
||||
import crypto from 'crypto';
|
||||
import { DataTypes, sequelize } from '../sequelize';
|
||||
import crypto from "crypto";
|
||||
import { DataTypes, sequelize } from "../sequelize";
|
||||
|
||||
const NotificationSetting = sequelize.define(
|
||||
'notification_setting',
|
||||
"notification_setting",
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
@@ -15,11 +15,11 @@ const NotificationSetting = sequelize.define(
|
||||
validate: {
|
||||
isIn: [
|
||||
[
|
||||
'documents.publish',
|
||||
'documents.update',
|
||||
'collections.create',
|
||||
'emails.onboarding',
|
||||
'emails.features',
|
||||
"documents.publish",
|
||||
"documents.update",
|
||||
"collections.create",
|
||||
"emails.onboarding",
|
||||
"emails.features",
|
||||
],
|
||||
],
|
||||
},
|
||||
@@ -43,20 +43,20 @@ const NotificationSetting = sequelize.define(
|
||||
);
|
||||
|
||||
NotificationSetting.getUnsubscribeToken = userId => {
|
||||
const hash = crypto.createHash('sha256');
|
||||
const hash = crypto.createHash("sha256");
|
||||
hash.update(`${userId}-${process.env.SECRET_KEY}`);
|
||||
return hash.digest('hex');
|
||||
return hash.digest("hex");
|
||||
};
|
||||
|
||||
NotificationSetting.associate = models => {
|
||||
NotificationSetting.belongsTo(models.User, {
|
||||
as: 'user',
|
||||
foreignKey: 'userId',
|
||||
onDelete: 'cascade',
|
||||
as: "user",
|
||||
foreignKey: "userId",
|
||||
onDelete: "cascade",
|
||||
});
|
||||
NotificationSetting.belongsTo(models.Team, {
|
||||
as: 'team',
|
||||
foreignKey: 'teamId',
|
||||
as: "team",
|
||||
foreignKey: "teamId",
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// @flow
|
||||
import { DataTypes, sequelize } from '../sequelize';
|
||||
import MarkdownSerializer from 'slate-md-serializer';
|
||||
import { DataTypes, sequelize } from "../sequelize";
|
||||
import MarkdownSerializer from "slate-md-serializer";
|
||||
|
||||
const serializer = new MarkdownSerializer();
|
||||
|
||||
const Revision = sequelize.define('revision', {
|
||||
const Revision = sequelize.define("revision", {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
@@ -23,18 +23,18 @@ const Revision = sequelize.define('revision', {
|
||||
|
||||
Revision.associate = models => {
|
||||
Revision.belongsTo(models.Document, {
|
||||
as: 'document',
|
||||
foreignKey: 'documentId',
|
||||
onDelete: 'cascade',
|
||||
as: "document",
|
||||
foreignKey: "documentId",
|
||||
onDelete: "cascade",
|
||||
});
|
||||
Revision.belongsTo(models.User, {
|
||||
as: 'user',
|
||||
foreignKey: 'userId',
|
||||
as: "user",
|
||||
foreignKey: "userId",
|
||||
});
|
||||
Revision.addScope(
|
||||
'defaultScope',
|
||||
"defaultScope",
|
||||
{
|
||||
include: [{ model: models.User, as: 'user', paranoid: false }],
|
||||
include: [{ model: models.User, as: "user", paranoid: false }],
|
||||
},
|
||||
{ override: true }
|
||||
);
|
||||
@@ -46,7 +46,7 @@ Revision.prototype.migrateVersion = function() {
|
||||
// migrate from document version 0 -> 1
|
||||
if (!this.version) {
|
||||
// removing the title from the document text attribute
|
||||
this.text = this.text.replace(/^#\s(.*)\n/, '');
|
||||
this.text = this.text.replace(/^#\s(.*)\n/, "");
|
||||
this.version = 1;
|
||||
migrated = true;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// @flow
|
||||
import { DataTypes, sequelize } from '../sequelize';
|
||||
import { DataTypes, sequelize } from "../sequelize";
|
||||
|
||||
const Share = sequelize.define(
|
||||
'share',
|
||||
"share",
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
@@ -23,16 +23,16 @@ const Share = sequelize.define(
|
||||
|
||||
Share.associate = models => {
|
||||
Share.belongsTo(models.User, {
|
||||
as: 'user',
|
||||
foreignKey: 'userId',
|
||||
as: "user",
|
||||
foreignKey: "userId",
|
||||
});
|
||||
Share.belongsTo(models.Team, {
|
||||
as: 'team',
|
||||
foreignKey: 'teamId',
|
||||
as: "team",
|
||||
foreignKey: "teamId",
|
||||
});
|
||||
Share.belongsTo(models.Document, {
|
||||
as: 'document',
|
||||
foreignKey: 'documentId',
|
||||
as: "document",
|
||||
foreignKey: "documentId",
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
import { DataTypes, sequelize } from '../sequelize';
|
||||
import { DataTypes, sequelize } from "../sequelize";
|
||||
|
||||
const Star = sequelize.define('star', {
|
||||
const Star = sequelize.define("star", {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
// @flow
|
||||
import uuid from 'uuid';
|
||||
import { URL } from 'url';
|
||||
import fs from 'fs';
|
||||
import util from 'util';
|
||||
import path from 'path';
|
||||
import { DataTypes, sequelize, Op } from '../sequelize';
|
||||
import { publicS3Endpoint, uploadToS3FromUrl } from '../utils/s3';
|
||||
import uuid from "uuid";
|
||||
import { URL } from "url";
|
||||
import fs from "fs";
|
||||
import util from "util";
|
||||
import path from "path";
|
||||
import { DataTypes, sequelize, Op } from "../sequelize";
|
||||
import { publicS3Endpoint, uploadToS3FromUrl } from "../utils/s3";
|
||||
import {
|
||||
stripSubdomain,
|
||||
RESERVED_SUBDOMAINS,
|
||||
} from '../../shared/utils/domains';
|
||||
import { ValidationError } from '../errors';
|
||||
} from "../../shared/utils/domains";
|
||||
import { ValidationError } from "../errors";
|
||||
|
||||
import Collection from './Collection';
|
||||
import Document from './Document';
|
||||
import User from './User';
|
||||
import Collection from "./Collection";
|
||||
import Document from "./Document";
|
||||
import User from "./User";
|
||||
|
||||
const readFile = util.promisify(fs.readFile);
|
||||
|
||||
const Team = sequelize.define(
|
||||
'team',
|
||||
"team",
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
@@ -33,16 +33,16 @@ const Team = sequelize.define(
|
||||
validate: {
|
||||
isLowercase: true,
|
||||
is: {
|
||||
args: [/^[a-z\d-]+$/, 'i'],
|
||||
msg: 'Must be only alphanumeric and dashes',
|
||||
args: [/^[a-z\d-]+$/, "i"],
|
||||
msg: "Must be only alphanumeric and dashes",
|
||||
},
|
||||
len: {
|
||||
args: [4, 32],
|
||||
msg: 'Must be between 4 and 32 characters',
|
||||
msg: "Must be between 4 and 32 characters",
|
||||
},
|
||||
notIn: {
|
||||
args: [RESERVED_SUBDOMAINS],
|
||||
msg: 'You chose a restricted word, please try another.',
|
||||
msg: "You chose a restricted word, please try another.",
|
||||
},
|
||||
},
|
||||
unique: true,
|
||||
@@ -66,13 +66,13 @@ const Team = sequelize.define(
|
||||
{
|
||||
getterMethods: {
|
||||
url() {
|
||||
if (!this.subdomain || process.env.SUBDOMAINS_ENABLED !== 'true') {
|
||||
if (!this.subdomain || process.env.SUBDOMAINS_ENABLED !== "true") {
|
||||
return process.env.URL;
|
||||
}
|
||||
|
||||
const url = new URL(process.env.URL);
|
||||
url.host = `${this.subdomain}.${stripSubdomain(url.host)}`;
|
||||
return url.href.replace(/\/$/, '');
|
||||
return url.href.replace(/\/$/, "");
|
||||
},
|
||||
logoUrl() {
|
||||
return (
|
||||
@@ -84,9 +84,9 @@ const Team = sequelize.define(
|
||||
);
|
||||
|
||||
Team.associate = models => {
|
||||
Team.hasMany(models.Collection, { as: 'collections' });
|
||||
Team.hasMany(models.Document, { as: 'documents' });
|
||||
Team.hasMany(models.User, { as: 'users' });
|
||||
Team.hasMany(models.Collection, { as: "collections" });
|
||||
Team.hasMany(models.Document, { as: "documents" });
|
||||
Team.hasMany(models.User, { as: "users" });
|
||||
};
|
||||
|
||||
const uploadAvatar = async model => {
|
||||
@@ -95,14 +95,14 @@ const uploadAvatar = async model => {
|
||||
|
||||
if (
|
||||
avatarUrl &&
|
||||
!avatarUrl.startsWith('/api') &&
|
||||
!avatarUrl.startsWith("/api") &&
|
||||
!avatarUrl.startsWith(endpoint)
|
||||
) {
|
||||
try {
|
||||
const newUrl = await uploadToS3FromUrl(
|
||||
avatarUrl,
|
||||
`avatars/${model.id}/${uuid.v4()}`,
|
||||
'public-read'
|
||||
"public-read"
|
||||
);
|
||||
if (newUrl) model.avatarUrl = newUrl;
|
||||
} catch (err) {
|
||||
@@ -131,10 +131,10 @@ Team.prototype.provisionSubdomain = async function(subdomain) {
|
||||
|
||||
Team.prototype.provisionFirstCollection = async function(userId) {
|
||||
const collection = await Collection.create({
|
||||
name: 'Welcome',
|
||||
name: "Welcome",
|
||||
description:
|
||||
'This collection is a quick guide to what Outline is all about. Feel free to delete this collection once your team is up to speed with the basics!',
|
||||
type: 'atlas',
|
||||
"This collection is a quick guide to what Outline is all about. Feel free to delete this collection once your team is up to speed with the basics!",
|
||||
type: "atlas",
|
||||
teamId: this.id,
|
||||
creatorId: userId,
|
||||
});
|
||||
@@ -142,15 +142,15 @@ Team.prototype.provisionFirstCollection = async function(userId) {
|
||||
// For the first collection we go ahead and create some intitial documents to get
|
||||
// the team started. You can edit these in /server/onboarding/x.md
|
||||
const onboardingDocs = [
|
||||
'❤️ Support',
|
||||
'🚀 Integrations & API',
|
||||
'📝 Our Editor',
|
||||
'👋 What is Outline',
|
||||
"❤️ Support",
|
||||
"🚀 Integrations & API",
|
||||
"📝 Our Editor",
|
||||
"👋 What is Outline",
|
||||
];
|
||||
for (const title of onboardingDocs) {
|
||||
const text = await readFile(
|
||||
path.join(__dirname, '..', 'onboarding', `${title}.md`),
|
||||
'utf8'
|
||||
path.join(__dirname, "..", "onboarding", `${title}.md`),
|
||||
"utf8"
|
||||
);
|
||||
const document = await Document.create({
|
||||
version: 1,
|
||||
@@ -186,13 +186,13 @@ Team.prototype.removeAdmin = async function(user: User) {
|
||||
if (res.count >= 1) {
|
||||
return user.update({ isAdmin: false });
|
||||
} else {
|
||||
throw new ValidationError('At least one admin is required');
|
||||
throw new ValidationError("At least one admin is required");
|
||||
}
|
||||
};
|
||||
|
||||
Team.prototype.suspendUser = async function(user: User, admin: User) {
|
||||
if (user.id === admin.id)
|
||||
throw new ValidationError('Unable to suspend the current user');
|
||||
throw new ValidationError("Unable to suspend the current user");
|
||||
return user.update({
|
||||
suspendedById: admin.id,
|
||||
suspendedAt: new Date(),
|
||||
@@ -208,7 +208,7 @@ Team.prototype.activateUser = async function(user: User, admin: User) {
|
||||
|
||||
Team.prototype.collectionIds = async function(paranoid: boolean = true) {
|
||||
let models = await Collection.findAll({
|
||||
attributes: ['id', 'private'],
|
||||
attributes: ["id", "private"],
|
||||
where: { teamId: this.id, private: false },
|
||||
paranoid,
|
||||
});
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
/* eslint-disable flowtype/require-valid-file-annotation */
|
||||
import { flushdb } from '../test/support';
|
||||
import { buildTeam } from '../test/factories';
|
||||
import { flushdb } from "../test/support";
|
||||
import { buildTeam } from "../test/factories";
|
||||
|
||||
beforeEach(flushdb);
|
||||
|
||||
it('should set subdomain if available', async () => {
|
||||
it("should set subdomain if available", async () => {
|
||||
const team = await buildTeam();
|
||||
const subdomain = await team.provisionSubdomain('testy');
|
||||
expect(subdomain).toEqual('testy');
|
||||
expect(team.subdomain).toEqual('testy');
|
||||
const subdomain = await team.provisionSubdomain("testy");
|
||||
expect(subdomain).toEqual("testy");
|
||||
expect(team.subdomain).toEqual("testy");
|
||||
});
|
||||
|
||||
it('should set subdomain with append if unavailable', async () => {
|
||||
await buildTeam({ subdomain: 'myteam' });
|
||||
it("should set subdomain with append if unavailable", async () => {
|
||||
await buildTeam({ subdomain: "myteam" });
|
||||
|
||||
const team = await buildTeam();
|
||||
const subdomain = await team.provisionSubdomain('myteam');
|
||||
expect(subdomain).toEqual('myteam1');
|
||||
expect(team.subdomain).toEqual('myteam1');
|
||||
const subdomain = await team.provisionSubdomain("myteam");
|
||||
expect(subdomain).toEqual("myteam1");
|
||||
expect(team.subdomain).toEqual("myteam1");
|
||||
});
|
||||
|
||||
it('should do nothing if subdomain already set', async () => {
|
||||
const team = await buildTeam({ subdomain: 'example' });
|
||||
const subdomain = await team.provisionSubdomain('myteam');
|
||||
expect(subdomain).toEqual('example');
|
||||
expect(team.subdomain).toEqual('example');
|
||||
it("should do nothing if subdomain already set", async () => {
|
||||
const team = await buildTeam({ subdomain: "example" });
|
||||
const subdomain = await team.provisionSubdomain("myteam");
|
||||
expect(subdomain).toEqual("example");
|
||||
expect(team.subdomain).toEqual("example");
|
||||
});
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
// @flow
|
||||
import crypto from 'crypto';
|
||||
import uuid from 'uuid';
|
||||
import JWT from 'jsonwebtoken';
|
||||
import subMinutes from 'date-fns/sub_minutes';
|
||||
import { ValidationError } from '../errors';
|
||||
import { DataTypes, sequelize, encryptedFields } from '../sequelize';
|
||||
import { publicS3Endpoint, uploadToS3FromUrl } from '../utils/s3';
|
||||
import { sendEmail } from '../mailer';
|
||||
import { Star, Team, Collection, NotificationSetting, ApiKey } from '.';
|
||||
import crypto from "crypto";
|
||||
import uuid from "uuid";
|
||||
import JWT from "jsonwebtoken";
|
||||
import subMinutes from "date-fns/sub_minutes";
|
||||
import { ValidationError } from "../errors";
|
||||
import { DataTypes, sequelize, encryptedFields } from "../sequelize";
|
||||
import { publicS3Endpoint, uploadToS3FromUrl } from "../utils/s3";
|
||||
import { sendEmail } from "../mailer";
|
||||
import { Star, Team, Collection, NotificationSetting, ApiKey } from ".";
|
||||
|
||||
const DEFAULT_AVATAR_HOST = 'https://tiley.herokuapp.com';
|
||||
const DEFAULT_AVATAR_HOST = "https://tiley.herokuapp.com";
|
||||
|
||||
const User = sequelize.define(
|
||||
'user',
|
||||
"user",
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
@@ -27,7 +27,7 @@ const User = sequelize.define(
|
||||
service: { type: DataTypes.STRING, allowNull: true },
|
||||
serviceId: { type: DataTypes.STRING, allowNull: true, unique: true },
|
||||
slackData: DataTypes.JSONB,
|
||||
jwtSecret: encryptedFields.vault('jwtSecret'),
|
||||
jwtSecret: encryptedFields.vault("jwtSecret"),
|
||||
lastActiveAt: DataTypes.DATE,
|
||||
lastActiveIp: { type: DataTypes.STRING, allowNull: true },
|
||||
lastSignedInAt: DataTypes.DATE,
|
||||
@@ -43,15 +43,15 @@ const User = sequelize.define(
|
||||
return !!this.suspendedAt;
|
||||
},
|
||||
avatarUrl() {
|
||||
const original = this.getDataValue('avatarUrl');
|
||||
const original = this.getDataValue("avatarUrl");
|
||||
if (original) {
|
||||
return original;
|
||||
}
|
||||
|
||||
const hash = crypto
|
||||
.createHash('md5')
|
||||
.update(this.email || '')
|
||||
.digest('hex');
|
||||
.createHash("md5")
|
||||
.update(this.email || "")
|
||||
.digest("hex");
|
||||
return `${DEFAULT_AVATAR_HOST}/avatar/${hash}/${this.name[0]}.png`;
|
||||
},
|
||||
},
|
||||
@@ -60,22 +60,22 @@ const User = sequelize.define(
|
||||
|
||||
// Class methods
|
||||
User.associate = models => {
|
||||
User.hasMany(models.ApiKey, { as: 'apiKeys', onDelete: 'cascade' });
|
||||
User.hasMany(models.ApiKey, { as: "apiKeys", onDelete: "cascade" });
|
||||
User.hasMany(models.NotificationSetting, {
|
||||
as: 'notificationSettings',
|
||||
onDelete: 'cascade',
|
||||
as: "notificationSettings",
|
||||
onDelete: "cascade",
|
||||
});
|
||||
User.hasMany(models.Document, { as: 'documents' });
|
||||
User.hasMany(models.View, { as: 'views' });
|
||||
User.hasMany(models.Document, { as: "documents" });
|
||||
User.hasMany(models.View, { as: "views" });
|
||||
User.belongsTo(models.Team);
|
||||
};
|
||||
|
||||
// Instance methods
|
||||
User.prototype.collectionIds = async function(paranoid: boolean = true) {
|
||||
const collectionStubs = await Collection.scope({
|
||||
method: ['withMembership', this.id],
|
||||
method: ["withMembership", this.id],
|
||||
}).findAll({
|
||||
attributes: ['id', 'private'],
|
||||
attributes: ["id", "private"],
|
||||
where: { teamId: this.teamId },
|
||||
paranoid,
|
||||
});
|
||||
@@ -113,8 +113,8 @@ User.prototype.getJwtToken = function() {
|
||||
};
|
||||
|
||||
User.prototype.getEmailSigninToken = function() {
|
||||
if (this.service && this.service !== 'email') {
|
||||
throw new Error('Cannot generate email signin token for OAuth user');
|
||||
if (this.service && this.service !== "email") {
|
||||
throw new Error("Cannot generate email signin token for OAuth user");
|
||||
}
|
||||
|
||||
return JWT.sign(
|
||||
@@ -129,7 +129,7 @@ const uploadAvatar = async model => {
|
||||
|
||||
if (
|
||||
avatarUrl &&
|
||||
!avatarUrl.startsWith('/api') &&
|
||||
!avatarUrl.startsWith("/api") &&
|
||||
!avatarUrl.startsWith(endpoint) &&
|
||||
!avatarUrl.startsWith(DEFAULT_AVATAR_HOST)
|
||||
) {
|
||||
@@ -137,7 +137,7 @@ const uploadAvatar = async model => {
|
||||
const newUrl = await uploadToS3FromUrl(
|
||||
avatarUrl,
|
||||
`avatars/${model.id}/${uuid.v4()}`,
|
||||
'public-read'
|
||||
"public-read"
|
||||
);
|
||||
if (newUrl) model.avatarUrl = newUrl;
|
||||
} catch (err) {
|
||||
@@ -148,7 +148,7 @@ const uploadAvatar = async model => {
|
||||
};
|
||||
|
||||
const setRandomJwtSecret = model => {
|
||||
model.jwtSecret = crypto.randomBytes(64).toString('hex');
|
||||
model.jwtSecret = crypto.randomBytes(64).toString("hex");
|
||||
};
|
||||
|
||||
const removeIdentifyingInfo = async (model, options) => {
|
||||
@@ -166,8 +166,8 @@ const removeIdentifyingInfo = async (model, options) => {
|
||||
});
|
||||
|
||||
model.email = null;
|
||||
model.name = 'Unknown';
|
||||
model.avatarUrl = '';
|
||||
model.name = "Unknown";
|
||||
model.avatarUrl = "";
|
||||
model.serviceId = null;
|
||||
model.username = null;
|
||||
model.slackData = null;
|
||||
@@ -188,7 +188,7 @@ const checkLastAdmin = async model => {
|
||||
|
||||
if (userCount > 1 && adminCount <= 1) {
|
||||
throw new ValidationError(
|
||||
'Cannot delete account as only admin. Please transfer admin permissions to another user and try again.'
|
||||
"Cannot delete account as only admin. Please transfer admin permissions to another user and try again."
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -204,8 +204,8 @@ User.afterCreate(async user => {
|
||||
// From Slack support:
|
||||
// If you wish to contact users at an email address obtained through Slack,
|
||||
// you need them to opt-in through a clear and separate process.
|
||||
if (user.service && user.service !== 'slack') {
|
||||
sendEmail('welcome', user.email, { teamUrl: team.url });
|
||||
if (user.service && user.service !== "slack") {
|
||||
sendEmail("welcome", user.email, { teamUrl: team.url });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -217,7 +217,7 @@ User.afterCreate(async (user, options) => {
|
||||
where: {
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
event: 'documents.update',
|
||||
event: "documents.update",
|
||||
},
|
||||
transaction: options.transaction,
|
||||
}),
|
||||
@@ -225,7 +225,7 @@ User.afterCreate(async (user, options) => {
|
||||
where: {
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
event: 'emails.onboarding',
|
||||
event: "emails.onboarding",
|
||||
},
|
||||
transaction: options.transaction,
|
||||
}),
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/* eslint-disable flowtype/require-valid-file-annotation */
|
||||
import { flushdb } from '../test/support';
|
||||
import { buildUser } from '../test/factories';
|
||||
import { flushdb } from "../test/support";
|
||||
import { buildUser } from "../test/factories";
|
||||
|
||||
beforeEach(flushdb);
|
||||
|
||||
it('should set JWT secret', async () => {
|
||||
it("should set JWT secret", async () => {
|
||||
const user = await buildUser();
|
||||
expect(user.getJwtToken()).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// @flow
|
||||
import subMilliseconds from 'date-fns/sub_milliseconds';
|
||||
import { Op, DataTypes, sequelize } from '../sequelize';
|
||||
import { User } from '../models';
|
||||
import { USER_PRESENCE_INTERVAL } from '../../shared/constants';
|
||||
import subMilliseconds from "date-fns/sub_milliseconds";
|
||||
import { Op, DataTypes, sequelize } from "../sequelize";
|
||||
import { User } from "../models";
|
||||
import { USER_PRESENCE_INTERVAL } from "../../shared/constants";
|
||||
|
||||
const View = sequelize.define(
|
||||
'view',
|
||||
"view",
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
@@ -42,7 +42,7 @@ View.increment = async where => {
|
||||
View.findByDocument = async documentId => {
|
||||
return View.findAll({
|
||||
where: { documentId },
|
||||
order: [['updatedAt', 'DESC']],
|
||||
order: [["updatedAt", "DESC"]],
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
@@ -60,7 +60,7 @@ View.findRecentlyEditingByDocument = async documentId => {
|
||||
[Op.gt]: subMilliseconds(new Date(), USER_PRESENCE_INTERVAL * 2),
|
||||
},
|
||||
},
|
||||
order: [['lastEditingAt', 'DESC']],
|
||||
order: [["lastEditingAt", "DESC"]],
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
// @flow
|
||||
import ApiKey from './ApiKey';
|
||||
import Attachment from './Attachment';
|
||||
import Authentication from './Authentication';
|
||||
import Backlink from './Backlink';
|
||||
import Collection from './Collection';
|
||||
import CollectionUser from './CollectionUser';
|
||||
import CollectionGroup from './CollectionGroup';
|
||||
import Document from './Document';
|
||||
import Event from './Event';
|
||||
import Integration from './Integration';
|
||||
import Group from './Group';
|
||||
import GroupUser from './GroupUser';
|
||||
import Notification from './Notification';
|
||||
import NotificationSetting from './NotificationSetting';
|
||||
import Revision from './Revision';
|
||||
import Share from './Share';
|
||||
import Star from './Star';
|
||||
import Team from './Team';
|
||||
import User from './User';
|
||||
import View from './View';
|
||||
import ApiKey from "./ApiKey";
|
||||
import Attachment from "./Attachment";
|
||||
import Authentication from "./Authentication";
|
||||
import Backlink from "./Backlink";
|
||||
import Collection from "./Collection";
|
||||
import CollectionUser from "./CollectionUser";
|
||||
import CollectionGroup from "./CollectionGroup";
|
||||
import Document from "./Document";
|
||||
import Event from "./Event";
|
||||
import Integration from "./Integration";
|
||||
import Group from "./Group";
|
||||
import GroupUser from "./GroupUser";
|
||||
import Notification from "./Notification";
|
||||
import NotificationSetting from "./NotificationSetting";
|
||||
import Revision from "./Revision";
|
||||
import Share from "./Share";
|
||||
import Star from "./Star";
|
||||
import Team from "./Team";
|
||||
import User from "./User";
|
||||
import View from "./View";
|
||||
|
||||
const models = {
|
||||
ApiKey,
|
||||
@@ -45,7 +45,7 @@ const models = {
|
||||
|
||||
// based on https://github.com/sequelize/express-example/blob/master/models/index.js
|
||||
Object.keys(models).forEach(modelName => {
|
||||
if ('associate' in models[modelName]) {
|
||||
if ("associate" in models[modelName]) {
|
||||
models[modelName].associate(models);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user