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:
@@ -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",
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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",
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -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",
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
);
|
||||
@@ -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",
|
||||
@@ -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",
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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",
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
@@ -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",
|
||||
@@ -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",
|
||||
@@ -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",
|
||||
@@ -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");
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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");
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
@@ -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 () {
|
||||
@@ -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);
|
||||
@@ -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: {
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import ApiKey from "./ApiKey";
|
||||
import Attachment from "./Attachment";
|
||||
import AuthenticationProvider from "./AuthenticationProvider";
|
||||
Reference in New Issue
Block a user