chore: Move to Typescript (#2783)

This PR moves the entire project to Typescript. Due to the ~1000 ignores this will lead to a messy codebase for a while, but the churn is worth it – all of those ignore comments are places that were never type-safe previously.

closes #1282
This commit is contained in:
Tom Moor
2021-11-29 06:40:55 -08:00
committed by GitHub
parent 25ccfb5d04
commit 15b1069bcc
1017 changed files with 17410 additions and 54942 deletions

View File

@@ -1,4 +1,3 @@
// @flow
import randomstring from "randomstring";
import { DataTypes, sequelize } from "../sequelize";
@@ -11,11 +10,15 @@ const ApiKey = sequelize.define(
primaryKey: true,
},
name: DataTypes.STRING,
secret: { type: DataTypes.STRING, unique: true },
secret: {
type: DataTypes.STRING,
unique: true,
},
},
{
paranoid: true,
hooks: {
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'key' implicitly has an 'any' type.
beforeValidate: (key) => {
key.secret = randomstring.generate(38);
},
@@ -23,6 +26,7 @@ const ApiKey = sequelize.define(
}
);
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type.
ApiKey.associate = (models) => {
ApiKey.belongsTo(models.User, {
as: "user",

View File

@@ -1,7 +1,6 @@
// @flow
import path from "path";
import { deleteFromS3, getFileByKey } from "@server/utils/s3";
import { DataTypes, sequelize } from "../sequelize";
import { deleteFromS3, getFileByKey } from "../utils/s3";
const Attachment = sequelize.define(
"attachment",
@@ -55,8 +54,13 @@ const Attachment = sequelize.define(
);
Attachment.findAllInBatches = async (
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'query' implicitly has an 'any' type.
query,
callback: (attachments: Array<Attachment>, query: Object) => Promise<void>
callback: (
// @ts-expect-error ts-migrate(2749) FIXME: 'Attachment' refers to a value, but is being used ... Remove this comment to see the full error message
attachments: Array<Attachment>,
query: Record<string, any>
) => Promise<void>
) => {
if (!query.offset) query.offset = 0;
if (!query.limit) query.limit = 10;
@@ -64,16 +68,17 @@ Attachment.findAllInBatches = async (
do {
results = await Attachment.findAll(query);
await callback(results, query);
query.offset += query.limit;
} while (results.length >= query.limit);
};
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'model' implicitly has an 'any' type.
Attachment.beforeDestroy(async (model) => {
await deleteFromS3(model.key);
});
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type.
Attachment.associate = (models) => {
Attachment.belongsTo(models.Team);
Attachment.belongsTo(models.Document);

View File

@@ -1,5 +1,5 @@
// @flow
import { ValidationError } from "../errors";
// @ts-expect-error ts-migrate(7034) FIXME: Variable 'providers' implicitly has type 'any[]' i... Remove this comment to see the full error message
import providers from "../routes/auth/providers";
import { DataTypes, Op, sequelize } from "../sequelize";
@@ -14,6 +14,7 @@ const AuthenticationProvider = sequelize.define(
name: {
type: DataTypes.STRING,
validate: {
// @ts-expect-error ts-migrate(7005) FIXME: Variable 'providers' implicitly has an 'any[]' typ... Remove this comment to see the full error message
isIn: [providers.map((p) => p.id)],
},
},
@@ -31,6 +32,7 @@ const AuthenticationProvider = sequelize.define(
}
);
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type.
AuthenticationProvider.associate = (models) => {
AuthenticationProvider.belongsTo(models.Team);
AuthenticationProvider.hasMany(models.UserAuthentication);
@@ -49,16 +51,18 @@ AuthenticationProvider.prototype.disable = async function () {
});
if (res.count >= 1) {
return this.update({ enabled: false });
return this.update({
enabled: false,
});
} else {
throw new ValidationError(
"At least one authentication provider is required"
);
throw ValidationError("At least one authentication provider is required");
}
};
AuthenticationProvider.prototype.enable = async function () {
return this.update({ enabled: true });
return this.update({
enabled: true,
});
};
export default AuthenticationProvider;

View File

@@ -1,4 +1,3 @@
// @flow
import { DataTypes, sequelize } from "../sequelize";
const Backlink = sequelize.define("backlink", {
@@ -9,6 +8,7 @@ const Backlink = sequelize.define("backlink", {
},
});
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type.
Backlink.associate = (models) => {
Backlink.belongsTo(models.Document, {
as: "document",

View File

@@ -1,42 +1,36 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import randomstring from "randomstring";
import { v4 as uuidv4 } from "uuid";
import { Collection, Document } from "../models";
import { Collection, Document } from "@server/models";
import {
buildUser,
buildGroup,
buildCollection,
buildTeam,
buildDocument,
} from "../test/factories";
import { flushdb, seed } from "../test/support";
import slugify from "../utils/slugify";
} from "@server/test/factories";
import { flushdb, seed } from "@server/test/support";
import slugify from "@server/utils/slugify";
beforeEach(() => flushdb());
beforeEach(jest.resetAllMocks);
describe("#url", () => {
test("should return correct url for the collection", () => {
const collection = new Collection({ id: "1234" });
const collection = new Collection({
id: "1234",
});
expect(collection.url).toBe(`/collection/untitled-${collection.urlId}`);
});
});
describe("getDocumentParents", () => {
test("should return array of parent document ids", async () => {
const parent = await buildDocument();
const document = await buildDocument();
const collection = await buildCollection({
documentStructure: [
{
...parent.toJSON(),
children: [document.toJSON()],
},
{ ...parent.toJSON(), children: [document.toJSON()] },
],
});
const result = collection.getDocumentParents(document.id);
expect(result.length).toBe(1);
expect(result[0]).toBe(parent.id);
});
@@ -46,13 +40,9 @@ describe("getDocumentParents", () => {
const document = await buildDocument();
const collection = await buildCollection({
documentStructure: [
{
...parent.toJSON(),
children: [document.toJSON()],
},
{ ...parent.toJSON(), children: [document.toJSON()] },
],
});
const result = collection.getDocumentParents(parent.id);
expect(result.length).toBe(0);
});
@@ -61,19 +51,16 @@ describe("getDocumentParents", () => {
const parent = await buildDocument();
await buildDocument();
const collection = await buildCollection();
const result = collection.getDocumentParents(parent.id);
expect(result).toBe(undefined);
});
});
describe("getDocumentTree", () => {
test("should return document tree", async () => {
const document = await buildDocument();
const collection = await buildCollection({
documentStructure: [document.toJSON()],
});
expect(collection.getDocumentTree(document.id)).toEqual(document.toJSON());
});
@@ -82,13 +69,9 @@ describe("getDocumentTree", () => {
const document = await buildDocument();
const collection = await buildCollection({
documentStructure: [
{
...parent.toJSON(),
children: [document.toJSON()],
},
{ ...parent.toJSON(), children: [document.toJSON()] },
],
});
expect(collection.getDocumentTree(parent.id)).toEqual({
...parent.toJSON(),
children: [document.toJSON()],
@@ -96,14 +79,12 @@ describe("getDocumentTree", () => {
expect(collection.getDocumentTree(document.id)).toEqual(document.toJSON());
});
});
describe("isChildDocument", () => {
test("should return false with unexpected data", async () => {
const document = await buildDocument();
const collection = await buildCollection({
documentStructure: [document.toJSON()],
});
expect(collection.isChildDocument(document.id, document.id)).toEqual(false);
expect(collection.isChildDocument(document.id, undefined)).toEqual(false);
expect(collection.isChildDocument(undefined, document.id)).toEqual(false);
@@ -115,7 +96,6 @@ describe("isChildDocument", () => {
const collection = await buildCollection({
documentStructure: [one.toJSON(), document.toJSON()],
});
expect(collection.isChildDocument(one.id, document.id)).toEqual(false);
expect(collection.isChildDocument(document.id, one.id)).toEqual(false);
});
@@ -125,13 +105,9 @@ describe("isChildDocument", () => {
const document = await buildDocument();
const collection = await buildCollection({
documentStructure: [
{
...parent.toJSON(),
children: [document.toJSON()],
},
{ ...parent.toJSON(), children: [document.toJSON()] },
],
});
expect(collection.isChildDocument(parent.id, document.id)).toEqual(true);
expect(collection.isChildDocument(document.id, parent.id)).toEqual(false);
});
@@ -144,21 +120,14 @@ describe("isChildDocument", () => {
documentStructure: [
{
...parent.toJSON(),
children: [
{
...nested.toJSON(),
children: [document.toJSON()],
},
],
children: [{ ...nested.toJSON(), children: [document.toJSON()] }],
},
],
});
expect(collection.isChildDocument(parent.id, document.id)).toEqual(true);
expect(collection.isChildDocument(document.id, parent.id)).toEqual(false);
});
});
describe("#addDocumentToStructure", () => {
test("should add as last element without index", async () => {
const { collection } = await seed();
@@ -168,7 +137,6 @@ describe("#addDocumentToStructure", () => {
title: "New end node",
parentDocumentId: null,
});
await collection.addDocumentToStructure(newDocument);
expect(collection.documentStructure.length).toBe(2);
expect(collection.documentStructure[1].id).toBe(id);
@@ -182,7 +150,6 @@ describe("#addDocumentToStructure", () => {
title: "New end node",
parentDocumentId: null,
});
await collection.addDocumentToStructure(newDocument, 1);
expect(collection.documentStructure.length).toBe(2);
expect(collection.documentStructure[1].id).toBe(id);
@@ -196,7 +163,6 @@ describe("#addDocumentToStructure", () => {
title: "New end node",
parentDocumentId: document.id,
});
await collection.addDocumentToStructure(newDocument, 1);
expect(collection.documentStructure.length).toBe(1);
expect(collection.documentStructure[0].id).toBe(document.id);
@@ -217,7 +183,6 @@ describe("#addDocumentToStructure", () => {
title: "New start node",
parentDocumentId: document.id,
});
await collection.addDocumentToStructure(newDocument);
await collection.addDocumentToStructure(secondDocument, 0);
expect(collection.documentStructure.length).toBe(1);
@@ -225,7 +190,6 @@ describe("#addDocumentToStructure", () => {
expect(collection.documentStructure[0].children.length).toBe(2);
expect(collection.documentStructure[0].children[0].id).toBe(id);
});
describe("options: documentJson", () => {
test("should append supplied json over document's own", async () => {
const { collection } = await seed();
@@ -235,7 +199,6 @@ describe("#addDocumentToStructure", () => {
title: "New end node",
parentDocumentId: null,
});
await collection.addDocumentToStructure(newDocument, undefined, {
documentJson: {
children: [
@@ -252,16 +215,12 @@ describe("#addDocumentToStructure", () => {
});
});
});
describe("#updateDocument", () => {
test("should update root document's data", async () => {
const { collection, document } = await seed();
document.title = "Updated title";
await document.save();
await collection.updateDocument(document);
expect(collection.documentStructure[0].title).toBe("Updated title");
});
@@ -278,35 +237,27 @@ describe("#updateDocument", () => {
text: "content",
});
await collection.addDocumentToStructure(newDocument);
newDocument.title = "Updated title";
await newDocument.save();
await collection.updateDocument(newDocument);
const reloaded = await Collection.findByPk(collection.id);
expect(reloaded.documentStructure[0].children[0].title).toBe(
"Updated title"
);
});
});
describe("#removeDocument", () => {
test("should save if removing", async () => {
const { collection, document } = await seed();
jest.spyOn(collection, "save");
await collection.deleteDocument(document);
expect(collection.save).toBeCalled();
});
test("should remove documents from root", async () => {
const { collection, document } = await seed();
await collection.deleteDocument(document);
expect(collection.documentStructure.length).toBe(0);
// Verify that the document was removed
const collectionDocuments = await Document.findAndCountAll({
where: {
@@ -318,7 +269,6 @@ describe("#removeDocument", () => {
test("should remove a document with child documents", async () => {
const { collection, document } = await seed();
// Add a child for testing
const newDocument = await Document.create({
parentDocumentId: document.id,
@@ -332,7 +282,6 @@ describe("#removeDocument", () => {
});
await collection.addDocumentToStructure(newDocument);
expect(collection.documentStructure[0].children.length).toBe(1);
// Remove the document
await collection.deleteDocument(document);
expect(collection.documentStructure.length).toBe(0);
@@ -346,7 +295,6 @@ describe("#removeDocument", () => {
test("should remove a child document", async () => {
const { collection, document } = await seed();
// Add a child for testing
const newDocument = await Document.create({
parentDocumentId: document.id,
@@ -362,15 +310,11 @@ describe("#removeDocument", () => {
await collection.addDocumentToStructure(newDocument);
expect(collection.documentStructure.length).toBe(1);
expect(collection.documentStructure[0].children.length).toBe(1);
// Remove the document
await collection.deleteDocument(newDocument);
const reloaded = await Collection.findByPk(collection.id);
expect(reloaded.documentStructure.length).toBe(1);
expect(reloaded.documentStructure[0].children.length).toBe(0);
const collectionDocuments = await Document.findAndCountAll({
where: {
collectionId: collection.id,
@@ -379,76 +323,101 @@ describe("#removeDocument", () => {
expect(collectionDocuments.count).toBe(1);
});
});
describe("#membershipUserIds", () => {
test("should return collection and group memberships", async () => {
const team = await buildTeam();
const teamId = team.id;
// Make 6 users
const users = await Promise.all(
Array(6)
// @ts-expect-error ts-migrate(2554) FIXME: Expected 1-3 arguments, but got 0.
.fill()
.map(() => buildUser({ teamId }))
.map(() =>
buildUser({
teamId,
})
)
);
const collection = await buildCollection({
userId: users[0].id,
permission: null,
teamId,
});
const group1 = await buildGroup({ teamId });
const group2 = await buildGroup({ teamId });
const group1 = await buildGroup({
teamId,
});
const group2 = await buildGroup({
teamId,
});
const createdById = users[0].id;
await group1.addUser(users[0], { through: { createdById } });
await group1.addUser(users[1], { through: { createdById } });
await group2.addUser(users[2], { through: { createdById } });
await group2.addUser(users[3], { through: { createdById } });
await collection.addUser(users[4], { through: { createdById } });
await collection.addUser(users[5], { through: { createdById } });
await collection.addGroup(group1, { through: { createdById } });
await collection.addGroup(group2, { through: { createdById } });
await group1.addUser(users[0], {
through: {
createdById,
},
});
await group1.addUser(users[1], {
through: {
createdById,
},
});
await group2.addUser(users[2], {
through: {
createdById,
},
});
await group2.addUser(users[3], {
through: {
createdById,
},
});
await collection.addUser(users[4], {
through: {
createdById,
},
});
await collection.addUser(users[5], {
through: {
createdById,
},
});
await collection.addGroup(group1, {
through: {
createdById,
},
});
await collection.addGroup(group2, {
through: {
createdById,
},
});
const membershipUserIds = await Collection.membershipUserIds(collection.id);
expect(membershipUserIds.length).toBe(6);
});
});
describe("#findByPk", () => {
test("should return collection with collection Id", async () => {
const collection = await buildCollection();
const response = await Collection.findByPk(collection.id);
expect(response.id).toBe(collection.id);
});
test("should return collection when urlId is present", async () => {
const collection = await buildCollection();
const id = `${slugify(collection.name)}-${collection.urlId}`;
const response = await Collection.findByPk(id);
expect(response.id).toBe(collection.id);
});
test("should return undefined when incorrect uuid type", async () => {
const collection = await buildCollection();
const response = await Collection.findByPk(collection.id + "-incorrect");
expect(response).toBe(undefined);
});
test("should return undefined when incorrect urlId length", async () => {
const collection = await buildCollection();
const id = `${slugify(collection.name)}-${collection.urlId}incorrect`;
const response = await Collection.findByPk(id);
expect(response).toBe(undefined);
});
@@ -456,15 +425,12 @@ describe("#findByPk", () => {
const response = await Collection.findByPk(
"a9e71a81-7342-4ea3-9889-9b9cc8f667da"
);
expect(response).toBe(null);
});
test("should return null when no collection is found with urlId", async () => {
const id = `${slugify("test collection")}-${randomstring.generate(15)}`;
const response = await Collection.findByPk(id);
expect(response).toBe(null);
});
});

View File

@@ -1,10 +1,9 @@
// @flow
import { find, findIndex, concat, remove, uniq } from "lodash";
import randomstring from "randomstring";
import isUUID from "validator/lib/isUUID";
import { SLUG_URL_REGEX } from "../../shared/utils/routeHelpers";
import { SLUG_URL_REGEX } from "@shared/utils/routeHelpers";
import slugify from "@server/utils/slugify";
import { Op, DataTypes, sequelize } from "../sequelize";
import slugify from "../utils/slugify";
import CollectionUser from "./CollectionUser";
import Document from "./Document";
import User from "./User";
@@ -17,7 +16,10 @@ const Collection = sequelize.define(
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
urlId: { type: DataTypes.STRING, unique: true },
urlId: {
type: DataTypes.STRING,
unique: true,
},
name: DataTypes.STRING,
description: DataTypes.STRING,
icon: DataTypes.STRING,
@@ -44,6 +46,7 @@ const Collection = sequelize.define(
sort: {
type: DataTypes.JSONB,
validate: {
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'value' implicitly has an 'any' type.
isSort(value) {
if (
typeof value !== "object" ||
@@ -53,9 +56,11 @@ const Collection = sequelize.define(
) {
throw new Error("Sort must be an object with field,direction");
}
if (!["asc", "desc"].includes(value.direction)) {
throw new Error("Sort direction must be one of asc,desc");
}
if (!["title", "index"].includes(value.field)) {
throw new Error("Sort field must be one of title,index");
}
@@ -67,6 +72,7 @@ const Collection = sequelize.define(
tableName: "collections",
paranoid: true,
hooks: {
// @ts-expect-error ts-migrate(2749) FIXME: 'Collection' refers to a value, but is being used ... Remove this comment to see the full error message
beforeValidate: (collection: Collection) => {
collection.urlId = collection.urlId || randomstring.generate(10);
},
@@ -74,18 +80,16 @@ const Collection = sequelize.define(
getterMethods: {
url() {
if (!this.name) return `/collection/untitled-${this.urlId}`;
return `/collection/${slugify(this.name)}-${this.urlId}`;
},
},
}
);
Collection.DEFAULT_SORT = {
field: "index",
direction: "asc",
};
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'model' implicitly has an 'any' type.
Collection.addHook("beforeSave", async (model) => {
if (model.icon === "collection") {
model.icon = null;
@@ -93,7 +97,7 @@ Collection.addHook("beforeSave", async (model) => {
});
// Class methods
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type.
Collection.associate = (models) => {
Collection.hasMany(models.Document, {
as: "documents",
@@ -127,24 +131,25 @@ Collection.associate = (models) => {
Collection.belongsTo(models.Team, {
as: "team",
});
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'userId' implicitly has an 'any' type.
Collection.addScope("withMembership", (userId) => ({
include: [
{
model: models.CollectionUser,
as: "memberships",
where: { userId },
where: {
userId,
},
required: false,
},
{
model: models.CollectionGroup,
as: "collectionGroupMemberships",
required: false,
// use of "separate" property: sequelize breaks when there are
// nested "includes" with alternating values for "required"
// see https://github.com/sequelize/sequelize/issues/9869
separate: true,
// include for groups that are members of this collection,
// of which userId is a member of, resulting in:
// CollectionGroup [inner join] Group [inner join] GroupUser [where] userId
@@ -156,7 +161,9 @@ Collection.associate = (models) => {
model: models.GroupUser,
as: "groupMemberships",
required: true,
where: { userId },
where: {
userId,
},
},
},
},
@@ -173,12 +180,10 @@ Collection.associate = (models) => {
model: models.CollectionGroup,
as: "collectionGroupMemberships",
required: false,
// use of "separate" property: sequelize breaks when there are
// nested "includes" with alternating values for "required"
// see https://github.com/sequelize/sequelize/issues/9869
separate: true,
// include for groups that are members of this collection,
// of which userId is a member of, resulting in:
// CollectionGroup [inner join] Group [inner join] GroupUser [where] userId
@@ -197,6 +202,7 @@ Collection.associate = (models) => {
});
};
// @ts-expect-error ts-migrate(2749) FIXME: 'Collection' refers to a value, but is being used ... Remove this comment to see the full error message
Collection.addHook("afterDestroy", async (model: Collection) => {
await Document.destroy({
where: {
@@ -207,7 +213,7 @@ Collection.addHook("afterDestroy", async (model: Collection) => {
},
});
});
// @ts-expect-error ts-migrate(2749) FIXME: 'Collection' refers to a value, but is being used ... Remove this comment to see the full error message
Collection.addHook("afterCreate", (model: Collection, options) => {
if (model.permission !== "read_write") {
return CollectionUser.findOrCreate({
@@ -225,13 +231,20 @@ Collection.addHook("afterCreate", (model: Collection, options) => {
});
// Class methods
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'id' implicitly has an 'any' type.
Collection.findByPk = async function (id, options = {}) {
if (isUUID(id)) {
return this.findOne({ where: { id }, ...options });
return this.findOne({
where: {
id,
},
...options,
});
} else if (id.match(SLUG_URL_REGEX)) {
return this.findOne({
where: { urlId: id.match(SLUG_URL_REGEX)[1] },
where: {
urlId: id.match(SLUG_URL_REGEX)[1],
},
...options,
});
}
@@ -243,11 +256,13 @@ Collection.findByPk = async function (id, options = {}) {
* @param user User object
* @returns collection First collection in the sidebar order
*/
// @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message
Collection.findFirstCollectionForUser = async (user: User) => {
const id = await user.collectionIds();
return Collection.findOne({
where: { id },
where: {
id,
},
order: [
// using LC_COLLATE:"C" because we need byte order to drive the sorting
sequelize.literal('"collection"."index" collate "C"'),
@@ -267,19 +282,17 @@ Collection.membershipUserIds = async (collectionId: string) => {
}
const groupMemberships = collection.collectionGroupMemberships
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'cgm' implicitly has an 'any' type.
.map((cgm) => cgm.group.groupMemberships)
.flat();
const membershipUserIds = concat(
groupMemberships,
collection.memberships
).map((membership) => membership.userId);
return uniq(membershipUserIds);
};
// Instance methods
Collection.prototype.addDocumentToStructure = async function (
document: Document,
index: number,
@@ -293,16 +306,16 @@ Collection.prototype.addDocumentToStructure = async function (
try {
// documentStructure can only be updated by one request at a time
// @ts-expect-error ts-migrate(2339) FIXME: Property 'save' does not exist on type '{}'.
if (options.save !== false) {
transaction = await sequelize.transaction();
}
// If moving existing document with children, use existing structure
const documentJson = {
...document.toJSON(),
...options.documentJson,
};
// @ts-expect-error ts-migrate(2339) FIXME: Property 'toJSON' does not exist on type 'Document... Remove this comment to see the full error message
const documentJson = { ...document.toJSON(), ...options.documentJson };
// @ts-expect-error ts-migrate(2339) FIXME: Property 'parentDocumentId' does not exist on type... Remove this comment to see the full error message
if (!document.parentDocumentId) {
// Note: Index is supported on DB level but it's being ignored
// by the API presentation until we build product support for it.
@@ -313,8 +326,11 @@ Collection.prototype.addDocumentToStructure = async function (
);
} else {
// Recursively place document
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'documentList' implicitly has an 'any' t... Remove this comment to see the full error message
const placeDocument = (documentList) => {
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'childDocument' implicitly has an 'any' ... Remove this comment to see the full error message
return documentList.map((childDocument) => {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'parentDocumentId' does not exist on type... Remove this comment to see the full error message
if (document.parentDocumentId === childDocument.id) {
childDocument.children.splice(
index !== undefined ? index : childDocument.children.length,
@@ -328,6 +344,7 @@ Collection.prototype.addDocumentToStructure = async function (
return childDocument;
});
};
this.documentStructure = placeDocument(this.documentStructure);
}
@@ -335,12 +352,14 @@ Collection.prototype.addDocumentToStructure = async function (
// https://github.com/sequelize/sequelize/blob/e1446837196c07b8ff0c23359b958d68af40fd6d/src/model.js#L3937
this.changed("documentStructure", true);
// @ts-expect-error ts-migrate(2339) FIXME: Property 'save' does not exist on type '{}'.
if (options.save !== false) {
await this.save({
...options,
fields: ["documentStructure"],
transaction,
});
if (transaction) {
await transaction.commit();
}
@@ -349,6 +368,7 @@ Collection.prototype.addDocumentToStructure = async function (
if (transaction) {
await transaction.rollback();
}
throw err;
}
@@ -362,65 +382,76 @@ Collection.prototype.updateDocument = async function (
updatedDocument: Document
) {
if (!this.documentStructure) return;
let transaction;
try {
// documentStructure can only be updated by one request at the time
transaction = await sequelize.transaction();
// @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'Document'.
const { id } = updatedDocument;
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'documents' implicitly has an 'any' type... Remove this comment to see the full error message
const updateChildren = (documents) => {
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'document' implicitly has an 'any' type.
return documents.map((document) => {
if (document.id === id) {
document = {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'toJSON' does not exist on type 'Document... Remove this comment to see the full error message
...updatedDocument.toJSON(),
children: document.children,
};
} else {
document.children = updateChildren(document.children);
}
return document;
});
};
this.documentStructure = updateChildren(this.documentStructure);
// Sequelize doesn't seem to set the value with splice on JSONB field
// https://github.com/sequelize/sequelize/blob/e1446837196c07b8ff0c23359b958d68af40fd6d/src/model.js#L3937
this.changed("documentStructure", true);
await this.save({ fields: ["documentStructure"], transaction });
await this.save({
fields: ["documentStructure"],
transaction,
});
await transaction.commit();
} catch (err) {
if (transaction) {
await transaction.rollback();
}
throw err;
}
return this;
};
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'document' implicitly has an 'any' type.
Collection.prototype.deleteDocument = async function (document) {
await this.removeDocumentInStructure(document);
await document.deleteWithChildren();
};
Collection.prototype.isChildDocument = function (
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'parentDocumentId' implicitly has an 'an... Remove this comment to see the full error message
parentDocumentId,
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'documentId' implicitly has an 'any' typ... Remove this comment to see the full error message
documentId
): boolean {
let result = false;
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'documents' implicitly has an 'any' type... Remove this comment to see the full error message
const loopChildren = (documents, input) => {
if (result) {
return;
}
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'document' implicitly has an 'any' type.
documents.forEach((document) => {
let parents = [...input];
const parents = [...input];
if (document.id === documentId) {
result = parents.includes(parentDocumentId);
} else {
@@ -431,22 +462,27 @@ Collection.prototype.isChildDocument = function (
};
loopChildren(this.documentStructure, []);
return result;
};
Collection.prototype.getDocumentTree = function (documentId: string) {
// @ts-expect-error ts-migrate(7034) FIXME: Variable 'result' implicitly has type 'any' in som... Remove this comment to see the full error message
let result;
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'documents' implicitly has an 'any' type... Remove this comment to see the full error message
const loopChildren = (documents) => {
// @ts-expect-error ts-migrate(7005) FIXME: Variable 'result' implicitly has an 'any' type.
if (result) {
return;
}
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'document' implicitly has an 'any' type.
documents.forEach((document) => {
// @ts-expect-error ts-migrate(7005) FIXME: Variable 'result' implicitly has an 'any' type.
if (result) {
return;
}
if (document.id === documentId) {
result = document;
} else {
@@ -462,17 +498,22 @@ Collection.prototype.getDocumentTree = function (documentId: string) {
Collection.prototype.getDocumentParents = function (
documentId: string
): string[] | void {
// @ts-expect-error ts-migrate(7034) FIXME: Variable 'result' implicitly has type 'any' in som... Remove this comment to see the full error message
let result;
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'documents' implicitly has an 'any' type... Remove this comment to see the full error message
const loopChildren = (documents, path = []) => {
// @ts-expect-error ts-migrate(7005) FIXME: Variable 'result' implicitly has an 'any' type.
if (result) {
return;
}
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'document' implicitly has an 'any' type.
documents.forEach((document) => {
if (document.id === documentId) {
result = path;
} else {
// @ts-expect-error ts-migrate(2322) FIXME: Type 'any' is not assignable to type 'never'.
loopChildren(document.children, [...path, document.id]);
}
});
@@ -486,10 +527,13 @@ Collection.prototype.getDocumentParents = function (
};
Collection.prototype.removeDocumentInStructure = async function (
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'document' implicitly has an 'any' type.
document,
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'options' implicitly has an 'any' type.
options
) {
if (!this.documentStructure) return;
// @ts-expect-error ts-migrate(7034) FIXME: Variable 'returnValue' implicitly has type 'any' i... Remove this comment to see the full error message
let returnValue;
let transaction;
@@ -497,8 +541,10 @@ Collection.prototype.removeDocumentInStructure = async function (
// documentStructure can only be updated by one request at the time
transaction = await sequelize.transaction();
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'children' implicitly has an 'any' type.
const removeFromChildren = async (children, id) => {
children = await Promise.all(
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'childDocument' implicitly has an 'any' ... Remove this comment to see the full error message
children.map(async (childDocument) => {
return {
...childDocument,
@@ -506,11 +552,22 @@ Collection.prototype.removeDocumentInStructure = async function (
};
})
);
const match = find(children, {
id,
});
const match = find(children, { id });
if (match) {
if (!returnValue) returnValue = [match, findIndex(children, { id })];
remove(children, { id });
// @ts-expect-error ts-migrate(7005) FIXME: Variable 'returnValue' implicitly has an 'any' typ... Remove this comment to see the full error message
if (!returnValue)
returnValue = [
match,
findIndex(children, {
id,
}),
];
remove(children, {
id,
});
}
return children;
@@ -520,17 +577,16 @@ Collection.prototype.removeDocumentInStructure = async function (
this.documentStructure,
document.id
);
// Sequelize doesn't seem to set the value with splice on JSONB field
// https://github.com/sequelize/sequelize/blob/e1446837196c07b8ff0c23359b958d68af40fd6d/src/model.js#L3937
this.changed("documentStructure", true);
await this.save({ ...options, fields: ["documentStructure"], transaction });
await transaction.commit();
} catch (err) {
if (transaction) {
await transaction.rollback();
}
throw err;
}

View File

@@ -1,4 +1,3 @@
// @flow
import { DataTypes, sequelize } from "../sequelize";
const CollectionGroup = sequelize.define(
@@ -18,6 +17,7 @@ const CollectionGroup = sequelize.define(
}
);
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type.
CollectionGroup.associate = (models) => {
CollectionGroup.belongsTo(models.Collection, {
as: "collection",

View File

@@ -1,4 +1,3 @@
// @flow
import { DataTypes, sequelize } from "../sequelize";
const CollectionUser = sequelize.define(
@@ -17,6 +16,7 @@ const CollectionUser = sequelize.define(
}
);
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type.
CollectionUser.associate = (models) => {
CollectionUser.belongsTo(models.Collection, {
as: "collection",

View File

@@ -1,17 +1,15 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import { Document } from "../models";
import { Document } from "@server/models";
import {
buildDocument,
buildCollection,
buildTeam,
buildUser,
} from "../test/factories";
import { flushdb, seed } from "../test/support";
import slugify from "../utils/slugify";
} from "@server/test/factories";
import { flushdb, seed } from "@server/test/support";
import slugify from "@server/utils/slugify";
beforeEach(() => flushdb());
beforeEach(jest.resetAllMocks);
describe("#getSummary", () => {
test("should strip markdown", async () => {
const document = await buildDocument({
@@ -20,7 +18,6 @@ describe("#getSummary", () => {
paragraph 2`,
});
expect(document.getSummary()).toBe("paragraph");
});
@@ -31,11 +28,9 @@ paragraph 2`,
*paragraph*`,
});
expect(document.getSummary()).toBe("paragraph");
});
});
describe("#migrateVersion", () => {
test("should maintain empty paragraph under headings", async () => {
const document = await buildDocument({
@@ -160,17 +155,17 @@ paragraph`);
`);
});
});
describe("#searchForTeam", () => {
test("should return search results from public collections", async () => {
const team = await buildTeam();
const collection = await buildCollection({ teamId: team.id });
const collection = await buildCollection({
teamId: team.id,
});
const document = await buildDocument({
teamId: team.id,
collectionId: collection.id,
title: "test",
});
const { results } = await Document.searchForTeam(team, "test");
expect(results.length).toBe(1);
expect(results[0].document.id).toBe(document.id);
@@ -187,7 +182,6 @@ describe("#searchForTeam", () => {
collectionId: collection.id,
title: "test",
});
const { results } = await Document.searchForTeam(team, "test");
expect(results.length).toBe(0);
});
@@ -206,7 +200,9 @@ describe("#searchForTeam", () => {
test("should return the total count of search results", async () => {
const team = await buildTeam();
const collection = await buildCollection({ teamId: team.id });
const collection = await buildCollection({
teamId: team.id,
});
await buildDocument({
teamId: team.id,
collectionId: collection.id,
@@ -217,39 +213,38 @@ describe("#searchForTeam", () => {
collectionId: collection.id,
title: "test number 2",
});
const { totalCount } = await Document.searchForTeam(team, "test");
expect(totalCount).toBe("2");
});
test("should return the document when searched with their previous titles", async () => {
const team = await buildTeam();
const collection = await buildCollection({ teamId: team.id });
const collection = await buildCollection({
teamId: team.id,
});
const document = await buildDocument({
teamId: team.id,
collectionId: collection.id,
title: "test number 1",
});
document.title = "change";
await document.save();
const { totalCount } = await Document.searchForTeam(team, "test number");
expect(totalCount).toBe("1");
});
test("should not return the document when searched with neither the titles nor the previous titles", async () => {
const team = await buildTeam();
const collection = await buildCollection({ teamId: team.id });
const collection = await buildCollection({
teamId: team.id,
});
const document = await buildDocument({
teamId: team.id,
collectionId: collection.id,
title: "test number 1",
});
document.title = "change";
await document.save();
const { totalCount } = await Document.searchForTeam(
team,
"title doesn't exist"
@@ -257,11 +252,12 @@ describe("#searchForTeam", () => {
expect(totalCount).toBe("0");
});
});
describe("#searchForUser", () => {
test("should return search results from collections", async () => {
const team = await buildTeam();
const user = await buildUser({ teamId: team.id });
const user = await buildUser({
teamId: team.id,
});
const collection = await buildCollection({
userId: user.id,
teamId: team.id,
@@ -272,7 +268,6 @@ describe("#searchForUser", () => {
collectionId: collection.id,
title: "test",
});
const { results } = await Document.searchForUser(user, "test");
expect(results.length).toBe(1);
expect(results[0].document.id).toBe(document.id);
@@ -280,14 +275,18 @@ describe("#searchForUser", () => {
test("should handle no collections", async () => {
const team = await buildTeam();
const user = await buildUser({ teamId: team.id });
const user = await buildUser({
teamId: team.id,
});
const { results } = await Document.searchForUser(user, "test");
expect(results.length).toBe(0);
});
test("should return the total count of search results", async () => {
const team = await buildTeam();
const user = await buildUser({ teamId: team.id });
const user = await buildUser({
teamId: team.id,
});
const collection = await buildCollection({
userId: user.id,
teamId: team.id,
@@ -304,14 +303,15 @@ describe("#searchForUser", () => {
collectionId: collection.id,
title: "test number 2",
});
const { totalCount } = await Document.searchForUser(user, "test");
expect(totalCount).toBe("2");
});
test("should return the document when searched with their previous titles", async () => {
const team = await buildTeam();
const user = await buildUser({ teamId: team.id });
const user = await buildUser({
teamId: team.id,
});
const collection = await buildCollection({
teamId: team.id,
userId: user.id,
@@ -322,17 +322,17 @@ describe("#searchForUser", () => {
collectionId: collection.id,
title: "test number 1",
});
document.title = "change";
await document.save();
const { totalCount } = await Document.searchForUser(user, "test number");
expect(totalCount).toBe("1");
});
test("should not return the document when searched with neither the titles nor the previous titles", async () => {
const team = await buildTeam();
const user = await buildUser({ teamId: team.id });
const user = await buildUser({
teamId: team.id,
});
const collection = await buildCollection({
teamId: team.id,
userId: user.id,
@@ -343,10 +343,8 @@ describe("#searchForUser", () => {
collectionId: collection.id,
title: "test number 1",
});
document.title = "change";
await document.save();
const { totalCount } = await Document.searchForUser(
user,
"title doesn't exist"
@@ -354,41 +352,44 @@ describe("#searchForUser", () => {
expect(totalCount).toBe("0");
});
});
describe("#delete", () => {
test("should soft delete and set last modified", async () => {
let document = await buildDocument();
let user = await buildUser();
const user = await buildUser();
await document.delete(user.id);
document = await Document.findByPk(document.id, { paranoid: false });
document = await Document.findByPk(document.id, {
paranoid: false,
});
expect(document.lastModifiedById).toBe(user.id);
expect(document.deletedAt).toBeTruthy();
});
test("should soft delete templates", async () => {
let document = await buildDocument({ template: true });
let user = await buildUser();
let document = await buildDocument({
template: true,
});
const user = await buildUser();
await document.delete(user.id);
document = await Document.findByPk(document.id, { paranoid: false });
document = await Document.findByPk(document.id, {
paranoid: false,
});
expect(document.lastModifiedById).toBe(user.id);
expect(document.deletedAt).toBeTruthy();
});
test("should soft delete archived", async () => {
let document = await buildDocument({ archivedAt: new Date() });
let user = await buildUser();
let document = await buildDocument({
archivedAt: new Date(),
});
const user = await buildUser();
await document.delete(user.id);
document = await Document.findByPk(document.id, { paranoid: false });
document = await Document.findByPk(document.id, {
paranoid: false,
});
expect(document.lastModifiedById).toBe(user.id);
expect(document.deletedAt).toBeTruthy();
});
});
describe("#save", () => {
test("should have empty previousTitles by default", async () => {
const document = await buildDocument();
@@ -397,40 +398,30 @@ describe("#save", () => {
test("should include previousTitles on save", async () => {
const document = await buildDocument();
document.title = "test";
await document.save();
expect(document.previousTitles.length).toBe(1);
});
test("should not duplicate previousTitles", async () => {
const document = await buildDocument();
document.title = "test";
await document.save();
document.title = "example";
await document.save();
document.title = "test";
await document.save();
expect(document.previousTitles.length).toBe(3);
});
});
describe("#findByPk", () => {
test("should return document when urlId is correct", async () => {
const { document } = await seed();
const id = `${slugify(document.title)}-${document.urlId}`;
const response = await Document.findByPk(id);
expect(response.id).toBe(document.id);
});
});
describe("tasks", () => {
test("should consider all the possible checkTtems", async () => {
const document = await buildDocument({
@@ -440,9 +431,7 @@ describe("tasks", () => {
- [-] test
- [_] test`,
});
const tasks = document.tasks;
expect(tasks.completed).toBe(4);
expect(tasks.total).toBe(5);
});
@@ -451,9 +440,7 @@ describe("tasks", () => {
const document = await buildDocument({
text: `text`,
});
const tasks = document.tasks;
expect(tasks.completed).toBe(0);
expect(tasks.total).toBe(0);
});
@@ -464,9 +451,7 @@ describe("tasks", () => {
- [ x ] test
- [ ] test`,
});
const tasks = document.tasks;
expect(tasks.completed).toBe(0);
expect(tasks.total).toBe(0);
});
@@ -476,9 +461,7 @@ describe("tasks", () => {
text: `- [x] list item
- [ ] list item`,
});
const tasks = document.tasks;
expect(tasks.completed).toBe(1);
expect(tasks.total).toBe(2);
});
@@ -488,20 +471,14 @@ describe("tasks", () => {
text: `- [x] list item
- [ ] list item`,
});
const tasks = document.tasks;
expect(tasks.completed).toBe(1);
expect(tasks.total).toBe(2);
document.text = `- [x] list item
- [ ] list item
- [ ] list item`;
await document.save();
const newTasks = document.tasks;
expect(newTasks.completed).toBe(1);
expect(newTasks.total).toBe(3);
});

View File

@@ -1,18 +1,20 @@
// @flow
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module '@tom... Remove this comment to see the full error message
import removeMarkdown from "@tommoor/remove-markdown";
import { compact, find, map, uniq } from "lodash";
import randomstring from "randomstring";
import Sequelize, { Transaction } from "sequelize";
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'slat... Remove this comment to see the full error message
import MarkdownSerializer from "slate-md-serializer";
import isUUID from "validator/lib/isUUID";
import { MAX_TITLE_LENGTH } from "../../shared/constants";
import getTasks from "../../shared/utils/getTasks";
import parseTitle from "../../shared/utils/parseTitle";
import { SLUG_URL_REGEX } from "../../shared/utils/routeHelpers";
import unescape from "../../shared/utils/unescape";
import { Collection, User } from "../models";
import { MAX_TITLE_LENGTH } from "@shared/constants";
import { DateFilter } from "@shared/types";
import getTasks from "@shared/utils/getTasks";
import parseTitle from "@shared/utils/parseTitle";
import { SLUG_URL_REGEX } from "@shared/utils/routeHelpers";
import unescape from "@shared/utils/unescape";
import { Collection, User } from "@server/models";
import slugify from "@server/utils/slugify";
import { DataTypes, sequelize } from "../sequelize";
import slugify from "../utils/slugify";
import Revision from "./Revision";
const Op = Sequelize.Op;
@@ -20,23 +22,25 @@ const serializer = new MarkdownSerializer();
export const DOCUMENT_VERSION = 2;
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'doc' implicitly has an 'any' type.
const createUrlId = (doc) => {
return (doc.urlId = doc.urlId || randomstring.generate(10));
};
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'doc' implicitly has an 'any' type.
const beforeCreate = async (doc) => {
if (doc.version === undefined) {
doc.version = DOCUMENT_VERSION;
}
return beforeSave(doc);
};
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'doc' implicitly has an 'any' type.
const beforeSave = async (doc) => {
const { emoji } = parseTitle(doc.text);
// emoji in the title is split out for easier display
doc.emoji = emoji;
// ensure documents have a title
doc.title = doc.title || "";
@@ -48,10 +52,8 @@ const beforeSave = async (doc) => {
// add the current user as a collaborator on this doc
if (!doc.collaboratorIds) doc.collaboratorIds = [];
doc.collaboratorIds = uniq(doc.collaboratorIds.concat(doc.lastModifiedById));
// increment revision
doc.revisionCount += 1;
return doc;
};
@@ -82,8 +84,14 @@ const Document = sequelize.define(
editorVersion: DataTypes.STRING,
text: DataTypes.TEXT,
state: DataTypes.BLOB,
isWelcome: { type: DataTypes.BOOLEAN, defaultValue: false },
revisionCount: { type: DataTypes.INTEGER, defaultValue: 0 },
isWelcome: {
type: DataTypes.BOOLEAN,
defaultValue: false,
},
revisionCount: {
type: DataTypes.INTEGER,
defaultValue: 0,
},
archivedAt: DataTypes.DATE,
publishedAt: DataTypes.DATE,
parentDocumentId: DataTypes.UUID,
@@ -99,7 +107,6 @@ const Document = sequelize.define(
getterMethods: {
url: function () {
if (!this.title) return `/doc/untitled-${this.urlId}`;
const slugifiedTitle = slugify(this.title);
return `/doc/${slugifiedTitle}-${this.urlId}`;
},
@@ -111,7 +118,7 @@ const Document = sequelize.define(
);
// Class methods
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type.
Document.associate = (models) => {
Document.belongsTo(models.Collection, {
as: "collection",
@@ -155,8 +162,16 @@ Document.associate = (models) => {
});
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: {
@@ -164,6 +179,7 @@ Document.associate = (models) => {
},
},
});
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'userId' implicitly has an 'any' type.
Document.addScope("withCollection", (userId, paranoid = true) => {
if (userId) {
return {
@@ -180,36 +196,54 @@ Document.associate = (models) => {
}
return {
include: [{ model: models.Collection, as: "collection" }],
include: [
{
model: models.Collection,
as: "collection",
},
],
};
});
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,
},
],
});
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'userId' implicitly has an 'any' type.
Document.addScope("withViews", (userId) => {
if (!userId) return {};
return {
include: [
{
model: models.View,
as: "views",
where: { userId },
where: {
userId,
},
required: false,
separate: true,
},
],
};
});
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'userId' implicitly has an 'any' type.
Document.addScope("withStarred", (userId) => ({
include: [
{
model: models.Star,
as: "starred",
where: { userId },
where: {
userId,
},
required: false,
separate: true,
},
@@ -217,22 +251,27 @@ Document.associate = (models) => {
}));
};
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'id' implicitly has an 'any' type.
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",
{
// @ts-expect-error ts-migrate(2339) FIXME: Property 'userId' does not exist on type '{}'.
method: ["withCollection", options.userId, options.paranoid],
},
{
// @ts-expect-error ts-migrate(2339) FIXME: Property 'userId' does not exist on type '{}'.
method: ["withViews", options.userId],
}
);
if (isUUID(id)) {
return scope.findOne({
where: { id },
where: {
id,
},
...options,
});
} else if (id.match(SLUG_URL_REGEX)) {
@@ -247,21 +286,20 @@ Document.findByPk = async function (id, options = {}) {
type SearchResponse = {
results: {
ranking: number,
context: string,
document: Document,
}[],
totalCount: number,
ranking: number;
context: string;
document: Document;
}[];
totalCount: number;
};
type SearchOptions = {
limit?: number,
offset?: number,
collectionId?: string,
dateFilter?: "day" | "week" | "month" | "year",
collaboratorIds?: string[],
includeArchived?: boolean,
includeDrafts?: boolean,
limit?: number;
offset?: number;
collectionId?: string;
dateFilter?: DateFilter;
collaboratorIds?: string[];
includeArchived?: boolean;
includeDrafts?: boolean;
};
function escape(query: string): string {
@@ -271,7 +309,9 @@ function escape(query: string): string {
}
Document.searchForTeam = async (
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'team' implicitly has an 'any' type.
team,
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'query' implicitly has an 'any' type.
query,
options: SearchOptions = {}
): Promise<SearchResponse> => {
@@ -282,7 +322,10 @@ Document.searchForTeam = async (
// If the team has access no public collections then shortcircuit the rest of this
if (!collectionIds.length) {
return { results: [], totalCount: 0 };
return {
results: [],
totalCount: 0,
};
}
// Build the SQL query to get documentIds, ranking, and search term context
@@ -293,7 +336,6 @@ Document.searchForTeam = async (
"deletedAt" IS NULL AND
"publishedAt" IS NOT NULL
`;
const selectSql = `
SELECT
id,
@@ -307,72 +349,76 @@ Document.searchForTeam = async (
LIMIT :limit
OFFSET :offset;
`;
const countSql = `
SELECT COUNT(id)
FROM documents
WHERE ${whereClause}
`;
const queryReplacements = {
teamId: team.id,
query: wildcardQuery,
collectionIds,
};
const resultsQuery = sequelize.query(selectSql, {
type: sequelize.QueryTypes.SELECT,
replacements: {
...queryReplacements,
limit,
offset,
},
replacements: { ...queryReplacements, limit, offset },
});
const countQuery = sequelize.query(countSql, {
type: sequelize.QueryTypes.SELECT,
replacements: queryReplacements,
});
const [results, [{ count }]] = await Promise.all([resultsQuery, countQuery]);
// Final query to get associated document data
const documents = await Document.findAll({
where: {
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,
},
],
});
return {
results: map(results, (result) => ({
ranking: result.searchRanking,
context: removeMarkdown(unescape(result.searchContext), {
stripHTML: false,
}),
document: find(documents, { id: result.id }),
document: find(documents, {
id: result.id,
}),
})),
totalCount: count,
};
};
Document.searchForUser = async (
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'user' implicitly has an 'any' type.
user,
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'query' implicitly has an 'any' type.
query,
options: SearchOptions = {}
): Promise<SearchResponse> => {
const limit = options.limit || 15;
const offset = options.offset || 0;
const wildcardQuery = `${escape(query)}:*`;
// Ensure we're filtering by the users accessible collections. If
// collectionId is passed as an option it is assumed that the authorization
// has already been done in the router
let collectionIds;
if (options.collectionId) {
collectionIds = [options.collectionId];
} else {
@@ -381,10 +427,14 @@ Document.searchForUser = async (
// If the user has access to no collections then shortcircuit the rest of this
if (!collectionIds.length) {
return { results: [], totalCount: 0 };
return {
results: [],
totalCount: 0,
};
}
let dateFilter;
if (options.dateFilter) {
dateFilter = `1 ${options.dateFilter}`;
}
@@ -410,7 +460,6 @@ Document.searchForUser = async (
: '"publishedAt" IS NOT NULL'
}
`;
const selectSql = `
SELECT
id,
@@ -424,13 +473,11 @@ Document.searchForUser = async (
LIMIT :limit
OFFSET :offset;
`;
const countSql = `
SELECT COUNT(id)
FROM documents
WHERE ${whereClause}
`;
const queryReplacements = {
teamId: user.teamId,
userId: user.id,
@@ -439,21 +486,14 @@ Document.searchForUser = async (
collectionIds,
dateFilter,
};
const resultsQuery = sequelize.query(selectSql, {
type: sequelize.QueryTypes.SELECT,
replacements: {
...queryReplacements,
limit,
offset,
},
replacements: { ...queryReplacements, limit, offset },
});
const countQuery = sequelize.query(countSql, {
type: sequelize.QueryTypes.SELECT,
replacements: queryReplacements,
});
const [results, [{ count }]] = await Promise.all([resultsQuery, countQuery]);
// Final query to get associated document data
const documents = await Document.scope(
@@ -468,31 +508,41 @@ Document.searchForUser = async (
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,
},
],
});
return {
results: map(results, (result) => ({
ranking: result.searchRanking,
context: removeMarkdown(unescape(result.searchContext), {
stripHTML: false,
}),
document: find(documents, { id: result.id }),
document: find(documents, {
id: result.id,
}),
})),
totalCount: count,
};
};
// Hooks
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'model' implicitly has an 'any' type.
Document.addHook("beforeSave", async (model) => {
if (!model.publishedAt || model.template) {
return;
}
const collection = await Collection.findByPk(model.collectionId);
if (!collection) {
return;
}
@@ -500,25 +550,24 @@ Document.addHook("beforeSave", async (model) => {
await collection.updateDocument(model);
model.collection = collection;
});
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'model' implicitly has an 'any' type.
Document.addHook("afterCreate", async (model) => {
if (!model.publishedAt || model.template) {
return;
}
const collection = await Collection.findByPk(model.collectionId);
if (!collection) {
return;
}
await collection.addDocumentToStructure(model, 0);
model.collection = collection;
return model;
});
// Instance methods
Document.prototype.toMarkdown = function () {
const text = unescape(this.text);
@@ -543,25 +592,35 @@ Document.prototype.migrateVersion = function () {
// migrate from document version 1 -> 2
if (this.version === 1) {
const nodes = serializer.deserialize(this.text);
this.text = serializer.serialize(nodes, { version: 2 });
this.text = serializer.serialize(nodes, {
version: 2,
});
this.version = 2;
migrated = true;
}
if (migrated) {
return this.save({ silent: true, hooks: false });
return this.save({
silent: true,
hooks: false,
});
}
};
// Note: This method marks the document and it's children as deleted
// in the database, it does not permanently delete them OR remove
// from the collection structure.
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'options' implicitly has an 'any' type.
Document.prototype.deleteWithChildren = async function (options) {
// Helper to destroy all child documents for a document
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'documentId' implicitly has an 'any' typ... Remove this comment to see the full error message
const loopChildren = async (documentId, opts) => {
const childDocuments = await Document.findAll({
where: { parentDocumentId: documentId },
where: {
parentDocumentId: documentId,
},
});
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'child' implicitly has an 'any' type.
childDocuments.forEach(async (child) => {
await loopChildren(child.id, opts);
await child.destroy(opts);
@@ -572,17 +631,21 @@ Document.prototype.deleteWithChildren = async function (options) {
await this.destroy(options);
};
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'userId' implicitly has an 'any' type.
Document.prototype.archiveWithChildren = async function (userId, options) {
const archivedAt = new Date();
// Helper to archive all child documents for a document
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'parentDocumentId' implicitly has an 'an... Remove this comment to see the full error message
const archiveChildren = async (parentDocumentId) => {
const childDocuments = await Document.findAll({
where: { parentDocumentId },
where: {
parentDocumentId,
},
});
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'child' implicitly has an 'any' type.
childDocuments.forEach(async (child) => {
await archiveChildren(child.id);
child.archivedAt = archivedAt;
child.lastModifiedById = userId;
await child.save(options);
@@ -595,6 +658,7 @@ Document.prototype.archiveWithChildren = async function (userId, options) {
return this.save(options);
};
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'options' implicitly has an 'any' type.
Document.prototype.publish = async function (userId: string, options) {
if (this.publishedAt) return this.save(options);
@@ -606,37 +670,32 @@ Document.prototype.publish = async function (userId: string, options) {
this.lastModifiedById = userId;
this.publishedAt = new Date();
await this.save(options);
return this;
};
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'options' implicitly has an 'any' type.
Document.prototype.unpublish = async function (userId: string, options) {
if (!this.publishedAt) return this;
const collection = await this.getCollection();
await collection.removeDocumentInStructure(this);
// unpublishing a document converts the "ownership" to yourself, so that it
// can appear in your drafts rather than the original creators
this.userId = userId;
this.lastModifiedById = userId;
this.publishedAt = null;
await this.save(options);
return this;
};
// Moves a document from being visible to the team within a collection
// to the archived area, where it can be subsequently restored.
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'userId' implicitly has an 'any' type.
Document.prototype.archive = async function (userId) {
// archive any children and remove from the document structure
const collection = await this.getCollection();
await collection.removeDocumentInStructure(this);
this.collection = collection;
await this.archiveWithChildren(userId);
return this;
};
@@ -670,7 +729,6 @@ Document.prototype.unarchive = async function (userId: string) {
this.archivedAt = null;
this.lastModifiedById = userId;
await this.save();
return this;
};
@@ -680,24 +738,33 @@ Document.prototype.delete = function (userId: string) {
async (transaction: Transaction): Promise<Document> => {
if (!this.archivedAt && !this.template) {
// delete any children and remove from the document structure
const collection = await this.getCollection({ transaction });
if (collection) await collection.deleteDocument(this, { transaction });
const collection = await this.getCollection({
transaction,
});
if (collection)
await collection.deleteDocument(this, {
transaction,
});
} else {
await this.destroy({ transaction });
await this.destroy({
transaction,
});
}
await Revision.destroy({
where: { documentId: this.id },
where: {
documentId: this.id,
},
transaction,
});
await this.update(
{ lastModifiedById: userId },
{
lastModifiedById: userId,
},
{
transaction,
}
);
return this;
}
);

View File

@@ -1,4 +1,3 @@
// @flow
import { globalEventQueue } from "../queues";
import { DataTypes, sequelize } from "../sequelize";
@@ -14,6 +13,7 @@ const Event = sequelize.define("event", {
data: DataTypes.JSONB,
});
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type.
Event.associate = (models) => {
Event.belongsTo(models.User, {
as: "user",
@@ -37,22 +37,23 @@ Event.associate = (models) => {
});
};
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'event' implicitly has an 'any' type.
Event.beforeCreate((event) => {
if (event.ip) {
// cleanup IPV6 representations of IPV4 addresses
event.ip = event.ip.replace(/^::ffff:/, "");
}
});
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'event' implicitly has an 'any' type.
Event.afterCreate((event) => {
globalEventQueue.add(event);
});
// add can be used to send events into the event system without recording them
// in the database or audit trail
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'event' implicitly has an 'any' type.
Event.add = (event) => {
const now = new Date();
globalEventQueue.add(
Event.build({
createdAt: now,
@@ -79,7 +80,6 @@ Event.ACTIVITY_EVENTS = [
"revisions.create",
"users.create",
];
Event.AUDIT_EVENTS = [
"api_keys.create",
"api_keys.delete",

View File

@@ -1,6 +1,5 @@
// @flow
import { deleteFromS3 } from "@server/utils/s3";
import { DataTypes, sequelize } from "../sequelize";
import { deleteFromS3 } from "../utils/s3";
const FileOperation = sequelize.define("file_operations", {
id: {
@@ -33,7 +32,7 @@ const FileOperation = sequelize.define("file_operations", {
allowNull: false,
},
});
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'model' implicitly has an 'any' type.
FileOperation.beforeDestroy(async (model) => {
await deleteFromS3(model.key);
});
@@ -44,6 +43,7 @@ FileOperation.prototype.expire = async function () {
await this.save();
};
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type.
FileOperation.associate = (models) => {
FileOperation.belongsTo(models.User, {
as: "user",

View File

@@ -1,19 +1,19 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import { CollectionGroup, GroupUser } from "../models";
import { buildUser, buildGroup, buildCollection } from "../test/factories";
import { flushdb } from "../test/support";
import { CollectionGroup, GroupUser } from "@server/models";
import { buildUser, buildGroup, buildCollection } from "@server/test/factories";
import { flushdb } from "@server/test/support";
beforeEach(() => flushdb());
beforeEach(jest.resetAllMocks);
describe("afterDestroy hook", () => {
test("should destroy associated group and collection join relations", async () => {
const group = await buildGroup();
const teamId = group.teamId;
const user1 = await buildUser({ teamId });
const user2 = await buildUser({ teamId });
const user1 = await buildUser({
teamId,
});
const user2 = await buildUser({
teamId,
});
const collection1 = await buildCollection({
permission: null,
teamId,
@@ -22,26 +22,34 @@ describe("afterDestroy hook", () => {
permission: null,
teamId,
});
const createdById = user1.id;
await group.addUser(user1, { through: { createdById } });
await group.addUser(user2, { through: { createdById } });
await collection1.addGroup(group, { through: { createdById } });
await collection2.addGroup(group, { through: { createdById } });
await group.addUser(user1, {
through: {
createdById,
},
});
await group.addUser(user2, {
through: {
createdById,
},
});
await collection1.addGroup(group, {
through: {
createdById,
},
});
await collection2.addGroup(group, {
through: {
createdById,
},
});
let collectionGroupCount = await CollectionGroup.count();
let groupUserCount = await GroupUser.count();
expect(collectionGroupCount).toBe(2);
expect(groupUserCount).toBe(2);
await group.destroy();
collectionGroupCount = await CollectionGroup.count();
groupUserCount = await GroupUser.count();
expect(collectionGroupCount).toBe(0);
expect(groupUserCount).toBe(0);
});

View File

@@ -1,5 +1,4 @@
// @flow
import { CollectionGroup, GroupUser } from "../models";
import { CollectionGroup, GroupUser } from "@server/models";
import { Op, DataTypes, sequelize } from "../sequelize";
const Group = sequelize.define(
@@ -27,10 +26,15 @@ const Group = sequelize.define(
const foundItem = await Group.findOne({
where: {
teamId: this.teamId,
name: { [Op.iLike]: this.name },
id: { [Op.not]: this.id },
name: {
[Op.iLike]: this.name,
},
id: {
[Op.not]: this.id,
},
},
});
if (foundItem) {
throw new Error("The name of this group is already in use");
}
@@ -39,6 +43,7 @@ const Group = sequelize.define(
}
);
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type.
Group.associate = (models) => {
Group.hasMany(models.GroupUser, {
as: "groupMemberships",
@@ -73,11 +78,19 @@ Group.associate = (models) => {
};
// Cascade deletes to group and collection relations
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'group' implicitly has an 'any' type.
Group.addHook("afterDestroy", async (group, options) => {
if (!group.deletedAt) return;
await GroupUser.destroy({ where: { groupId: group.id } });
await CollectionGroup.destroy({ where: { groupId: group.id } });
await GroupUser.destroy({
where: {
groupId: group.id,
},
});
await CollectionGroup.destroy({
where: {
groupId: group.id,
},
});
});
export default Group;

View File

@@ -1,4 +1,3 @@
// @flow
import { sequelize } from "../sequelize";
const GroupUser = sequelize.define(
@@ -10,6 +9,7 @@ const GroupUser = sequelize.define(
}
);
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type.
GroupUser.associate = (models) => {
GroupUser.belongsTo(models.Group, {
as: "group",
@@ -26,7 +26,11 @@ GroupUser.associate = (models) => {
foreignKey: "createdById",
});
GroupUser.addScope("defaultScope", {
include: [{ association: "user" }],
include: [
{
association: "user",
},
],
});
};

View File

@@ -1,4 +1,3 @@
// @flow
import { DataTypes, sequelize } from "../sequelize";
const Integration = sequelize.define("integration", {
@@ -13,6 +12,7 @@ const Integration = sequelize.define("integration", {
events: DataTypes.ARRAY(DataTypes.STRING),
});
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type.
Integration.associate = (models) => {
Integration.belongsTo(models.User, {
as: "user",

View File

@@ -1,4 +1,3 @@
// @flow
import { DataTypes, sequelize, encryptedFields } from "../sequelize";
const IntegrationAuthentication = sequelize.define("authentication", {
@@ -12,6 +11,7 @@ const IntegrationAuthentication = sequelize.define("authentication", {
token: encryptedFields().vault("token"),
});
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type.
IntegrationAuthentication.associate = (models) => {
IntegrationAuthentication.belongsTo(models.User, {
as: "user",

View File

@@ -1,4 +1,3 @@
// @flow
import { DataTypes, sequelize } from "../sequelize";
const Notification = sequelize.define(
@@ -22,6 +21,7 @@ const Notification = sequelize.define(
}
);
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type.
Notification.associate = (models) => {
Notification.belongsTo(models.User, {
as: "actor",

View File

@@ -1,4 +1,3 @@
// @flow
import crypto from "crypto";
import { DataTypes, sequelize } from "../sequelize";
@@ -40,12 +39,14 @@ const NotificationSetting = sequelize.define(
}
);
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'userId' implicitly has an 'any' type.
NotificationSetting.getUnsubscribeToken = (userId) => {
const hash = crypto.createHash("sha256");
hash.update(`${userId}-${process.env.SECRET_KEY}`);
return hash.digest("hex");
};
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type.
NotificationSetting.associate = (models) => {
NotificationSetting.belongsTo(models.User, {
as: "user",

View File

@@ -1,11 +1,9 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import { Revision } from "../models";
import { buildDocument } from "../test/factories";
import { flushdb } from "../test/support";
import { Revision } from "@server/models";
import { buildDocument } from "@server/test/factories";
import { flushdb } from "@server/test/support";
beforeEach(() => flushdb());
beforeEach(jest.resetAllMocks);
describe("#findLatest", () => {
test("should return latest revision", async () => {
const document = await buildDocument({
@@ -13,17 +11,13 @@ describe("#findLatest", () => {
text: "Content",
});
await Revision.createFromDocument(document);
document.title = "Changed 1";
await document.save();
await Revision.createFromDocument(document);
document.title = "Changed 2";
await document.save();
await Revision.createFromDocument(document);
const revision = await Revision.findLatest(document.id);
expect(revision.title).toBe("Changed 2");
expect(revision.text).toBe("Content");
});

View File

@@ -1,9 +1,8 @@
// @flow
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'slat... Remove this comment to see the full error message
import MarkdownSerializer from "slate-md-serializer";
import { DataTypes, sequelize } from "../sequelize";
const serializer = new MarkdownSerializer();
const Revision = sequelize.define("revision", {
id: {
type: DataTypes.UUID,
@@ -16,6 +15,7 @@ const Revision = sequelize.define("revision", {
text: DataTypes.TEXT,
});
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type.
Revision.associate = (models) => {
Revision.belongsTo(models.Document, {
as: "document",
@@ -29,12 +29,21 @@ Revision.associate = (models) => {
Revision.addScope(
"defaultScope",
{
include: [{ model: models.User, as: "user", paranoid: false }],
include: [
{
model: models.User,
as: "user",
paranoid: false,
},
],
},
{ override: true }
{
override: true,
}
);
};
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'documentId' implicitly has an 'any' typ... Remove this comment to see the full error message
Revision.findLatest = function (documentId) {
return Revision.findOne({
where: {
@@ -44,6 +53,7 @@ Revision.findLatest = function (documentId) {
});
};
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'document' implicitly has an 'any' type.
Revision.createFromDocument = function (document, options) {
return Revision.create(
{
@@ -53,7 +63,6 @@ Revision.createFromDocument = function (document, options) {
editorVersion: document.editorVersion,
version: document.version,
documentId: document.id,
// revision time is set to the last time document was touched as this
// handler can be debounced in the case of an update
createdAt: document.updatedAt,
@@ -76,13 +85,18 @@ Revision.prototype.migrateVersion = function () {
// migrate from document version 1 -> 2
if (this.version === 1) {
const nodes = serializer.deserialize(this.text);
this.text = serializer.serialize(nodes, { version: 2 });
this.text = serializer.serialize(nodes, {
version: 2,
});
this.version = 2;
migrated = true;
}
if (migrated) {
return this.save({ silent: true, hooks: false });
return this.save({
silent: true,
hooks: false,
});
}
};

View File

@@ -1,4 +1,3 @@
// @flow
import { DataTypes, sequelize } from "../sequelize";
const SearchQuery = sequelize.define(
@@ -15,9 +14,12 @@ const SearchQuery = sequelize.define(
},
query: {
type: DataTypes.STRING,
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'val' implicitly has an 'any' type.
set(val) {
this.setDataValue("query", val.substring(0, 255));
},
allowNull: false,
},
results: {
@@ -31,6 +33,7 @@ const SearchQuery = sequelize.define(
}
);
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type.
SearchQuery.associate = (models) => {
SearchQuery.belongsTo(models.User, {
as: "user",

View File

@@ -1,4 +1,3 @@
// @flow
import { DataTypes, sequelize } from "../sequelize";
const Share = sequelize.define(
@@ -24,6 +23,7 @@ const Share = sequelize.define(
}
);
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type.
Share.associate = (models) => {
Share.belongsTo(models.User, {
as: "user",
@@ -39,11 +39,19 @@ Share.associate = (models) => {
});
Share.addScope("defaultScope", {
include: [
{ association: "user", paranoid: false },
{ association: "document" },
{ association: "team" },
{
association: "user",
paranoid: false,
},
{
association: "document",
},
{
association: "team",
},
],
});
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'userId' implicitly has an 'any' type.
Share.addScope("withCollection", (userId) => {
return {
include: [
@@ -60,13 +68,19 @@ Share.associate = (models) => {
},
],
},
{ association: "user", paranoid: false },
{ association: "team" },
{
association: "user",
paranoid: false,
},
{
association: "team",
},
],
};
});
};
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'userId' implicitly has an 'any' type.
Share.prototype.revoke = function (userId) {
this.revokedAt = new Date();
this.revokedById = userId;

View File

@@ -1,4 +1,3 @@
// @flow
import { DataTypes, sequelize } from "../sequelize";
const Star = sequelize.define("star", {
@@ -9,6 +8,7 @@ const Star = sequelize.define("star", {
},
});
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type.
Star.associate = (models) => {
Star.belongsTo(models.Document);
Star.belongsTo(models.User);

View File

@@ -1,25 +1,25 @@
// @flow
import { buildTeam, buildCollection } from "../test/factories";
import { flushdb } from "../test/support";
import { buildTeam, buildCollection } from "@server/test/factories";
import { flushdb } from "@server/test/support";
beforeEach(() => flushdb());
describe("collectionIds", () => {
it("should return non-private collection ids", async () => {
const team = await buildTeam();
const collection = await buildCollection({ teamId: team.id });
const collection = await buildCollection({
teamId: team.id,
});
// build a collection in another team
await buildCollection();
// build a private collection
await buildCollection({ teamId: team.id, permission: null });
await buildCollection({
teamId: team.id,
permission: null,
});
const response = await team.collectionIds();
expect(response.length).toEqual(1);
expect(response[0]).toEqual(collection.id);
});
});
describe("provisionSubdomain", () => {
it("should set subdomain if available", async () => {
const team = await buildTeam();
@@ -29,8 +29,9 @@ describe("provisionSubdomain", () => {
});
it("should set subdomain append if unavailable", async () => {
await buildTeam({ subdomain: "myteam" });
await buildTeam({
subdomain: "myteam",
});
const team = await buildTeam();
const subdomain = await team.provisionSubdomain("myteam");
expect(subdomain).toEqual("myteam1");
@@ -38,9 +39,12 @@ describe("provisionSubdomain", () => {
});
it("should increment subdomain append if unavailable", async () => {
await buildTeam({ subdomain: "myteam" });
await buildTeam({ subdomain: "myteam1" });
await buildTeam({
subdomain: "myteam",
});
await buildTeam({
subdomain: "myteam1",
});
const team = await buildTeam();
const subdomain = await team.provisionSubdomain("myteam");
expect(subdomain).toEqual("myteam2");
@@ -48,7 +52,9 @@ describe("provisionSubdomain", () => {
});
it("should do nothing if subdomain already set", async () => {
const team = await buildTeam({ subdomain: "example" });
const team = await buildTeam({
subdomain: "example",
});
const subdomain = await team.provisionSubdomain("myteam");
expect(subdomain).toEqual("example");
expect(team.subdomain).toEqual("example");

View File

@@ -1,18 +1,13 @@
// @flow
import fs from "fs";
import path from "path";
import { URL } from "url";
import util from "util";
import { v4 as uuidv4 } from "uuid";
import {
stripSubdomain,
RESERVED_SUBDOMAINS,
} from "../../shared/utils/domains";
import Logger from "../logging/logger";
import { stripSubdomain, RESERVED_SUBDOMAINS } from "@shared/utils/domains";
import Logger from "@server/logging/logger";
import { generateAvatarUrl } from "@server/utils/avatars";
import { publicS3Endpoint, uploadToS3FromUrl } from "@server/utils/s3";
import { DataTypes, sequelize, Op } from "../sequelize";
import { generateAvatarUrl } from "../utils/avatars";
import { publicS3Endpoint, uploadToS3FromUrl } from "../utils/s3";
import Collection from "./Collection";
import Document from "./Document";
@@ -52,10 +47,23 @@ const Team = sequelize.define(
allowNull: true,
unique: true,
},
slackId: { type: DataTypes.STRING, allowNull: true },
googleId: { type: DataTypes.STRING, allowNull: true },
avatarUrl: { type: DataTypes.STRING, allowNull: true },
sharing: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: true },
slackId: {
type: DataTypes.STRING,
allowNull: true,
},
googleId: {
type: DataTypes.STRING,
allowNull: true,
},
avatarUrl: {
type: DataTypes.STRING,
allowNull: true,
},
sharing: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true,
},
signupQueryParams: {
type: DataTypes.JSONB,
allowNull: true,
@@ -80,10 +88,7 @@ const Team = sequelize.define(
defaultValue: "member",
allowNull: false,
validate: {
isIn: {
args: [["viewer", "member"]],
msg: "Must be 'viewer' or 'member'",
},
isIn: [["viewer", "member"]],
},
},
},
@@ -94,14 +99,17 @@ const Team = sequelize.define(
if (this.domain) {
return `https://${this.domain}`;
}
if (!this.subdomain || process.env.SUBDOMAINS_ENABLED !== "true") {
return process.env.URL;
}
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'string | undefined' is not assig... Remove this comment to see the full error message
const url = new URL(process.env.URL);
url.host = `${this.subdomain}.${stripSubdomain(url.host)}`;
return url.href.replace(/\/$/, "");
},
logoUrl() {
return (
this.avatarUrl ||
@@ -115,20 +123,31 @@ const Team = sequelize.define(
}
);
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type.
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",
});
Team.hasMany(models.AuthenticationProvider, {
as: "authenticationProviders",
});
Team.addScope("withAuthenticationProviders", {
include: [
{ model: models.AuthenticationProvider, as: "authenticationProviders" },
{
model: models.AuthenticationProvider,
as: "authenticationProviders",
},
],
});
};
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'model' implicitly has an 'any' type.
const uploadAvatar = async (model) => {
const endpoint = publicS3Endpoint();
const { avatarUrl } = model;
@@ -146,7 +165,9 @@ const uploadAvatar = async (model) => {
);
if (newUrl) model.avatarUrl = newUrl;
} catch (err) {
Logger.error("Error uploading avatar to S3", err, { url: avatarUrl });
Logger.error("Error uploading avatar to S3", err, {
url: avatarUrl,
});
}
}
};
@@ -156,12 +177,17 @@ Team.prototype.provisionSubdomain = async function (
options = {}
) {
if (this.subdomain) return this.subdomain;
let subdomain = requestedSubdomain;
let append = 0;
while (true) {
for (;;) {
try {
await this.update({ subdomain }, options);
await this.update(
{
subdomain,
},
options
);
break;
} catch (err) {
// subdomain was invalid or already used, try again
@@ -172,6 +198,7 @@ Team.prototype.provisionSubdomain = async function (
return subdomain;
};
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'userId' implicitly has an 'any' type.
Team.prototype.provisionFirstCollection = async function (userId) {
const collection = await Collection.create({
name: "Welcome",
@@ -182,7 +209,6 @@ Team.prototype.provisionFirstCollection = async function (userId) {
sort: Collection.DEFAULT_SORT,
permission: "read_write",
});
// 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 = [
@@ -213,8 +239,8 @@ Team.prototype.provisionFirstCollection = async function (userId) {
}
};
Team.prototype.collectionIds = async function (paranoid: boolean = true) {
let models = await Collection.findAll({
Team.prototype.collectionIds = async function (paranoid = true) {
const models = await Collection.findAll({
attributes: ["id"],
where: {
teamId: this.id,
@@ -224,7 +250,7 @@ Team.prototype.collectionIds = async function (paranoid: boolean = true) {
},
paranoid,
});
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'c' implicitly has an 'any' type.
return models.map((c) => c.id);
};

View File

@@ -1,82 +1,77 @@
// @flow
import { UserAuthentication, CollectionUser } from "../models";
import { buildUser, buildTeam, buildCollection } from "../test/factories";
import { flushdb } from "../test/support";
import { UserAuthentication, CollectionUser } from "@server/models";
import { buildUser, buildTeam, buildCollection } from "@server/test/factories";
import { flushdb } from "@server/test/support";
beforeEach(() => flushdb());
describe("user model", () => {
describe("destroy", () => {
it("should delete user authentications", async () => {
const user = await buildUser();
expect(await UserAuthentication.count()).toBe(1);
await user.destroy();
expect(await UserAuthentication.count()).toBe(0);
});
});
describe("getJwtToken", () => {
it("should set JWT secret", async () => {
const user = await buildUser();
expect(user.getJwtToken()).toBeTruthy();
});
});
describe("collectionIds", () => {
it("should return read_write collections", async () => {
const team = await buildTeam();
const user = await buildUser({ teamId: team.id });
const user = await buildUser({
teamId: team.id,
});
const collection = await buildCollection({
teamId: team.id,
permission: "read_write",
});
const response = await user.collectionIds();
expect(response.length).toEqual(1);
expect(response[0]).toEqual(collection.id);
});
it("should return read collections", async () => {
const team = await buildTeam();
const user = await buildUser({ teamId: team.id });
const user = await buildUser({
teamId: team.id,
});
const collection = await buildCollection({
teamId: team.id,
permission: "read",
});
const response = await user.collectionIds();
expect(response.length).toEqual(1);
expect(response[0]).toEqual(collection.id);
});
it("should not return private collections", async () => {
const team = await buildTeam();
const user = await buildUser({ teamId: team.id });
const user = await buildUser({
teamId: team.id,
});
await buildCollection({
teamId: team.id,
permission: null,
});
const response = await user.collectionIds();
expect(response.length).toEqual(0);
});
it("should not return private collection with membership", async () => {
const team = await buildTeam();
const user = await buildUser({ teamId: team.id });
const user = await buildUser({
teamId: team.id,
});
const collection = await buildCollection({
teamId: team.id,
permission: null,
});
await CollectionUser.create({
createdById: user.id,
collectionId: collection.id,
userId: user.id,
permission: "read",
});
const response = await user.collectionIds();
expect(response.length).toEqual(1);
expect(response[0]).toEqual(collection.id);

View File

@@ -1,15 +1,14 @@
// @flow
import crypto from "crypto";
import { addMinutes, subMinutes } from "date-fns";
import JWT from "jsonwebtoken";
import { v4 as uuidv4 } from "uuid";
import { languages } from "../../shared/i18n";
import { languages } from "@shared/i18n";
import Logger from "@server/logging/logger";
import { DEFAULT_AVATAR_HOST } from "@server/utils/avatars";
import { palette } from "@server/utils/color";
import { publicS3Endpoint, uploadToS3FromUrl } from "@server/utils/s3";
import { ValidationError } from "../errors";
import Logger from "../logging/logger";
import { DataTypes, sequelize, encryptedFields, Op } from "../sequelize";
import { DEFAULT_AVATAR_HOST } from "../utils/avatars";
import { palette } from "../utils/color";
import { publicS3Endpoint, uploadToS3FromUrl } from "../utils/s3";
import {
UserAuthentication,
Star,
@@ -26,23 +25,43 @@ const User = sequelize.define(
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
email: { type: DataTypes.STRING },
username: { type: DataTypes.STRING },
email: {
type: DataTypes.STRING,
},
username: {
type: DataTypes.STRING,
},
name: DataTypes.STRING,
avatarUrl: { type: DataTypes.STRING, allowNull: true },
avatarUrl: {
type: DataTypes.STRING,
allowNull: true,
},
isAdmin: DataTypes.BOOLEAN,
isViewer: {
type: DataTypes.BOOLEAN,
defaultValue: false,
allowNull: false,
},
service: { type: DataTypes.STRING, allowNull: true },
serviceId: { type: DataTypes.STRING, allowNull: true, unique: true },
service: {
type: DataTypes.STRING,
allowNull: true,
},
serviceId: {
type: DataTypes.STRING,
allowNull: true,
unique: true,
},
jwtSecret: encryptedFields().vault("jwtSecret"),
lastActiveAt: DataTypes.DATE,
lastActiveIp: { type: DataTypes.STRING, allowNull: true },
lastActiveIp: {
type: DataTypes.STRING,
allowNull: true,
},
lastSignedInAt: DataTypes.DATE,
lastSignedInIp: { type: DataTypes.STRING, allowNull: true },
lastSignedInIp: {
type: DataTypes.STRING,
allowNull: true,
},
lastSigninEmailSentAt: DataTypes.DATE,
suspendedAt: DataTypes.DATE,
suspendedById: DataTypes.UUID,
@@ -60,11 +79,14 @@ const User = sequelize.define(
isSuspended() {
return !!this.suspendedAt;
},
isInvited() {
return !this.lastActiveAt;
},
avatarUrl() {
const original = this.getDataValue("avatarUrl");
if (original) {
return original;
}
@@ -76,6 +98,7 @@ const User = sequelize.define(
.digest("hex");
return `${DEFAULT_AVATAR_HOST}/avatar/${hash}/${initial}.png`;
},
color() {
const idAsHex = crypto.createHash("md5").update(this.id).digest("hex");
const idAsNumber = parseInt(idAsHex, 16);
@@ -86,19 +109,33 @@ const User = sequelize.define(
);
// Class methods
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type.
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",
});
User.hasMany(models.Document, { as: "documents" });
User.hasMany(models.View, { as: "views" });
User.hasMany(models.UserAuthentication, { as: "authentications" });
User.hasMany(models.Document, {
as: "documents",
});
User.hasMany(models.View, {
as: "views",
});
User.hasMany(models.UserAuthentication, {
as: "authentications",
});
User.belongsTo(models.Team);
User.addScope("withAuthentications", {
include: [{ model: models.UserAuthentication, as: "authentications" }],
include: [
{
model: models.UserAuthentication,
as: "authentications",
},
],
});
};
@@ -108,22 +145,28 @@ User.prototype.collectionIds = async function (options = {}) {
method: ["withMembership", this.id],
}).findAll({
attributes: ["id", "permission"],
where: { teamId: this.teamId },
where: {
teamId: this.teamId,
},
paranoid: true,
...options,
});
return collectionStubs
.filter(
(c) =>
c.permission === "read" ||
c.permission === "read_write" ||
c.memberships.length > 0 ||
c.collectionGroupMemberships.length > 0
)
.map((c) => c.id);
return (
collectionStubs
.filter(
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'c' implicitly has an 'any' type.
(c) =>
c.permission === "read" ||
c.permission === "read_write" ||
c.memberships.length > 0 ||
c.collectionGroupMemberships.length > 0
)
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'c' implicitly has an 'any' type.
.map((c) => c.id)
);
};
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'ip' implicitly has an 'any' type.
User.prototype.updateActiveAt = function (ip, force = false) {
const fiveMinutesAgo = subMinutes(new Date(), 5);
@@ -132,14 +175,19 @@ User.prototype.updateActiveAt = function (ip, force = false) {
if (this.lastActiveAt < fiveMinutesAgo || force) {
this.lastActiveAt = new Date();
this.lastActiveIp = ip;
return this.save({ hooks: false });
return this.save({
hooks: false,
});
}
};
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'ip' implicitly has an 'any' type.
User.prototype.updateSignedIn = function (ip) {
this.lastSignedInAt = new Date();
this.lastSignedInIp = ip;
return this.save({ hooks: false });
return this.save({
hooks: false,
});
};
// Returns a session token that is used to make API requests and is stored
@@ -173,11 +221,16 @@ User.prototype.getTransferToken = function () {
// It can only be used to sign in once and has a medium length expiry
User.prototype.getEmailSigninToken = function () {
return JWT.sign(
{ id: this.id, createdAt: new Date().toISOString(), type: "email-signin" },
{
id: this.id,
createdAt: new Date().toISOString(),
type: "email-signin",
},
this.jwtSecret
);
};
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'model' implicitly has an 'any' type.
const uploadAvatar = async (model) => {
const endpoint = publicS3Endpoint();
const { avatarUrl } = model;
@@ -203,28 +256,37 @@ const uploadAvatar = async (model) => {
}
};
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'model' implicitly has an 'any' type.
const setRandomJwtSecret = (model) => {
model.jwtSecret = crypto.randomBytes(64).toString("hex");
};
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'model' implicitly has an 'any' type.
const removeIdentifyingInfo = async (model, options) => {
await NotificationSetting.destroy({
where: { userId: model.id },
where: {
userId: model.id,
},
transaction: options.transaction,
});
await ApiKey.destroy({
where: { userId: model.id },
where: {
userId: model.id,
},
transaction: options.transaction,
});
await Star.destroy({
where: { userId: model.id },
where: {
userId: model.id,
},
transaction: options.transaction,
});
await UserAuthentication.destroy({
where: { userId: model.id },
where: {
userId: model.id,
},
transaction: options.transaction,
});
model.email = null;
model.name = "Unknown";
model.avatarUrl = "";
@@ -232,18 +294,20 @@ const removeIdentifyingInfo = async (model, options) => {
model.username = null;
model.lastActiveIp = null;
model.lastSignedInIp = null;
// this shouldn't be needed once this issue is resolved:
// https://github.com/sequelize/sequelize/issues/9318
await model.save({ hooks: false, transaction: options.transaction });
await model.save({
hooks: false,
transaction: options.transaction,
});
};
User.beforeDestroy(removeIdentifyingInfo);
User.beforeSave(uploadAvatar);
User.beforeCreate(setRandomJwtSecret);
// By default when a user signs up we subscribe them to email notifications
// when documents they created are edited by other team members and onboarding
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'user' implicitly has an 'any' type.
User.afterCreate(async (user, options) => {
await Promise.all([
NotificationSetting.findOrCreate({
@@ -293,7 +357,6 @@ User.getCounts = async function (teamId: string) {
},
});
const counts = results[0];
return {
active: parseInt(counts.activeCount),
admins: parseInt(counts.adminCount),
@@ -305,8 +368,10 @@ User.getCounts = async function (teamId: string) {
};
User.findAllInBatches = async (
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'query' implicitly has an 'any' type.
query,
callback: (users: Array<User>, query: Object) => Promise<void>
// @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message
callback: (users: Array<User>, query: Record<string, any>) => Promise<void>
) => {
if (!query.offset) query.offset = 0;
if (!query.limit) query.limit = 10;
@@ -314,7 +379,6 @@ User.findAllInBatches = async (
do {
results = await User.findAll(query);
await callback(results, query);
query.offset += query.limit;
} while (results.length >= query.limit);
@@ -337,17 +401,26 @@ User.prototype.demote = async function (
if (res.count >= 1) {
if (to === "member") {
return this.update({ isAdmin: false, isViewer: false });
return this.update({
isAdmin: false,
isViewer: false,
});
} else if (to === "viewer") {
return this.update({ isAdmin: false, isViewer: true });
return this.update({
isAdmin: false,
isViewer: true,
});
}
} else {
throw new ValidationError("At least one admin is required");
throw ValidationError("At least one admin is required");
}
};
User.prototype.promote = async function () {
return this.update({ isAdmin: true, isViewer: false });
return this.update({
isAdmin: true,
isViewer: false,
});
};
User.prototype.activate = async function () {

View File

@@ -1,4 +1,3 @@
// @flow
import { DataTypes, sequelize, encryptedFields } from "../sequelize";
const UserAuthentication = sequelize.define("user_authentications", {
@@ -16,6 +15,7 @@ const UserAuthentication = sequelize.define("user_authentications", {
},
});
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type.
UserAuthentication.associate = (models) => {
UserAuthentication.belongsTo(models.AuthenticationProvider);
UserAuthentication.belongsTo(models.User);

View File

@@ -1,7 +1,6 @@
// @flow
import { subMilliseconds } from "date-fns";
import { USER_PRESENCE_INTERVAL } from "../../shared/constants";
import { User } from "../models";
import { USER_PRESENCE_INTERVAL } from "@shared/constants";
import { User } from "@server/models";
import { DataTypes, Op, sequelize } from "../sequelize";
const View = sequelize.define("view", {
@@ -19,23 +18,32 @@ const View = sequelize.define("view", {
},
});
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type.
View.associate = (models) => {
View.belongsTo(models.Document);
View.belongsTo(models.User);
};
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'where' implicitly has an 'any' type.
View.increment = async (where) => {
const [model, created] = await View.findOrCreate({ where });
const [model, created] = await View.findOrCreate({
where,
});
if (!created) {
model.count += 1;
model.save();
}
return model;
};
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'documentId' implicitly has an 'any' typ... Remove this comment to see the full error message
View.findByDocument = async (documentId) => {
return View.findAll({
where: { documentId },
where: {
documentId,
},
order: [["updatedAt", "DESC"]],
include: [
{
@@ -46,6 +54,7 @@ View.findByDocument = async (documentId) => {
});
};
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'documentId' implicitly has an 'any' typ... Remove this comment to see the full error message
View.findRecentlyEditingByDocument = async (documentId) => {
return View.findAll({
where: {

View File

@@ -1,4 +1,3 @@
// @flow
import ApiKey from "./ApiKey";
import Attachment from "./Attachment";
import AuthenticationProvider from "./AuthenticationProvider";