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,27 +1,25 @@
|
||||
// @flow
|
||||
import { Collection, UserAuthentication } from "@server/models";
|
||||
import { buildUser, buildTeam } from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
import mailer from "../mailer";
|
||||
import { Collection, UserAuthentication } from "../models";
|
||||
import { buildUser, buildTeam } from "../test/factories";
|
||||
import { flushdb } from "../test/support";
|
||||
import accountProvisioner from "./accountProvisioner";
|
||||
|
||||
jest.mock("../mailer");
|
||||
|
||||
jest.mock("aws-sdk", () => {
|
||||
const mS3 = { putObject: jest.fn().mockReturnThis(), promise: jest.fn() };
|
||||
const mS3 = {
|
||||
putObject: jest.fn().mockReturnThis(),
|
||||
promise: jest.fn(),
|
||||
};
|
||||
return {
|
||||
S3: jest.fn(() => mS3),
|
||||
Endpoint: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// $FlowFixMe
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'mockReset' does not exist on type '(type... Remove this comment to see the full error message
|
||||
mailer.sendTemplate.mockReset();
|
||||
|
||||
return flushdb();
|
||||
});
|
||||
|
||||
describe("accountProvisioner", () => {
|
||||
const ip = "127.0.0.1";
|
||||
|
||||
@@ -49,10 +47,8 @@ describe("accountProvisioner", () => {
|
||||
scopes: ["read"],
|
||||
},
|
||||
});
|
||||
|
||||
const authentications = await user.getAuthentications();
|
||||
const auth = authentications[0];
|
||||
|
||||
expect(auth.accessToken).toEqual("123");
|
||||
expect(auth.scopes.length).toEqual(1);
|
||||
expect(auth.scopes[0]).toEqual("read");
|
||||
@@ -62,7 +58,6 @@ describe("accountProvisioner", () => {
|
||||
expect(isNewUser).toEqual(true);
|
||||
expect(isNewTeam).toEqual(true);
|
||||
expect(mailer.sendTemplate).toHaveBeenCalled();
|
||||
|
||||
const collectionCount = await Collection.count();
|
||||
expect(collectionCount).toEqual(1);
|
||||
});
|
||||
@@ -71,12 +66,13 @@ describe("accountProvisioner", () => {
|
||||
const existingTeam = await buildTeam();
|
||||
const providers = await existingTeam.getAuthenticationProviders();
|
||||
const authenticationProvider = providers[0];
|
||||
const existing = await buildUser({ teamId: existingTeam.id });
|
||||
const existing = await buildUser({
|
||||
teamId: existingTeam.id,
|
||||
});
|
||||
const authentications = await existing.getAuthentications();
|
||||
const authentication = authentications[0];
|
||||
const newEmail = "test@example.com";
|
||||
const newUsername = "tname";
|
||||
|
||||
const { user, isNewUser, isNewTeam } = await accountProvisioner({
|
||||
ip,
|
||||
user: {
|
||||
@@ -100,7 +96,6 @@ describe("accountProvisioner", () => {
|
||||
scopes: ["read"],
|
||||
},
|
||||
});
|
||||
|
||||
const auth = await UserAuthentication.findByPk(authentication.id);
|
||||
expect(auth.accessToken).toEqual("123");
|
||||
expect(auth.scopes.length).toEqual(1);
|
||||
@@ -110,7 +105,6 @@ describe("accountProvisioner", () => {
|
||||
expect(isNewTeam).toEqual(false);
|
||||
expect(isNewUser).toEqual(false);
|
||||
expect(mailer.sendTemplate).not.toHaveBeenCalled();
|
||||
|
||||
const collectionCount = await Collection.count();
|
||||
expect(collectionCount).toEqual(0);
|
||||
});
|
||||
@@ -119,9 +113,12 @@ describe("accountProvisioner", () => {
|
||||
const existingTeam = await buildTeam();
|
||||
const providers = await existingTeam.getAuthenticationProviders();
|
||||
const authenticationProvider = providers[0];
|
||||
await authenticationProvider.update({ enabled: false });
|
||||
|
||||
const existing = await buildUser({ teamId: existingTeam.id });
|
||||
await authenticationProvider.update({
|
||||
enabled: false,
|
||||
});
|
||||
const existing = await buildUser({
|
||||
teamId: existingTeam.id,
|
||||
});
|
||||
const authentications = await existing.getAuthentications();
|
||||
const authentication = authentications[0];
|
||||
let error;
|
||||
@@ -160,7 +157,6 @@ describe("accountProvisioner", () => {
|
||||
const team = await buildTeam();
|
||||
const authenticationProviders = await team.getAuthenticationProviders();
|
||||
const authenticationProvider = authenticationProviders[0];
|
||||
|
||||
const { user, isNewUser } = await accountProvisioner({
|
||||
ip,
|
||||
user: {
|
||||
@@ -184,10 +180,8 @@ describe("accountProvisioner", () => {
|
||||
scopes: ["read"],
|
||||
},
|
||||
});
|
||||
|
||||
const authentications = await user.getAuthentications();
|
||||
const auth = authentications[0];
|
||||
|
||||
expect(auth.accessToken).toEqual("123");
|
||||
expect(auth.scopes.length).toEqual(1);
|
||||
expect(auth.scopes[0]).toEqual("read");
|
||||
@@ -195,7 +189,6 @@ describe("accountProvisioner", () => {
|
||||
expect(user.username).toEqual("jtester");
|
||||
expect(isNewUser).toEqual(true);
|
||||
expect(mailer.sendTemplate).toHaveBeenCalled();
|
||||
|
||||
// should provision welcome collection
|
||||
const collectionCount = await Collection.count();
|
||||
expect(collectionCount).toEqual(1);
|
||||
@@ -1,48 +1,49 @@
|
||||
// @flow
|
||||
import invariant from "invariant";
|
||||
import Sequelize from "sequelize";
|
||||
import { Collection, Team, User } from "@server/models";
|
||||
import {
|
||||
AuthenticationError,
|
||||
EmailAuthenticationRequiredError,
|
||||
AuthenticationProviderDisabledError,
|
||||
} from "../errors";
|
||||
import mailer from "../mailer";
|
||||
import { Collection, Team, User } from "../models";
|
||||
import teamCreator from "./teamCreator";
|
||||
import userCreator from "./userCreator";
|
||||
|
||||
type Props = {|
|
||||
ip: string,
|
||||
user: {|
|
||||
name: string,
|
||||
email: string,
|
||||
avatarUrl?: string,
|
||||
username?: string,
|
||||
|},
|
||||
team: {|
|
||||
name: string,
|
||||
domain?: string,
|
||||
subdomain: string,
|
||||
avatarUrl?: string,
|
||||
|},
|
||||
authenticationProvider: {|
|
||||
name: string,
|
||||
providerId: string,
|
||||
|},
|
||||
authentication: {|
|
||||
providerId: string,
|
||||
scopes: string[],
|
||||
accessToken?: string,
|
||||
refreshToken?: string,
|
||||
|},
|
||||
|};
|
||||
type Props = {
|
||||
ip: string;
|
||||
user: {
|
||||
name: string;
|
||||
email: string;
|
||||
avatarUrl?: string;
|
||||
username?: string;
|
||||
};
|
||||
team: {
|
||||
name: string;
|
||||
domain?: string;
|
||||
subdomain: string;
|
||||
avatarUrl?: string;
|
||||
};
|
||||
authenticationProvider: {
|
||||
name: string;
|
||||
providerId: string;
|
||||
};
|
||||
authentication: {
|
||||
providerId: string;
|
||||
scopes: string[];
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type AccountProvisionerResult = {|
|
||||
user: User,
|
||||
team: Team,
|
||||
isNewTeam: boolean,
|
||||
isNewUser: boolean,
|
||||
|};
|
||||
export type AccountProvisionerResult = {
|
||||
// @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
|
||||
user: User;
|
||||
// @ts-expect-error ts-migrate(2749) FIXME: 'Team' refers to a value, but is being used as a t... Remove this comment to see the full error message
|
||||
team: Team;
|
||||
isNewTeam: boolean;
|
||||
isNewUser: boolean;
|
||||
};
|
||||
|
||||
export default async function accountProvisioner({
|
||||
ip,
|
||||
@@ -52,6 +53,7 @@ export default async function accountProvisioner({
|
||||
authentication: authenticationParams,
|
||||
}: Props): Promise<AccountProvisionerResult> {
|
||||
let result;
|
||||
|
||||
try {
|
||||
result = await teamCreator({
|
||||
name: teamParams.name,
|
||||
@@ -61,14 +63,14 @@ export default async function accountProvisioner({
|
||||
authenticationProvider: authenticationProviderParams,
|
||||
});
|
||||
} catch (err) {
|
||||
throw new AuthenticationError(err.message);
|
||||
throw AuthenticationError(err.message);
|
||||
}
|
||||
|
||||
invariant(result, "Team creator result must exist");
|
||||
const { authenticationProvider, team, isNewTeam } = result;
|
||||
|
||||
if (!authenticationProvider.enabled) {
|
||||
throw new AuthenticationProviderDisabledError();
|
||||
throw AuthenticationProviderDisabledError();
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -85,7 +87,6 @@ export default async function accountProvisioner({
|
||||
authenticationProviderId: authenticationProvider.id,
|
||||
},
|
||||
});
|
||||
|
||||
const { isNewUser, user } = result;
|
||||
|
||||
if (isNewUser) {
|
||||
@@ -102,7 +103,11 @@ export default async function accountProvisioner({
|
||||
// failed. In this case we have a valid previously created team but no
|
||||
// onboarding collection.
|
||||
if (!isNewTeam) {
|
||||
const count = await Collection.count({ where: { teamId: team.id } });
|
||||
const count = await Collection.count({
|
||||
where: {
|
||||
teamId: team.id,
|
||||
},
|
||||
});
|
||||
provision = count === 0;
|
||||
}
|
||||
|
||||
@@ -127,12 +132,12 @@ export default async function accountProvisioner({
|
||||
});
|
||||
|
||||
if (exists) {
|
||||
throw new EmailAuthenticationRequiredError(
|
||||
throw EmailAuthenticationRequiredError(
|
||||
"Email authentication required",
|
||||
team.url
|
||||
);
|
||||
} else {
|
||||
throw new AuthenticationError(err.message, team.url);
|
||||
throw AuthenticationError(err.message, team.url);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// @flow
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { Attachment, Event, User } from "../models";
|
||||
import { uploadToS3FromBuffer } from "../utils/s3";
|
||||
import { Attachment, Event, User } from "@server/models";
|
||||
import { uploadToS3FromBuffer } from "@server/utils/s3";
|
||||
|
||||
export default async function attachmentCreator({
|
||||
name,
|
||||
@@ -11,17 +10,17 @@ export default async function attachmentCreator({
|
||||
source,
|
||||
ip,
|
||||
}: {
|
||||
name: string,
|
||||
type: string,
|
||||
buffer: Buffer,
|
||||
user: User,
|
||||
source?: "import",
|
||||
ip: string,
|
||||
name: string;
|
||||
type: string;
|
||||
buffer: Buffer;
|
||||
// @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
|
||||
user: User;
|
||||
source?: "import";
|
||||
ip: string;
|
||||
}) {
|
||||
const key = `uploads/${user.id}/${uuidv4()}/${name}`;
|
||||
const acl = process.env.AWS_S3_ACL || "private";
|
||||
const url = await uploadToS3FromBuffer(buffer, type, key, acl);
|
||||
|
||||
const attachment = await Attachment.create({
|
||||
key,
|
||||
acl,
|
||||
@@ -31,15 +30,16 @@ export default async function attachmentCreator({
|
||||
teamId: user.teamId,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
await Event.create({
|
||||
name: "attachments.create",
|
||||
data: { name, source },
|
||||
data: {
|
||||
name,
|
||||
source,
|
||||
},
|
||||
modelId: attachment.id,
|
||||
teamId: user.teamId,
|
||||
actorId: user.id,
|
||||
ip,
|
||||
});
|
||||
|
||||
return attachment;
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
// @flow
|
||||
import { Collection, Event, Team, User, FileOperation } from "../models";
|
||||
import { getAWSKeyForFileOp } from "../utils/s3";
|
||||
import { Collection, Event, Team, User, FileOperation } from "@server/models";
|
||||
import { getAWSKeyForFileOp } from "@server/utils/s3";
|
||||
|
||||
export default async function collectionExporter({
|
||||
collection,
|
||||
@@ -8,14 +7,16 @@ export default async function collectionExporter({
|
||||
user,
|
||||
ip,
|
||||
}: {
|
||||
collection?: Collection,
|
||||
team: Team,
|
||||
user: User,
|
||||
ip: string,
|
||||
// @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?: Collection;
|
||||
// @ts-expect-error ts-migrate(2749) FIXME: 'Team' refers to a value, but is being used as a t... Remove this comment to see the full error message
|
||||
team: Team;
|
||||
// @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
|
||||
user: User;
|
||||
ip: string;
|
||||
}) {
|
||||
const collectionId = collection?.id;
|
||||
const key = getAWSKeyForFileOp(user.teamId, collection?.name || team.name);
|
||||
|
||||
const fileOperation = await FileOperation.create({
|
||||
type: "export",
|
||||
state: "creating",
|
||||
@@ -26,7 +27,6 @@ export default async function collectionExporter({
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
});
|
||||
|
||||
// Event is consumed on worker in queues/processors/exports
|
||||
await Event.create({
|
||||
name: collection ? "collections.export" : "collections.export_all",
|
||||
@@ -36,9 +36,7 @@ export default async function collectionExporter({
|
||||
modelId: fileOperation.id,
|
||||
ip,
|
||||
});
|
||||
|
||||
fileOperation.user = user;
|
||||
fileOperation.collection = collection;
|
||||
|
||||
return fileOperation;
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
// @flow
|
||||
import path from "path";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'form... Remove this comment to see the full error message
|
||||
import File from "formidable/lib/file";
|
||||
import { Attachment, Document, Collection } from "../models";
|
||||
import { buildUser } from "../test/factories";
|
||||
import { flushdb } from "../test/support";
|
||||
import { Attachment, Document, Collection } from "@server/models";
|
||||
import { buildUser } from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
import collectionImporter from "./collectionImporter";
|
||||
|
||||
jest.mock("../utils/s3");
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
|
||||
describe("collectionImporter", () => {
|
||||
const ip = "127.0.0.1";
|
||||
|
||||
@@ -21,18 +19,15 @@ describe("collectionImporter", () => {
|
||||
type: "application/zip",
|
||||
path: path.resolve(__dirname, "..", "test", "fixtures", name),
|
||||
});
|
||||
|
||||
const response = await collectionImporter({
|
||||
type: "outline",
|
||||
user,
|
||||
file,
|
||||
ip,
|
||||
});
|
||||
|
||||
expect(response.collections.length).toEqual(1);
|
||||
expect(response.documents.length).toEqual(8);
|
||||
expect(response.attachments.length).toEqual(6);
|
||||
|
||||
expect(await Collection.count()).toEqual(1);
|
||||
expect(await Document.count()).toEqual(8);
|
||||
expect(await Attachment.count()).toEqual(6);
|
||||
@@ -46,8 +41,8 @@ describe("collectionImporter", () => {
|
||||
type: "application/zip",
|
||||
path: path.resolve(__dirname, "..", "test", "fixtures", name),
|
||||
});
|
||||
|
||||
let error;
|
||||
|
||||
try {
|
||||
await collectionImporter({
|
||||
type: "outline",
|
||||
@@ -70,8 +65,8 @@ describe("collectionImporter", () => {
|
||||
type: "application/zip",
|
||||
path: path.resolve(__dirname, "..", "test", "fixtures", name),
|
||||
});
|
||||
|
||||
let error;
|
||||
|
||||
try {
|
||||
await collectionImporter({
|
||||
type: "outline",
|
||||
@@ -1,15 +1,15 @@
|
||||
// @flow
|
||||
import fs from "fs";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'form... Remove this comment to see the full error message
|
||||
import File from "formidable/lib/file";
|
||||
import invariant from "invariant";
|
||||
import { values, keys } from "lodash";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { parseOutlineExport } from "../../shared/utils/zip";
|
||||
import { parseOutlineExport } from "@shared/utils/zip";
|
||||
import Logger from "@server/logging/logger";
|
||||
import { Attachment, Event, Document, Collection, User } from "@server/models";
|
||||
import { FileImportError } from "../errors";
|
||||
import Logger from "../logging/logger";
|
||||
import { Attachment, Event, Document, Collection, User } from "../models";
|
||||
import attachmentCreator from "./attachmentCreator";
|
||||
import documentCreator from "./documentCreator";
|
||||
import documentImporter from "./documentImporter";
|
||||
@@ -20,36 +20,48 @@ export default async function collectionImporter({
|
||||
user,
|
||||
ip,
|
||||
}: {
|
||||
file: File,
|
||||
user: User,
|
||||
type: "outline",
|
||||
ip: string,
|
||||
file: File;
|
||||
// @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
|
||||
user: User;
|
||||
type: "outline";
|
||||
ip: string;
|
||||
}) {
|
||||
// load the zip structure into memory
|
||||
const zipData = await fs.promises.readFile(file.path);
|
||||
|
||||
let items;
|
||||
|
||||
try {
|
||||
items = await await parseOutlineExport(zipData);
|
||||
} catch (err) {
|
||||
throw new FileImportError(err.message);
|
||||
throw FileImportError(err.message);
|
||||
}
|
||||
|
||||
if (!items.filter((item) => item.type === "document").length) {
|
||||
throw new FileImportError(
|
||||
throw FileImportError(
|
||||
"Uploaded file does not contain importable documents"
|
||||
);
|
||||
}
|
||||
|
||||
// store progress and pointers
|
||||
let collections: { string: Collection } = {};
|
||||
let documents: { string: Document } = {};
|
||||
let attachments: { string: Attachment } = {};
|
||||
// @ts-expect-error ts-migrate(2741) FIXME: Property 'string' is missing in type '{}' but requ... Remove this comment to see the full error message
|
||||
const collections: {
|
||||
// @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
|
||||
string: Collection;
|
||||
} = {};
|
||||
// @ts-expect-error ts-migrate(2741) FIXME: Property 'string' is missing in type '{}' but requ... Remove this comment to see the full error message
|
||||
const documents: {
|
||||
string: Document;
|
||||
} = {};
|
||||
// @ts-expect-error ts-migrate(2741) FIXME: Property 'string' is missing in type '{}' but requ... Remove this comment to see the full error message
|
||||
const attachments: {
|
||||
// @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
|
||||
string: Attachment;
|
||||
} = {};
|
||||
|
||||
for (const item of items) {
|
||||
if (item.type === "collection") {
|
||||
// check if collection with name exists
|
||||
let [collection, isCreated] = await Collection.findOrCreate({
|
||||
const [collection, isCreated] = await Collection.findOrCreate({
|
||||
where: {
|
||||
teamId: user.teamId,
|
||||
name: item.name,
|
||||
@@ -65,6 +77,7 @@ export default async function collectionImporter({
|
||||
// with right now
|
||||
if (!isCreated) {
|
||||
const name = `${item.name} (Imported)`;
|
||||
// @ts-expect-error ts-migrate(2588) FIXME: Cannot assign to 'collection' because it is a cons... Remove this comment to see the full error message
|
||||
collection = await Collection.create({
|
||||
teamId: user.teamId,
|
||||
createdById: user.id,
|
||||
@@ -76,7 +89,9 @@ export default async function collectionImporter({
|
||||
collectionId: collection.id,
|
||||
teamId: collection.teamId,
|
||||
actorId: user.id,
|
||||
data: { name },
|
||||
data: {
|
||||
name,
|
||||
},
|
||||
ip,
|
||||
});
|
||||
}
|
||||
@@ -89,30 +104,26 @@ export default async function collectionImporter({
|
||||
const collectionDir = item.dir.split("/")[0];
|
||||
const collection = collections[collectionDir];
|
||||
invariant(collection, `Collection must exist for document ${item.dir}`);
|
||||
|
||||
// we have a document
|
||||
const content = await item.item.async("string");
|
||||
const name = path.basename(item.name);
|
||||
const tmpDir = os.tmpdir();
|
||||
const tmpFilePath = `${tmpDir}/upload-${uuidv4()}`;
|
||||
|
||||
await fs.promises.writeFile(tmpFilePath, content);
|
||||
const file = new File({
|
||||
name,
|
||||
type: "text/markdown",
|
||||
path: tmpFilePath,
|
||||
});
|
||||
|
||||
const { text, title } = await documentImporter({
|
||||
file,
|
||||
user,
|
||||
ip,
|
||||
});
|
||||
|
||||
await fs.promises.unlink(tmpFilePath);
|
||||
|
||||
// must be a nested document, find and reference the parent document
|
||||
let parentDocumentId;
|
||||
|
||||
if (item.depth > 1) {
|
||||
const parentDocument =
|
||||
documents[`${item.dir}.md`] || documents[item.dir];
|
||||
@@ -128,13 +139,14 @@ export default async function collectionImporter({
|
||||
collectionId: collection.id,
|
||||
createdAt: item.metadata.createdAt
|
||||
? new Date(item.metadata.createdAt)
|
||||
: item.date,
|
||||
: // @ts-expect-error ts-migrate(2339) FIXME: Property 'date' does not exist on type 'Item'.
|
||||
item.date,
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'date' does not exist on type 'Item'.
|
||||
updatedAt: item.date,
|
||||
parentDocumentId,
|
||||
user,
|
||||
ip,
|
||||
});
|
||||
|
||||
documents[item.path] = document;
|
||||
continue;
|
||||
}
|
||||
@@ -168,14 +180,16 @@ export default async function collectionImporter({
|
||||
/(.*)uploads\//,
|
||||
"uploads/"
|
||||
);
|
||||
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'text' does not exist on type 'Document'.
|
||||
document.text = document.text
|
||||
.replace(attachmentPath, attachment.redirectUrl)
|
||||
.replace(normalizedAttachmentPath, attachment.redirectUrl)
|
||||
.replace(`/${normalizedAttachmentPath}`, attachment.redirectUrl);
|
||||
|
||||
// does nothing if the document text is unchanged
|
||||
await document.save({ fields: ["text"] });
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'save' does not exist on type 'Document'.
|
||||
await document.save({
|
||||
fields: ["text"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// @flow
|
||||
import { Document, Event, User } from "../models";
|
||||
import { Document, Event, User } from "@server/models";
|
||||
|
||||
export default async function documentCreator({
|
||||
title = "",
|
||||
@@ -8,7 +7,8 @@ export default async function documentCreator({
|
||||
collectionId,
|
||||
parentDocumentId,
|
||||
templateDocument,
|
||||
createdAt, // allows override for import
|
||||
createdAt,
|
||||
// allows override for import
|
||||
updatedAt,
|
||||
template,
|
||||
index,
|
||||
@@ -17,23 +17,26 @@ export default async function documentCreator({
|
||||
source,
|
||||
ip,
|
||||
}: {
|
||||
title: string,
|
||||
text: string,
|
||||
publish?: boolean,
|
||||
collectionId: string,
|
||||
parentDocumentId?: string,
|
||||
templateDocument?: Document,
|
||||
template?: boolean,
|
||||
createdAt?: Date,
|
||||
updatedAt?: Date,
|
||||
index?: number,
|
||||
user: User,
|
||||
editorVersion?: string,
|
||||
source?: "import",
|
||||
ip: string,
|
||||
title: string;
|
||||
text: string;
|
||||
publish?: boolean;
|
||||
collectionId: string;
|
||||
parentDocumentId?: string;
|
||||
templateDocument?: Document;
|
||||
template?: boolean;
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
index?: number;
|
||||
// @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
|
||||
user: User;
|
||||
editorVersion?: string;
|
||||
source?: "import";
|
||||
ip: string;
|
||||
// @ts-expect-error ts-migrate(1064) FIXME: The return type of an async function or method mus... Remove this comment to see the full error message
|
||||
}): Document {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'Document'.
|
||||
const templateId = templateDocument ? templateDocument.id : undefined;
|
||||
let document = await Document.create({
|
||||
const document = await Document.create({
|
||||
parentDocumentId,
|
||||
editorVersion,
|
||||
collectionId,
|
||||
@@ -46,29 +49,35 @@ export default async function documentCreator({
|
||||
template,
|
||||
templateId,
|
||||
title: templateDocument ? templateDocument.title : title,
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'text' does not exist on type 'Document'.
|
||||
text: templateDocument ? templateDocument.text : text,
|
||||
});
|
||||
|
||||
await Event.create({
|
||||
name: "documents.create",
|
||||
documentId: document.id,
|
||||
collectionId: document.collectionId,
|
||||
teamId: document.teamId,
|
||||
actorId: user.id,
|
||||
data: { source, title: document.title, templateId },
|
||||
data: {
|
||||
source,
|
||||
title: document.title,
|
||||
templateId,
|
||||
},
|
||||
ip,
|
||||
});
|
||||
|
||||
if (publish) {
|
||||
await document.publish(user.id);
|
||||
|
||||
await Event.create({
|
||||
name: "documents.publish",
|
||||
documentId: document.id,
|
||||
collectionId: document.collectionId,
|
||||
teamId: document.teamId,
|
||||
actorId: user.id,
|
||||
data: { source, title: document.title },
|
||||
data: {
|
||||
source,
|
||||
title: document.title,
|
||||
},
|
||||
ip,
|
||||
});
|
||||
}
|
||||
@@ -77,6 +86,9 @@ export default async function documentCreator({
|
||||
// we need to specify publishedAt to bypass default scope that only returns
|
||||
// published documents
|
||||
return Document.findOne({
|
||||
where: { id: document.id, publishedAt: document.publishedAt },
|
||||
where: {
|
||||
id: document.id,
|
||||
publishedAt: document.publishedAt,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
// @flow
|
||||
import path from "path";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'form... Remove this comment to see the full error message
|
||||
import File from "formidable/lib/file";
|
||||
import { Attachment } from "../models";
|
||||
import { buildUser } from "../test/factories";
|
||||
import { flushdb } from "../test/support";
|
||||
import { Attachment } from "@server/models";
|
||||
import { buildUser } from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
import documentImporter from "./documentImporter";
|
||||
|
||||
jest.mock("../utils/s3");
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
|
||||
describe("documentImporter", () => {
|
||||
const ip = "127.0.0.1";
|
||||
|
||||
@@ -22,16 +20,13 @@ describe("documentImporter", () => {
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
path: path.resolve(__dirname, "..", "test", "fixtures", name),
|
||||
});
|
||||
|
||||
const response = await documentImporter({
|
||||
user,
|
||||
file,
|
||||
ip,
|
||||
});
|
||||
|
||||
const attachments = await Attachment.count();
|
||||
expect(attachments).toEqual(1);
|
||||
|
||||
expect(response.text).toContain("This is a test document for images");
|
||||
expect(response.text).toContain(";
|
||||
expect(response.title).toEqual("images");
|
||||
@@ -45,16 +40,13 @@ describe("documentImporter", () => {
|
||||
type: "application/octet-stream",
|
||||
path: path.resolve(__dirname, "..", "test", "fixtures", name),
|
||||
});
|
||||
|
||||
const response = await documentImporter({
|
||||
user,
|
||||
file,
|
||||
ip,
|
||||
});
|
||||
|
||||
const attachments = await Attachment.count();
|
||||
expect(attachments).toEqual(1);
|
||||
|
||||
expect(response.text).toContain("This is a test document for images");
|
||||
expect(response.text).toContain(";
|
||||
expect(response.title).toEqual("images");
|
||||
@@ -68,8 +60,8 @@ describe("documentImporter", () => {
|
||||
type: "application/octet-stream",
|
||||
path: path.resolve(__dirname, "..", "test", "fixtures", name),
|
||||
});
|
||||
|
||||
let error;
|
||||
|
||||
try {
|
||||
await documentImporter({
|
||||
user,
|
||||
@@ -91,16 +83,13 @@ describe("documentImporter", () => {
|
||||
type: "application/octet-stream",
|
||||
path: path.resolve(__dirname, "..", "test", "fixtures", name),
|
||||
});
|
||||
|
||||
const response = await documentImporter({
|
||||
user,
|
||||
file,
|
||||
ip,
|
||||
});
|
||||
|
||||
const attachments = await Attachment.count();
|
||||
expect(attachments).toEqual(1);
|
||||
|
||||
expect(response.text).toContain("This is a test document for images");
|
||||
expect(response.text).toContain(";
|
||||
expect(response.title).toEqual("images");
|
||||
@@ -114,13 +103,11 @@ describe("documentImporter", () => {
|
||||
type: "text/html",
|
||||
path: path.resolve(__dirname, "..", "test", "fixtures", name),
|
||||
});
|
||||
|
||||
const response = await documentImporter({
|
||||
user,
|
||||
file,
|
||||
ip,
|
||||
});
|
||||
|
||||
expect(response.text).toContain("Text paragraph");
|
||||
expect(response.title).toEqual("Heading 1");
|
||||
});
|
||||
@@ -133,13 +120,11 @@ describe("documentImporter", () => {
|
||||
type: "application/msword",
|
||||
path: path.resolve(__dirname, "..", "test", "fixtures", name),
|
||||
});
|
||||
|
||||
const response = await documentImporter({
|
||||
user,
|
||||
file,
|
||||
ip,
|
||||
});
|
||||
|
||||
expect(response.text).toContain("this is a test document");
|
||||
expect(response.title).toEqual("Heading 1");
|
||||
});
|
||||
@@ -152,13 +137,11 @@ describe("documentImporter", () => {
|
||||
type: "text/plain",
|
||||
path: path.resolve(__dirname, "..", "test", "fixtures", name),
|
||||
});
|
||||
|
||||
const response = await documentImporter({
|
||||
user,
|
||||
file,
|
||||
ip,
|
||||
});
|
||||
|
||||
expect(response.text).toContain("This is a test paragraph");
|
||||
expect(response.title).toEqual("Heading 1");
|
||||
});
|
||||
@@ -171,13 +154,11 @@ describe("documentImporter", () => {
|
||||
type: "text/plain",
|
||||
path: path.resolve(__dirname, "..", "test", "fixtures", "empty.md"),
|
||||
});
|
||||
|
||||
const response = await documentImporter({
|
||||
user,
|
||||
file,
|
||||
ip,
|
||||
});
|
||||
|
||||
expect(response.text).toContain("");
|
||||
expect(response.title).toEqual("this / and / this");
|
||||
});
|
||||
@@ -190,13 +171,11 @@ describe("documentImporter", () => {
|
||||
type: "application/lol",
|
||||
path: path.resolve(__dirname, "..", "test", "fixtures", name),
|
||||
});
|
||||
|
||||
const response = await documentImporter({
|
||||
user,
|
||||
file,
|
||||
ip,
|
||||
});
|
||||
|
||||
expect(response.text).toContain("This is a test paragraph");
|
||||
expect(response.title).toEqual("Heading 1");
|
||||
});
|
||||
@@ -209,8 +188,8 @@ describe("documentImporter", () => {
|
||||
type: "executable/zip",
|
||||
path: path.resolve(__dirname, "..", "test", "fixtures", name),
|
||||
});
|
||||
|
||||
let error;
|
||||
|
||||
try {
|
||||
await documentImporter({
|
||||
user,
|
||||
@@ -1,18 +1,19 @@
|
||||
// @flow
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'form... Remove this comment to see the full error message
|
||||
import File from "formidable/lib/file";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'jopl... Remove this comment to see the full error message
|
||||
import { strikethrough, tables } from "joplin-turndown-plugin-gfm";
|
||||
import mammoth from "mammoth";
|
||||
import quotedPrintable from "quoted-printable";
|
||||
import TurndownService from "turndown";
|
||||
import utf8 from "utf8";
|
||||
import parseTitle from "../../shared/utils/parseTitle";
|
||||
import parseTitle from "@shared/utils/parseTitle";
|
||||
import { User } from "@server/models";
|
||||
import dataURItoBuffer from "@server/utils/dataURItoBuffer";
|
||||
import { deserializeFilename } from "@server/utils/fs";
|
||||
import parseImages from "@server/utils/parseImages";
|
||||
import { FileImportError, InvalidRequestError } from "../errors";
|
||||
import { User } from "../models";
|
||||
import dataURItoBuffer from "../utils/dataURItoBuffer";
|
||||
import { deserializeFilename } from "../utils/fs";
|
||||
import parseImages from "../utils/parseImages";
|
||||
import attachmentCreator from "./attachmentCreator";
|
||||
|
||||
// https://github.com/domchristie/turndown#options
|
||||
@@ -21,7 +22,6 @@ const turndownService = new TurndownService({
|
||||
bulletListMarker: "-",
|
||||
headingStyle: "atx",
|
||||
});
|
||||
|
||||
// Use the GitHub-flavored markdown plugin to parse
|
||||
// strikethoughs and tables
|
||||
turndownService
|
||||
@@ -33,7 +33,6 @@ turndownService
|
||||
return "\n";
|
||||
},
|
||||
});
|
||||
|
||||
interface ImportableFile {
|
||||
type: string;
|
||||
getMarkdown: (file: any) => Promise<string>;
|
||||
@@ -67,20 +66,24 @@ const importMapping: ImportableFile[] = [
|
||||
},
|
||||
];
|
||||
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'file' implicitly has an 'any' type.
|
||||
async function fileToMarkdown(file): Promise<string> {
|
||||
return fs.promises.readFile(file.path, "utf8");
|
||||
}
|
||||
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'file' implicitly has an 'any' type.
|
||||
async function docxToMarkdown(file): Promise<string> {
|
||||
const { value } = await mammoth.convertToHtml(file);
|
||||
return turndownService.turndown(value);
|
||||
}
|
||||
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'file' implicitly has an 'any' type.
|
||||
async function htmlToMarkdown(file): Promise<string> {
|
||||
const value = await fs.promises.readFile(file.path, "utf8");
|
||||
return turndownService.turndown(value);
|
||||
}
|
||||
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'file' implicitly has an 'any' type.
|
||||
async function confluenceToMarkdown(file): Promise<string> {
|
||||
let value = await fs.promises.readFile(file.path, "utf8");
|
||||
|
||||
@@ -88,13 +91,14 @@ async function confluenceToMarkdown(file): Promise<string> {
|
||||
// Word documents should call into the docxToMarkdown importer.
|
||||
// See: https://jira.atlassian.com/browse/CONFSERVER-38237
|
||||
if (!value.includes("Content-Type: multipart/related")) {
|
||||
throw new FileImportError("Unsupported Word file");
|
||||
throw FileImportError("Unsupported Word file");
|
||||
}
|
||||
|
||||
// get boundary marker
|
||||
const boundaryMarker = value.match(/boundary="(.+)"/);
|
||||
|
||||
if (!boundaryMarker) {
|
||||
throw new FileImportError("Unsupported Word file (No boundary marker)");
|
||||
throw FileImportError("Unsupported Word file (No boundary marker)");
|
||||
}
|
||||
|
||||
// get content between multipart boundaries
|
||||
@@ -104,6 +108,7 @@ async function confluenceToMarkdown(file): Promise<string> {
|
||||
boundaryReached++;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (line.startsWith("Content-")) {
|
||||
return false;
|
||||
}
|
||||
@@ -114,11 +119,12 @@ async function confluenceToMarkdown(file): Promise<string> {
|
||||
if (boundaryReached === 2) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!lines.length) {
|
||||
throw new FileImportError("Unsupported Word file (No content found)");
|
||||
throw FileImportError("Unsupported Word file (No content found)");
|
||||
}
|
||||
|
||||
// Mime attachment is "quoted printable" encoded, must be decoded first
|
||||
@@ -127,11 +133,10 @@ async function confluenceToMarkdown(file): Promise<string> {
|
||||
|
||||
// If we don't remove the title here it becomes printed in the document
|
||||
// body by turndown
|
||||
turndownService.remove(["style", "xml", "title"]);
|
||||
turndownService.remove(["style", "title"]);
|
||||
|
||||
// Now we should have something that looks like HTML
|
||||
const html = turndownService.turndown(value);
|
||||
|
||||
return html.replace(/<br>/g, " \\n ");
|
||||
}
|
||||
|
||||
@@ -140,10 +145,14 @@ export default async function documentImporter({
|
||||
user,
|
||||
ip,
|
||||
}: {
|
||||
user: User,
|
||||
file: File,
|
||||
ip: string,
|
||||
}): Promise<{ text: string, title: string }> {
|
||||
// @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
|
||||
user: User;
|
||||
file: File;
|
||||
ip: string;
|
||||
}): Promise<{
|
||||
text: string;
|
||||
title: string;
|
||||
}> {
|
||||
const fileInfo = importMapping.filter((item) => {
|
||||
if (item.type === file.type) {
|
||||
if (
|
||||
@@ -152,17 +161,21 @@ export default async function documentImporter({
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (item.type === "text/markdown" && path.extname(file.name) === ".md") {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})[0];
|
||||
|
||||
if (!fileInfo) {
|
||||
throw new InvalidRequestError(`File type ${file.type} not supported`);
|
||||
throw InvalidRequestError(`File type ${file.type} not supported`);
|
||||
}
|
||||
|
||||
let title = deserializeFilename(file.name.replace(/\.[^/.]+$/, ""));
|
||||
let text = await fileInfo.getMarkdown(file);
|
||||
|
||||
@@ -181,7 +194,6 @@ export default async function documentImporter({
|
||||
for (const uri of dataURIs) {
|
||||
const name = "imported";
|
||||
const { buffer, type } = dataURItoBuffer(uri);
|
||||
|
||||
const attachment = await attachmentCreator({
|
||||
name,
|
||||
type,
|
||||
@@ -189,9 +201,11 @@ export default async function documentImporter({
|
||||
user,
|
||||
ip,
|
||||
});
|
||||
|
||||
text = text.replace(uri, attachment.redirectUrl);
|
||||
}
|
||||
|
||||
return { text, title };
|
||||
return {
|
||||
text,
|
||||
title,
|
||||
};
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
// @flow
|
||||
import { Document, Attachment, Collection, User, Event } from "../models";
|
||||
import { sequelize } from "../sequelize";
|
||||
import parseAttachmentIds from "../utils/parseAttachmentIds";
|
||||
|
||||
async function copyAttachments(document: Document, options) {
|
||||
let text = document.text;
|
||||
const documentId = document.id;
|
||||
|
||||
// find any image attachments that are in this documents text
|
||||
const attachmentIds = parseAttachmentIds(text);
|
||||
|
||||
for (const id of attachmentIds) {
|
||||
const existing = await Attachment.findOne({
|
||||
where: {
|
||||
teamId: document.teamId,
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
// if the image attachment was originally uploaded to another document
|
||||
// (this can happen in various ways, copy/paste, or duplicate for example)
|
||||
// then create a new attachment pointed to this doc and update the reference
|
||||
// in the text so that it gets the moved documents permissions
|
||||
if (existing && existing.documentId !== documentId) {
|
||||
const { id, ...rest } = existing.dataValues;
|
||||
const attachment = await Attachment.create(
|
||||
{
|
||||
...rest,
|
||||
documentId,
|
||||
},
|
||||
options
|
||||
);
|
||||
text = text.replace(existing.redirectUrl, attachment.redirectUrl);
|
||||
}
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
export default async function documentMover({
|
||||
user,
|
||||
document,
|
||||
collectionId,
|
||||
parentDocumentId = null, // convert undefined to null so parentId comparison treats them as equal
|
||||
index,
|
||||
ip,
|
||||
}: {
|
||||
user: User,
|
||||
document: Document,
|
||||
collectionId: string,
|
||||
parentDocumentId?: string,
|
||||
index?: number,
|
||||
ip: string,
|
||||
}) {
|
||||
let transaction;
|
||||
const collectionChanged = collectionId !== document.collectionId;
|
||||
const result = { collections: [], documents: [], collectionChanged };
|
||||
|
||||
if (document.template) {
|
||||
if (!collectionChanged) {
|
||||
return result;
|
||||
}
|
||||
|
||||
document.collectionId = collectionId;
|
||||
document.parentDocumentId = null;
|
||||
document.lastModifiedById = user.id;
|
||||
document.updatedBy = user;
|
||||
|
||||
await document.save();
|
||||
result.documents.push(document);
|
||||
} else {
|
||||
try {
|
||||
transaction = await sequelize.transaction();
|
||||
|
||||
// remove from original collection
|
||||
const collection = await Collection.findByPk(document.collectionId, {
|
||||
transaction,
|
||||
paranoid: false,
|
||||
});
|
||||
const [
|
||||
documentJson,
|
||||
fromIndex,
|
||||
] = (await collection.removeDocumentInStructure(document, {
|
||||
save: false,
|
||||
})) || [undefined, index];
|
||||
|
||||
// if we're reordering from within the same parent
|
||||
// the original and destination collection are the same,
|
||||
// so when the initial item is removed above, the list will reduce by 1.
|
||||
// We need to compensate for this when reordering
|
||||
const toIndex =
|
||||
index !== undefined &&
|
||||
document.parentDocumentId === parentDocumentId &&
|
||||
document.collectionId === collectionId &&
|
||||
fromIndex < index
|
||||
? index - 1
|
||||
: index;
|
||||
|
||||
// if the collection is the same then it will get saved below, this
|
||||
// line prevents a pointless intermediate save from occurring.
|
||||
if (collectionChanged) {
|
||||
await collection.save({ transaction });
|
||||
|
||||
document.text = await copyAttachments(document, { transaction });
|
||||
}
|
||||
|
||||
// add to new collection (may be the same)
|
||||
document.collectionId = collectionId;
|
||||
document.parentDocumentId = parentDocumentId;
|
||||
document.lastModifiedById = user.id;
|
||||
document.updatedBy = user;
|
||||
|
||||
const newCollection: Collection = collectionChanged
|
||||
? await Collection.scope({
|
||||
method: ["withMembership", user.id],
|
||||
}).findByPk(collectionId, { transaction })
|
||||
: collection;
|
||||
await newCollection.addDocumentToStructure(document, toIndex, {
|
||||
documentJson,
|
||||
});
|
||||
result.collections.push(collection);
|
||||
|
||||
// if collection does not remain the same loop through children and change their
|
||||
// collectionId and move any attachments they may have too. This includes
|
||||
// archived children, otherwise their collection would be wrong once restored.
|
||||
if (collectionChanged) {
|
||||
result.collections.push(newCollection);
|
||||
|
||||
const loopChildren = async (documentId) => {
|
||||
const childDocuments = await Document.findAll({
|
||||
where: { parentDocumentId: documentId },
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
childDocuments.map(async (child) => {
|
||||
await loopChildren(child.id);
|
||||
child.text = await copyAttachments(child, { transaction });
|
||||
child.collectionId = collectionId;
|
||||
await child.save();
|
||||
child.collection = newCollection;
|
||||
result.documents.push(child);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
await loopChildren(document.id);
|
||||
}
|
||||
|
||||
await document.save({ transaction });
|
||||
|
||||
document.collection = newCollection;
|
||||
result.documents.push(document);
|
||||
|
||||
await transaction.commit();
|
||||
} catch (err) {
|
||||
if (transaction) {
|
||||
await transaction.rollback();
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
await Event.create({
|
||||
name: "documents.move",
|
||||
actorId: user.id,
|
||||
documentId: document.id,
|
||||
collectionId,
|
||||
teamId: document.teamId,
|
||||
data: {
|
||||
title: document.title,
|
||||
collectionIds: result.collections.map((c) => c.id),
|
||||
documentIds: result.documents.map((d) => d.id),
|
||||
},
|
||||
ip,
|
||||
});
|
||||
|
||||
// we need to send all updated models back to the client
|
||||
return result;
|
||||
}
|
||||
@@ -1,47 +1,45 @@
|
||||
// @flow
|
||||
import { Attachment } from "../models";
|
||||
import { Attachment } from "@server/models";
|
||||
import {
|
||||
buildDocument,
|
||||
buildAttachment,
|
||||
buildCollection,
|
||||
buildUser,
|
||||
} from "../test/factories";
|
||||
import { flushdb, seed } from "../test/support";
|
||||
import parseAttachmentIds from "../utils/parseAttachmentIds";
|
||||
} from "@server/test/factories";
|
||||
import { flushdb, seed } from "@server/test/support";
|
||||
import parseAttachmentIds from "@server/utils/parseAttachmentIds";
|
||||
import documentMover from "./documentMover";
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
|
||||
describe("documentMover", () => {
|
||||
const ip = "127.0.0.1";
|
||||
|
||||
it("should move within a collection", async () => {
|
||||
const { document, user, collection } = await seed();
|
||||
|
||||
const response = await documentMover({
|
||||
user,
|
||||
document,
|
||||
collectionId: collection.id,
|
||||
ip,
|
||||
});
|
||||
|
||||
expect(response.collections.length).toEqual(1);
|
||||
expect(response.documents.length).toEqual(1);
|
||||
});
|
||||
|
||||
it("should not error when not in source collection documentStructure", async () => {
|
||||
const user = await buildUser();
|
||||
const collection = await buildCollection({ teamId: user.teamId });
|
||||
const document = await buildDocument({ collectionId: collection.id });
|
||||
const collection = await buildCollection({
|
||||
teamId: user.teamId,
|
||||
});
|
||||
const document = await buildDocument({
|
||||
collectionId: collection.id,
|
||||
});
|
||||
await document.archive();
|
||||
|
||||
const response = await documentMover({
|
||||
user,
|
||||
document,
|
||||
collectionId: collection.id,
|
||||
ip,
|
||||
});
|
||||
|
||||
expect(response.collections.length).toEqual(1);
|
||||
expect(response.documents.length).toEqual(1);
|
||||
});
|
||||
@@ -57,7 +55,6 @@ describe("documentMover", () => {
|
||||
text: "content",
|
||||
});
|
||||
await collection.addDocumentToStructure(newDocument);
|
||||
|
||||
const response = await documentMover({
|
||||
user,
|
||||
document,
|
||||
@@ -66,12 +63,13 @@ describe("documentMover", () => {
|
||||
index: 0,
|
||||
ip,
|
||||
});
|
||||
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'documentStructure' does not exist on typ... Remove this comment to see the full error message
|
||||
expect(response.collections[0].documentStructure[0].children[0].id).toBe(
|
||||
newDocument.id
|
||||
);
|
||||
expect(response.collections.length).toEqual(1);
|
||||
expect(response.documents.length).toEqual(1);
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'collection' does not exist on type 'neve... Remove this comment to see the full error message
|
||||
expect(response.documents[0].collection.id).toEqual(collection.id);
|
||||
});
|
||||
|
||||
@@ -89,7 +87,6 @@ describe("documentMover", () => {
|
||||
text: "content",
|
||||
});
|
||||
await collection.addDocumentToStructure(newDocument);
|
||||
|
||||
const response = await documentMover({
|
||||
user,
|
||||
document,
|
||||
@@ -98,23 +95,25 @@ describe("documentMover", () => {
|
||||
index: 0,
|
||||
ip,
|
||||
});
|
||||
|
||||
// check document ids where updated
|
||||
await newDocument.reload();
|
||||
expect(newDocument.collectionId).toBe(newCollection.id);
|
||||
|
||||
await document.reload();
|
||||
expect(document.collectionId).toBe(newCollection.id);
|
||||
|
||||
// check collection structure updated
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'never'.
|
||||
expect(response.collections[0].id).toBe(collection.id);
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'never'.
|
||||
expect(response.collections[1].id).toBe(newCollection.id);
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'documentStructure' does not exist on typ... Remove this comment to see the full error message
|
||||
expect(response.collections[1].documentStructure[0].children[0].id).toBe(
|
||||
newDocument.id
|
||||
);
|
||||
expect(response.collections.length).toEqual(2);
|
||||
expect(response.documents.length).toEqual(2);
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'collection' does not exist on type 'neve... Remove this comment to see the full error message
|
||||
expect(response.documents[0].collection.id).toEqual(newCollection.id);
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'collection' does not exist on type 'neve... Remove this comment to see the full error message
|
||||
expect(response.documents[1].collection.id).toEqual(newCollection.id);
|
||||
});
|
||||
|
||||
@@ -136,7 +135,6 @@ describe("documentMover", () => {
|
||||
text: `content `,
|
||||
});
|
||||
await collection.addDocumentToStructure(newDocument);
|
||||
|
||||
await documentMover({
|
||||
user,
|
||||
document,
|
||||
@@ -145,17 +143,14 @@ describe("documentMover", () => {
|
||||
index: 0,
|
||||
ip,
|
||||
});
|
||||
|
||||
// check document ids where updated
|
||||
await newDocument.reload();
|
||||
expect(newDocument.collectionId).toBe(newCollection.id);
|
||||
|
||||
// check new attachment was created pointint to same key
|
||||
const attachmentIds = parseAttachmentIds(newDocument.text);
|
||||
const newAttachment = await Attachment.findByPk(attachmentIds[0]);
|
||||
expect(newAttachment.documentId).toBe(newDocument.id);
|
||||
expect(newAttachment.key).toBe(attachment.key);
|
||||
|
||||
await document.reload();
|
||||
expect(document.collectionId).toBe(newCollection.id);
|
||||
});
|
||||
222
server/commands/documentMover.ts
Normal file
222
server/commands/documentMover.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
import { Document, Attachment, Collection, User, Event } from "@server/models";
|
||||
import parseAttachmentIds from "@server/utils/parseAttachmentIds";
|
||||
import { sequelize } from "../sequelize";
|
||||
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'options' implicitly has an 'any' type.
|
||||
async function copyAttachments(document: Document, options) {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'text' does not exist on type 'Document'.
|
||||
let text = document.text;
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'Document'.
|
||||
const documentId = document.id;
|
||||
// find any image attachments that are in this documents text
|
||||
const attachmentIds = parseAttachmentIds(text);
|
||||
|
||||
for (const id of attachmentIds) {
|
||||
const existing = await Attachment.findOne({
|
||||
where: {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'teamId' does not exist on type 'Document... Remove this comment to see the full error message
|
||||
teamId: document.teamId,
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
// if the image attachment was originally uploaded to another document
|
||||
// (this can happen in various ways, copy/paste, or duplicate for example)
|
||||
// then create a new attachment pointed to this doc and update the reference
|
||||
// in the text so that it gets the moved documents permissions
|
||||
if (existing && existing.documentId !== documentId) {
|
||||
const { id, ...rest } = existing.dataValues;
|
||||
const attachment = await Attachment.create(
|
||||
{ ...rest, documentId },
|
||||
options
|
||||
);
|
||||
text = text.replace(existing.redirectUrl, attachment.redirectUrl);
|
||||
}
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
export default async function documentMover({
|
||||
user,
|
||||
document,
|
||||
collectionId,
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type 'null' is not assignable to type 'string'.
|
||||
parentDocumentId = null,
|
||||
// convert undefined to null so parentId comparison treats them as equal
|
||||
index,
|
||||
ip,
|
||||
}: {
|
||||
// @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
|
||||
user: User;
|
||||
document: Document;
|
||||
collectionId: string;
|
||||
parentDocumentId?: string;
|
||||
index?: number;
|
||||
ip: string;
|
||||
}) {
|
||||
// @ts-expect-error ts-migrate(7034) FIXME: Variable 'transaction' implicitly has type 'any' i... Remove this comment to see the full error message
|
||||
let transaction;
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'collectionId' does not exist on type 'Do... Remove this comment to see the full error message
|
||||
const collectionChanged = collectionId !== document.collectionId;
|
||||
const result = {
|
||||
collections: [],
|
||||
documents: [],
|
||||
collectionChanged,
|
||||
};
|
||||
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'template' does not exist on type 'Docume... Remove this comment to see the full error message
|
||||
if (document.template) {
|
||||
if (!collectionChanged) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'collectionId' does not exist on type 'Do... Remove this comment to see the full error message
|
||||
document.collectionId = collectionId;
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'parentDocumentId' does not exist on type... Remove this comment to see the full error message
|
||||
document.parentDocumentId = null;
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'lastModifiedById' does not exist on type... Remove this comment to see the full error message
|
||||
document.lastModifiedById = user.id;
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'updatedBy' does not exist on type 'Docum... Remove this comment to see the full error message
|
||||
document.updatedBy = user;
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'save' does not exist on type 'Document'.
|
||||
await document.save();
|
||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'Document' is not assignable to p... Remove this comment to see the full error message
|
||||
result.documents.push(document);
|
||||
} else {
|
||||
try {
|
||||
transaction = await sequelize.transaction();
|
||||
// remove from original collection
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'collectionId' does not exist on type 'Do... Remove this comment to see the full error message
|
||||
const collection = await Collection.findByPk(document.collectionId, {
|
||||
transaction,
|
||||
paranoid: false,
|
||||
});
|
||||
const [
|
||||
documentJson,
|
||||
fromIndex,
|
||||
] = (await collection.removeDocumentInStructure(document, {
|
||||
save: false,
|
||||
})) || [undefined, index];
|
||||
// if we're reordering from within the same parent
|
||||
// the original and destination collection are the same,
|
||||
// so when the initial item is removed above, the list will reduce by 1.
|
||||
// We need to compensate for this when reordering
|
||||
const toIndex =
|
||||
index !== undefined &&
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'parentDocumentId' does not exist on type... Remove this comment to see the full error message
|
||||
document.parentDocumentId === parentDocumentId &&
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'collectionId' does not exist on type 'Do... Remove this comment to see the full error message
|
||||
document.collectionId === collectionId &&
|
||||
fromIndex < index
|
||||
? index - 1
|
||||
: index;
|
||||
|
||||
// if the collection is the same then it will get saved below, this
|
||||
// line prevents a pointless intermediate save from occurring.
|
||||
if (collectionChanged) {
|
||||
await collection.save({
|
||||
transaction,
|
||||
});
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'text' does not exist on type 'Document'.
|
||||
document.text = await copyAttachments(document, {
|
||||
transaction,
|
||||
});
|
||||
}
|
||||
|
||||
// add to new collection (may be the same)
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'collectionId' does not exist on type 'Do... Remove this comment to see the full error message
|
||||
document.collectionId = collectionId;
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'parentDocumentId' does not exist on type... Remove this comment to see the full error message
|
||||
document.parentDocumentId = parentDocumentId;
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'lastModifiedById' does not exist on type... Remove this comment to see the full error message
|
||||
document.lastModifiedById = user.id;
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'updatedBy' does not exist on type 'Docum... Remove this comment to see the full error message
|
||||
document.updatedBy = user;
|
||||
// @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
|
||||
const newCollection: Collection = collectionChanged
|
||||
? await Collection.scope({
|
||||
method: ["withMembership", user.id],
|
||||
}).findByPk(collectionId, {
|
||||
transaction,
|
||||
})
|
||||
: collection;
|
||||
await newCollection.addDocumentToStructure(document, toIndex, {
|
||||
documentJson,
|
||||
});
|
||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'any' is not assignable to parame... Remove this comment to see the full error message
|
||||
result.collections.push(collection);
|
||||
|
||||
// if collection does not remain the same loop through children and change their
|
||||
// collectionId and move any attachments they may have too. This includes
|
||||
// archived children, otherwise their collection would be wrong once restored.
|
||||
if (collectionChanged) {
|
||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'any' is not assignable to parame... Remove this comment to see the full error message
|
||||
result.collections.push(newCollection);
|
||||
|
||||
// @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) => {
|
||||
const childDocuments = await Document.findAll({
|
||||
where: {
|
||||
parentDocumentId: documentId,
|
||||
},
|
||||
});
|
||||
await Promise.all(
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'child' implicitly has an 'any' type.
|
||||
childDocuments.map(async (child) => {
|
||||
await loopChildren(child.id);
|
||||
child.text = await copyAttachments(child, {
|
||||
// @ts-expect-error ts-migrate(7005) FIXME: Variable 'transaction' implicitly has an 'any' typ... Remove this comment to see the full error message
|
||||
transaction,
|
||||
});
|
||||
child.collectionId = collectionId;
|
||||
await child.save();
|
||||
child.collection = newCollection;
|
||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'any' is not assignable to parame... Remove this comment to see the full error message
|
||||
result.documents.push(child);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'Document'.
|
||||
await loopChildren(document.id);
|
||||
}
|
||||
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'save' does not exist on type 'Document'.
|
||||
await document.save({
|
||||
transaction,
|
||||
});
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'collection' does not exist on type 'Docu... Remove this comment to see the full error message
|
||||
document.collection = newCollection;
|
||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'Document' is not assignable to p... Remove this comment to see the full error message
|
||||
result.documents.push(document);
|
||||
await transaction.commit();
|
||||
} catch (err) {
|
||||
if (transaction) {
|
||||
await transaction.rollback();
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
await Event.create({
|
||||
name: "documents.move",
|
||||
actorId: user.id,
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'Document'.
|
||||
documentId: document.id,
|
||||
collectionId,
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'teamId' does not exist on type 'Document... Remove this comment to see the full error message
|
||||
teamId: document.teamId,
|
||||
data: {
|
||||
title: document.title,
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'never'.
|
||||
collectionIds: result.collections.map((c) => c.id),
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'never'.
|
||||
documentIds: result.documents.map((d) => d.id),
|
||||
},
|
||||
ip,
|
||||
});
|
||||
// we need to send all updated models back to the client
|
||||
return result;
|
||||
}
|
||||
@@ -1,39 +1,41 @@
|
||||
// @flow
|
||||
import { subDays } from "date-fns";
|
||||
import { Attachment, Document } from "../models";
|
||||
import { buildAttachment, buildDocument } from "../test/factories";
|
||||
import { flushdb } from "../test/support";
|
||||
import { Attachment, Document } from "@server/models";
|
||||
import { buildAttachment, buildDocument } from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
import documentPermanentDeleter from "./documentPermanentDeleter";
|
||||
|
||||
jest.mock("aws-sdk", () => {
|
||||
const mS3 = { deleteObject: jest.fn().mockReturnThis(), promise: jest.fn() };
|
||||
const mS3 = {
|
||||
deleteObject: jest.fn().mockReturnThis(),
|
||||
promise: jest.fn(),
|
||||
};
|
||||
return {
|
||||
S3: jest.fn(() => mS3),
|
||||
Endpoint: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
|
||||
describe("documentPermanentDeleter", () => {
|
||||
it("should destroy documents", async () => {
|
||||
const document = await buildDocument({
|
||||
publishedAt: subDays(new Date(), 90),
|
||||
deletedAt: new Date(),
|
||||
});
|
||||
|
||||
const countDeletedDoc = await documentPermanentDeleter([document]);
|
||||
|
||||
expect(countDeletedDoc).toEqual(1);
|
||||
expect(await Document.unscoped().count({ paranoid: false })).toEqual(0);
|
||||
expect(
|
||||
await Document.unscoped().count({
|
||||
paranoid: false,
|
||||
})
|
||||
).toEqual(0);
|
||||
});
|
||||
|
||||
it("should error when trying to destroy undeleted documents", async () => {
|
||||
const document = await buildDocument({
|
||||
publishedAt: new Date(),
|
||||
});
|
||||
|
||||
let error;
|
||||
|
||||
try {
|
||||
await documentPermanentDeleter([document]);
|
||||
} catch (err) {
|
||||
@@ -50,20 +52,20 @@ describe("documentPermanentDeleter", () => {
|
||||
publishedAt: subDays(new Date(), 90),
|
||||
deletedAt: new Date(),
|
||||
});
|
||||
|
||||
const attachment = await buildAttachment({
|
||||
teamId: document.teamId,
|
||||
documentId: document.id,
|
||||
});
|
||||
|
||||
document.text = ``;
|
||||
await document.save();
|
||||
|
||||
const countDeletedDoc = await documentPermanentDeleter([document]);
|
||||
|
||||
expect(countDeletedDoc).toEqual(1);
|
||||
expect(await Attachment.count()).toEqual(0);
|
||||
expect(await Document.unscoped().count({ paranoid: false })).toEqual(0);
|
||||
expect(
|
||||
await Document.unscoped().count({
|
||||
paranoid: false,
|
||||
})
|
||||
).toEqual(0);
|
||||
});
|
||||
|
||||
it("should handle unknown attachment ids", async () => {
|
||||
@@ -71,53 +73,51 @@ describe("documentPermanentDeleter", () => {
|
||||
publishedAt: subDays(new Date(), 90),
|
||||
deletedAt: new Date(),
|
||||
});
|
||||
|
||||
const attachment = await buildAttachment({
|
||||
teamId: document.teamId,
|
||||
documentId: document.id,
|
||||
});
|
||||
|
||||
document.text = ``;
|
||||
await document.save();
|
||||
|
||||
// remove attachment so it no longer exists in the database, this is also
|
||||
// representative of a corrupt attachment id in the doc or the regex returning
|
||||
// an incorrect string
|
||||
await attachment.destroy({ force: true });
|
||||
|
||||
await attachment.destroy({
|
||||
force: true,
|
||||
});
|
||||
const countDeletedDoc = await documentPermanentDeleter([document]);
|
||||
|
||||
expect(countDeletedDoc).toEqual(1);
|
||||
expect(await Attachment.count()).toEqual(0);
|
||||
expect(await Document.unscoped().count({ paranoid: false })).toEqual(0);
|
||||
expect(
|
||||
await Document.unscoped().count({
|
||||
paranoid: false,
|
||||
})
|
||||
).toEqual(0);
|
||||
});
|
||||
|
||||
it("should not destroy attachments referenced in other documents", async () => {
|
||||
const document1 = await buildDocument();
|
||||
|
||||
const document = await buildDocument({
|
||||
teamId: document1.teamId,
|
||||
publishedAt: subDays(new Date(), 90),
|
||||
deletedAt: subDays(new Date(), 60),
|
||||
});
|
||||
|
||||
const attachment = await buildAttachment({
|
||||
teamId: document1.teamId,
|
||||
documentId: document.id,
|
||||
});
|
||||
|
||||
document1.text = ``;
|
||||
await document1.save();
|
||||
|
||||
document.text = ``;
|
||||
await document.save();
|
||||
|
||||
expect(await Attachment.count()).toEqual(1);
|
||||
|
||||
const countDeletedDoc = await documentPermanentDeleter([document]);
|
||||
|
||||
expect(countDeletedDoc).toEqual(1);
|
||||
expect(await Attachment.count()).toEqual(1);
|
||||
expect(await Document.unscoped().count({ paranoid: false })).toEqual(1);
|
||||
expect(
|
||||
await Document.unscoped().count({
|
||||
paranoid: false,
|
||||
})
|
||||
).toEqual(1);
|
||||
});
|
||||
});
|
||||
@@ -1,14 +1,15 @@
|
||||
// @flow
|
||||
import Logger from "../logging/logger";
|
||||
import { Document, Attachment } from "../models";
|
||||
import Logger from "@server/logging/logger";
|
||||
import { Document, Attachment } from "@server/models";
|
||||
import parseAttachmentIds from "@server/utils/parseAttachmentIds";
|
||||
import { sequelize } from "../sequelize";
|
||||
import parseAttachmentIds from "../utils/parseAttachmentIds";
|
||||
|
||||
export default async function documentPermanentDeleter(documents: Document[]) {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'deletedAt' does not exist on type 'Docum... Remove this comment to see the full error message
|
||||
const activeDocument = documents.find((doc) => !doc.deletedAt);
|
||||
|
||||
if (activeDocument) {
|
||||
throw new Error(
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'Document'.
|
||||
`Cannot permanently delete ${activeDocument.id} document. Please delete it and try again.`
|
||||
);
|
||||
}
|
||||
@@ -22,13 +23,16 @@ export default async function documentPermanentDeleter(documents: Document[]) {
|
||||
`;
|
||||
|
||||
for (const document of documents) {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'text' does not exist on type 'Document'.
|
||||
const attachmentIds = parseAttachmentIds(document.text);
|
||||
|
||||
for (const attachmentId of attachmentIds) {
|
||||
const [{ count }] = await sequelize.query(query, {
|
||||
type: sequelize.QueryTypes.SELECT,
|
||||
replacements: {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'Document'.
|
||||
documentId: document.id,
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'teamId' does not exist on type 'Document... Remove this comment to see the full error message
|
||||
teamId: document.teamId,
|
||||
query: attachmentId,
|
||||
},
|
||||
@@ -37,6 +41,7 @@ export default async function documentPermanentDeleter(documents: Document[]) {
|
||||
if (parseInt(count) === 0) {
|
||||
const attachment = await Attachment.findOne({
|
||||
where: {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'teamId' does not exist on type 'Document... Remove this comment to see the full error message
|
||||
teamId: document.teamId,
|
||||
id: attachmentId,
|
||||
},
|
||||
@@ -44,7 +49,6 @@ export default async function documentPermanentDeleter(documents: Document[]) {
|
||||
|
||||
if (attachment) {
|
||||
await attachment.destroy();
|
||||
|
||||
Logger.info("commands", `Attachment ${attachmentId} deleted`);
|
||||
} else {
|
||||
Logger.info("commands", `Unknown attachment ${attachmentId} ignored`);
|
||||
@@ -55,6 +59,7 @@ export default async function documentPermanentDeleter(documents: Document[]) {
|
||||
|
||||
return Document.scope("withUnpublished").destroy({
|
||||
where: {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'Document'.
|
||||
id: documents.map((document) => document.id),
|
||||
},
|
||||
force: true,
|
||||
@@ -1,25 +1,24 @@
|
||||
// @flow
|
||||
import { uniq } from "lodash";
|
||||
import { Node } from "prosemirror-model";
|
||||
import { schema, serializer } from "rich-markdown-editor";
|
||||
import { yDocToProsemirrorJSON } from "y-prosemirror";
|
||||
import * as Y from "yjs";
|
||||
import { Document, Event } from "../models";
|
||||
import { Document, Event } from "@server/models";
|
||||
|
||||
export default async function documentUpdater({
|
||||
documentId,
|
||||
ydoc,
|
||||
userId,
|
||||
}: {
|
||||
documentId: string,
|
||||
ydoc: Y.Doc,
|
||||
userId?: string,
|
||||
documentId: string;
|
||||
ydoc: Y.Doc;
|
||||
userId?: string;
|
||||
}) {
|
||||
const document = await Document.findByPk(documentId);
|
||||
const state = Y.encodeStateAsUpdate(ydoc);
|
||||
const node = Node.fromJSON(schema, yDocToProsemirrorJSON(ydoc, "default"));
|
||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
|
||||
const text = serializer.serialize(node);
|
||||
|
||||
const isUnchanged = document.text === text;
|
||||
const hasMultiplayerState = !!document.state;
|
||||
|
||||
@@ -32,7 +31,6 @@ export default async function documentUpdater({
|
||||
const pudIds = Array.from(pud.clients.values());
|
||||
const existingIds = document.collaboratorIds;
|
||||
const collaboratorIds = uniq([...pudIds, ...existingIds]);
|
||||
|
||||
await Document.scope("withUnpublished").update(
|
||||
{
|
||||
text,
|
||||
@@ -1,31 +0,0 @@
|
||||
// @flow
|
||||
import { FileOperation, Event, User } from "../models";
|
||||
import { sequelize } from "../sequelize";
|
||||
|
||||
export default async function fileOperationDeleter(
|
||||
fileOp: FileOperation,
|
||||
user: User,
|
||||
ip: string
|
||||
) {
|
||||
let transaction = await sequelize.transaction();
|
||||
|
||||
try {
|
||||
await fileOp.destroy({ transaction });
|
||||
|
||||
await Event.create(
|
||||
{
|
||||
name: "fileOperations.delete",
|
||||
teamId: user.teamId,
|
||||
actorId: user.id,
|
||||
data: fileOp.dataValues,
|
||||
ip,
|
||||
},
|
||||
{ transaction }
|
||||
);
|
||||
|
||||
await transaction.commit();
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,19 @@
|
||||
// @flow
|
||||
import { FileOperation } from "../models";
|
||||
import { buildAdmin, buildFileOperation } from "../test/factories";
|
||||
import { flushdb } from "../test/support";
|
||||
import { FileOperation } from "@server/models";
|
||||
import { buildAdmin, buildFileOperation } from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
import fileOperationDeleter from "./fileOperationDeleter";
|
||||
|
||||
jest.mock("aws-sdk", () => {
|
||||
const mS3 = { deleteObject: jest.fn().mockReturnThis(), promise: jest.fn() };
|
||||
const mS3 = {
|
||||
deleteObject: jest.fn().mockReturnThis(),
|
||||
promise: jest.fn(),
|
||||
};
|
||||
return {
|
||||
S3: jest.fn(() => mS3),
|
||||
Endpoint: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
|
||||
describe("fileOperationDeleter", () => {
|
||||
const ip = "127.0.0.1";
|
||||
|
||||
@@ -23,9 +23,7 @@ describe("fileOperationDeleter", () => {
|
||||
userId: admin.id,
|
||||
teamId: admin.teamId,
|
||||
});
|
||||
|
||||
await fileOperationDeleter(fileOp, admin, ip);
|
||||
|
||||
expect(await FileOperation.count()).toEqual(0);
|
||||
});
|
||||
});
|
||||
34
server/commands/fileOperationDeleter.ts
Normal file
34
server/commands/fileOperationDeleter.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { FileOperation, Event, User } from "@server/models";
|
||||
import { sequelize } from "../sequelize";
|
||||
|
||||
export default async function fileOperationDeleter(
|
||||
// @ts-expect-error ts-migrate(2749) FIXME: 'FileOperation' refers to a value, but is being us... Remove this comment to see the full error message
|
||||
fileOp: FileOperation,
|
||||
// @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
|
||||
user: User,
|
||||
ip: string
|
||||
) {
|
||||
const transaction = await sequelize.transaction();
|
||||
|
||||
try {
|
||||
await fileOp.destroy({
|
||||
transaction,
|
||||
});
|
||||
await Event.create(
|
||||
{
|
||||
name: "fileOperations.delete",
|
||||
teamId: user.teamId,
|
||||
actorId: user.id,
|
||||
data: fileOp.dataValues,
|
||||
ip,
|
||||
},
|
||||
{
|
||||
transaction,
|
||||
}
|
||||
);
|
||||
await transaction.commit();
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
// @flow
|
||||
import { Event } from "../models";
|
||||
import { buildDocument, buildUser } from "../test/factories";
|
||||
import { flushdb } from "../test/support";
|
||||
import { Event } from "@server/models";
|
||||
import { buildDocument, buildUser } from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
import revisionCreator from "./revisionCreator";
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
|
||||
describe("revisionCreator", () => {
|
||||
const ip = "127.0.0.1";
|
||||
|
||||
@@ -15,10 +13,12 @@ describe("revisionCreator", () => {
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
});
|
||||
|
||||
const revision = await revisionCreator({ document, user, ip });
|
||||
const revision = await revisionCreator({
|
||||
document,
|
||||
user,
|
||||
ip,
|
||||
});
|
||||
const event = await Event.findOne();
|
||||
|
||||
expect(revision.documentId).toEqual(document.id);
|
||||
expect(revision.userId).toEqual(user.id);
|
||||
expect(event.name).toEqual("revisions.create");
|
||||
@@ -1,5 +1,4 @@
|
||||
// @flow
|
||||
import { Document, User, Event, Revision } from "../models";
|
||||
import { Document, User, Event, Revision } from "@server/models";
|
||||
import { sequelize } from "../sequelize";
|
||||
|
||||
export default async function revisionCreator({
|
||||
@@ -7,38 +6,42 @@ export default async function revisionCreator({
|
||||
user,
|
||||
ip,
|
||||
}: {
|
||||
document: Document,
|
||||
user: User,
|
||||
ip?: string,
|
||||
document: Document;
|
||||
// @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
|
||||
user: User;
|
||||
ip?: string;
|
||||
}) {
|
||||
let transaction;
|
||||
|
||||
try {
|
||||
transaction = await sequelize.transaction();
|
||||
|
||||
const revision = await Revision.createFromDocument(document, {
|
||||
transaction,
|
||||
});
|
||||
|
||||
await Event.create(
|
||||
{
|
||||
name: "revisions.create",
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'Document'.
|
||||
documentId: document.id,
|
||||
modelId: revision.id,
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'teamId' does not exist on type 'Document... Remove this comment to see the full error message
|
||||
teamId: document.teamId,
|
||||
actorId: user.id,
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'updatedAt' does not exist on type 'Docum... Remove this comment to see the full error message
|
||||
createdAt: document.updatedAt,
|
||||
ip: ip || user.lastActiveIp,
|
||||
},
|
||||
{ transaction }
|
||||
{
|
||||
transaction,
|
||||
}
|
||||
);
|
||||
await transaction.commit();
|
||||
|
||||
return revision;
|
||||
} catch (err) {
|
||||
if (transaction) {
|
||||
await transaction.rollback();
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
// @flow
|
||||
import { buildTeam } from "../test/factories";
|
||||
import { flushdb } from "../test/support";
|
||||
import { buildTeam } from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
import teamCreator from "./teamCreator";
|
||||
|
||||
jest.mock("aws-sdk", () => {
|
||||
const mS3 = { putObject: jest.fn().mockReturnThis(), promise: jest.fn() };
|
||||
const mS3 = {
|
||||
putObject: jest.fn().mockReturnThis(),
|
||||
promise: jest.fn(),
|
||||
};
|
||||
return {
|
||||
S3: jest.fn(() => mS3),
|
||||
Endpoint: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
|
||||
describe("teamCreator", () => {
|
||||
it("should create team and authentication provider", async () => {
|
||||
const result = await teamCreator({
|
||||
@@ -24,9 +24,7 @@ describe("teamCreator", () => {
|
||||
providerId: "example.com",
|
||||
},
|
||||
});
|
||||
|
||||
const { team, authenticationProvider, isNewTeam } = result;
|
||||
|
||||
expect(authenticationProvider.name).toEqual("google");
|
||||
expect(authenticationProvider.providerId).toEqual("example.com");
|
||||
expect(team.name).toEqual("Test team");
|
||||
@@ -59,7 +57,6 @@ describe("teamCreator", () => {
|
||||
it("should return existing team when within allowed domains", async () => {
|
||||
delete process.env.DEPLOYMENT;
|
||||
const existing = await buildTeam();
|
||||
|
||||
const result = await teamCreator({
|
||||
name: "Updated name",
|
||||
subdomain: "example",
|
||||
@@ -69,40 +66,32 @@ describe("teamCreator", () => {
|
||||
providerId: "allowed-domain.com",
|
||||
},
|
||||
});
|
||||
|
||||
const { team, authenticationProvider, isNewTeam } = result;
|
||||
|
||||
expect(team.id).toEqual(existing.id);
|
||||
expect(team.name).toEqual(existing.name);
|
||||
expect(authenticationProvider.name).toEqual("google");
|
||||
expect(authenticationProvider.providerId).toEqual("allowed-domain.com");
|
||||
expect(isNewTeam).toEqual(false);
|
||||
|
||||
const providers = await team.getAuthenticationProviders();
|
||||
expect(providers.length).toEqual(2);
|
||||
});
|
||||
|
||||
it("should return exising team", async () => {
|
||||
delete process.env.DEPLOYMENT;
|
||||
|
||||
const authenticationProvider = {
|
||||
name: "google",
|
||||
providerId: "example.com",
|
||||
};
|
||||
|
||||
const existing = await buildTeam({
|
||||
subdomain: "example",
|
||||
authenticationProviders: [authenticationProvider],
|
||||
});
|
||||
|
||||
const result = await teamCreator({
|
||||
name: "Updated name",
|
||||
subdomain: "example",
|
||||
authenticationProvider,
|
||||
});
|
||||
|
||||
const { team, isNewTeam } = result;
|
||||
|
||||
expect(team.id).toEqual(existing.id);
|
||||
expect(team.name).toEqual(existing.name);
|
||||
expect(team.subdomain).toEqual("example");
|
||||
@@ -1,16 +1,17 @@
|
||||
// @flow
|
||||
import Logger from "@server/logging/logger";
|
||||
import { Team, AuthenticationProvider } from "@server/models";
|
||||
import { getAllowedDomains } from "@server/utils/authentication";
|
||||
import { generateAvatarUrl } from "@server/utils/avatars";
|
||||
import { MaximumTeamsError } from "../errors";
|
||||
import Logger from "../logging/logger";
|
||||
import { Team, AuthenticationProvider } from "../models";
|
||||
import { sequelize } from "../sequelize";
|
||||
import { getAllowedDomains } from "../utils/authentication";
|
||||
import { generateAvatarUrl } from "../utils/avatars";
|
||||
|
||||
type TeamCreatorResult = {|
|
||||
team: Team,
|
||||
authenticationProvider: AuthenticationProvider,
|
||||
isNewTeam: boolean,
|
||||
|};
|
||||
type TeamCreatorResult = {
|
||||
// @ts-expect-error ts-migrate(2749) FIXME: 'Team' refers to a value, but is being used as a t... Remove this comment to see the full error message
|
||||
team: Team;
|
||||
// @ts-expect-error ts-migrate(2749) FIXME: 'AuthenticationProvider' refers to a value, but is... Remove this comment to see the full error message
|
||||
authenticationProvider: AuthenticationProvider;
|
||||
isNewTeam: boolean;
|
||||
};
|
||||
|
||||
export default async function teamCreator({
|
||||
name,
|
||||
@@ -18,16 +19,16 @@ export default async function teamCreator({
|
||||
subdomain,
|
||||
avatarUrl,
|
||||
authenticationProvider,
|
||||
}: {|
|
||||
name: string,
|
||||
domain?: string,
|
||||
subdomain: string,
|
||||
avatarUrl?: string,
|
||||
authenticationProvider: {|
|
||||
name: string,
|
||||
providerId: string,
|
||||
|},
|
||||
|}): Promise<TeamCreatorResult> {
|
||||
}: {
|
||||
name: string;
|
||||
domain?: string;
|
||||
subdomain: string;
|
||||
avatarUrl?: string;
|
||||
authenticationProvider: {
|
||||
name: string;
|
||||
providerId: string;
|
||||
};
|
||||
}): Promise<TeamCreatorResult> {
|
||||
let authP = await AuthenticationProvider.findOne({
|
||||
where: authenticationProvider,
|
||||
include: [
|
||||
@@ -61,7 +62,6 @@ export default async function teamCreator({
|
||||
if (teamCount === 1 && domain && getAllowedDomains().includes(domain)) {
|
||||
const team = await Team.findOne();
|
||||
authP = await team.createAuthenticationProvider(authenticationProvider);
|
||||
|
||||
return {
|
||||
authenticationProvider: authP,
|
||||
team,
|
||||
@@ -70,7 +70,7 @@ export default async function teamCreator({
|
||||
}
|
||||
|
||||
if (teamCount >= 1) {
|
||||
throw new MaximumTeamsError();
|
||||
throw MaximumTeamsError();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ export default async function teamCreator({
|
||||
});
|
||||
}
|
||||
|
||||
let transaction = await sequelize.transaction();
|
||||
const transaction = await sequelize.transaction();
|
||||
let team;
|
||||
|
||||
try {
|
||||
@@ -99,7 +99,6 @@ export default async function teamCreator({
|
||||
transaction,
|
||||
}
|
||||
);
|
||||
|
||||
await transaction.commit();
|
||||
} catch (err) {
|
||||
await transaction.rollback();
|
||||
@@ -1,25 +1,25 @@
|
||||
// @flow
|
||||
import { subDays } from "date-fns";
|
||||
import { Attachment, User, Document, Collection, Team } from "../models";
|
||||
import { Attachment, User, Document, Collection, Team } from "@server/models";
|
||||
import {
|
||||
buildAttachment,
|
||||
buildUser,
|
||||
buildTeam,
|
||||
buildDocument,
|
||||
} from "../test/factories";
|
||||
import { flushdb } from "../test/support";
|
||||
} from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
import teamPermanentDeleter from "./teamPermanentDeleter";
|
||||
|
||||
jest.mock("aws-sdk", () => {
|
||||
const mS3 = { deleteObject: jest.fn().mockReturnThis(), promise: jest.fn() };
|
||||
const mS3 = {
|
||||
deleteObject: jest.fn().mockReturnThis(),
|
||||
promise: jest.fn(),
|
||||
};
|
||||
return {
|
||||
S3: jest.fn(() => mS3),
|
||||
Endpoint: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
|
||||
describe("teamPermanentDeleter", () => {
|
||||
it("should destroy related data", async () => {
|
||||
const team = await buildTeam({
|
||||
@@ -32,30 +32,42 @@ describe("teamPermanentDeleter", () => {
|
||||
teamId: team.id,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
await teamPermanentDeleter(team);
|
||||
|
||||
expect(await Team.count()).toEqual(0);
|
||||
expect(await User.count()).toEqual(0);
|
||||
expect(await Document.unscoped().count({ paranoid: false })).toEqual(0);
|
||||
expect(await Collection.unscoped().count({ paranoid: false })).toEqual(0);
|
||||
expect(
|
||||
await Document.unscoped().count({
|
||||
paranoid: false,
|
||||
})
|
||||
).toEqual(0);
|
||||
expect(
|
||||
await Collection.unscoped().count({
|
||||
paranoid: false,
|
||||
})
|
||||
).toEqual(0);
|
||||
});
|
||||
|
||||
it("should not destroy unrelated data", async () => {
|
||||
const team = await buildTeam({
|
||||
deletedAt: subDays(new Date(), 90),
|
||||
});
|
||||
|
||||
await buildUser();
|
||||
await buildTeam();
|
||||
await buildDocument();
|
||||
|
||||
await teamPermanentDeleter(team);
|
||||
|
||||
expect(await Team.count()).toEqual(4); // each build command creates a team
|
||||
|
||||
expect(await User.count()).toEqual(2);
|
||||
expect(await Document.unscoped().count({ paranoid: false })).toEqual(1);
|
||||
expect(await Collection.unscoped().count({ paranoid: false })).toEqual(1);
|
||||
expect(
|
||||
await Document.unscoped().count({
|
||||
paranoid: false,
|
||||
})
|
||||
).toEqual(1);
|
||||
expect(
|
||||
await Collection.unscoped().count({
|
||||
paranoid: false,
|
||||
})
|
||||
).toEqual(1);
|
||||
});
|
||||
|
||||
it("should destroy attachments", async () => {
|
||||
@@ -73,20 +85,26 @@ describe("teamPermanentDeleter", () => {
|
||||
teamId: document.teamId,
|
||||
documentId: document.id,
|
||||
});
|
||||
|
||||
await teamPermanentDeleter(team);
|
||||
|
||||
expect(await Team.count()).toEqual(0);
|
||||
expect(await User.count()).toEqual(0);
|
||||
expect(await Attachment.count()).toEqual(0);
|
||||
expect(await Document.unscoped().count({ paranoid: false })).toEqual(0);
|
||||
expect(await Collection.unscoped().count({ paranoid: false })).toEqual(0);
|
||||
expect(
|
||||
await Document.unscoped().count({
|
||||
paranoid: false,
|
||||
})
|
||||
).toEqual(0);
|
||||
expect(
|
||||
await Collection.unscoped().count({
|
||||
paranoid: false,
|
||||
})
|
||||
).toEqual(0);
|
||||
});
|
||||
|
||||
it("should error when trying to destroy undeleted team", async () => {
|
||||
const team = await buildTeam();
|
||||
|
||||
let error;
|
||||
|
||||
try {
|
||||
await teamPermanentDeleter(team);
|
||||
} catch (err) {
|
||||
@@ -1,5 +1,4 @@
|
||||
// @flow
|
||||
import Logger from "../logging/logger";
|
||||
import Logger from "@server/logging/logger";
|
||||
import {
|
||||
ApiKey,
|
||||
Attachment,
|
||||
@@ -17,9 +16,10 @@ import {
|
||||
IntegrationAuthentication,
|
||||
SearchQuery,
|
||||
Share,
|
||||
} from "../models";
|
||||
} from "@server/models";
|
||||
import { sequelize } from "../sequelize";
|
||||
|
||||
// @ts-expect-error ts-migrate(2749) FIXME: 'Team' refers to a value, but is being used as a t... Remove this comment to see the full error message
|
||||
export default async function teamPermanentDeleter(team: Team) {
|
||||
if (!team.deletedAt) {
|
||||
throw new Error(
|
||||
@@ -31,13 +31,12 @@ export default async function teamPermanentDeleter(team: Team) {
|
||||
"commands",
|
||||
`Permanently deleting team ${team.name} (${team.id})`
|
||||
);
|
||||
|
||||
const teamId = team.id;
|
||||
// @ts-expect-error ts-migrate(7034) FIXME: Variable 'transaction' implicitly has type 'any' i... Remove this comment to see the full error message
|
||||
let transaction;
|
||||
|
||||
try {
|
||||
transaction = await sequelize.transaction();
|
||||
|
||||
await Attachment.findAllInBatches(
|
||||
{
|
||||
where: {
|
||||
@@ -46,6 +45,7 @@ export default async function teamPermanentDeleter(team: Team) {
|
||||
limit: 100,
|
||||
offset: 0,
|
||||
},
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'attachments' implicitly has an 'any' ty... Remove this comment to see the full error message
|
||||
async (attachments, options) => {
|
||||
Logger.info(
|
||||
"commands",
|
||||
@@ -53,13 +53,17 @@ export default async function teamPermanentDeleter(team: Team) {
|
||||
options.offset + options.limit
|
||||
}…`
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
attachments.map((attachment) => attachment.destroy({ transaction }))
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'attachment' implicitly has an 'any' typ... Remove this comment to see the full error message
|
||||
attachments.map((attachment) =>
|
||||
attachment.destroy({
|
||||
// @ts-expect-error ts-migrate(7005) FIXME: Variable 'transaction' implicitly has an 'any' typ... Remove this comment to see the full error message
|
||||
transaction,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// Destroy user-relation models
|
||||
await User.findAllInBatches(
|
||||
{
|
||||
@@ -70,115 +74,133 @@ export default async function teamPermanentDeleter(team: Team) {
|
||||
limit: 100,
|
||||
offset: 0,
|
||||
},
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'users' implicitly has an 'any' type.
|
||||
async (users) => {
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'user' implicitly has an 'any' type.
|
||||
const userIds = users.map((user) => user.id);
|
||||
|
||||
await UserAuthentication.destroy({
|
||||
where: { userId: userIds },
|
||||
where: {
|
||||
userId: userIds,
|
||||
},
|
||||
force: true,
|
||||
// @ts-expect-error ts-migrate(7005) FIXME: Variable 'transaction' implicitly has an 'any' typ... Remove this comment to see the full error message
|
||||
transaction,
|
||||
});
|
||||
|
||||
await ApiKey.destroy({
|
||||
where: { userId: userIds },
|
||||
where: {
|
||||
userId: userIds,
|
||||
},
|
||||
force: true,
|
||||
// @ts-expect-error ts-migrate(7005) FIXME: Variable 'transaction' implicitly has an 'any' typ... Remove this comment to see the full error message
|
||||
transaction,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// Destory team-relation models
|
||||
await AuthenticationProvider.destroy({
|
||||
where: { teamId },
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
force: true,
|
||||
transaction,
|
||||
});
|
||||
|
||||
// events must be first due to db constraints
|
||||
await Event.destroy({
|
||||
where: { teamId },
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
force: true,
|
||||
transaction,
|
||||
});
|
||||
|
||||
await Collection.destroy({
|
||||
where: { teamId },
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
force: true,
|
||||
transaction,
|
||||
});
|
||||
|
||||
await Document.unscoped().destroy({
|
||||
where: { teamId },
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
force: true,
|
||||
transaction,
|
||||
});
|
||||
|
||||
await FileOperation.destroy({
|
||||
where: { teamId },
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
force: true,
|
||||
transaction,
|
||||
});
|
||||
|
||||
await Group.unscoped().destroy({
|
||||
where: { teamId },
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
force: true,
|
||||
transaction,
|
||||
});
|
||||
|
||||
await Integration.destroy({
|
||||
where: { teamId },
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
force: true,
|
||||
transaction,
|
||||
});
|
||||
|
||||
await IntegrationAuthentication.destroy({
|
||||
where: { teamId },
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
force: true,
|
||||
transaction,
|
||||
});
|
||||
|
||||
await NotificationSetting.destroy({
|
||||
where: { teamId },
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
force: true,
|
||||
transaction,
|
||||
});
|
||||
|
||||
await SearchQuery.destroy({
|
||||
where: { teamId },
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
force: true,
|
||||
transaction,
|
||||
});
|
||||
|
||||
await Share.destroy({
|
||||
where: { teamId },
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
force: true,
|
||||
transaction,
|
||||
});
|
||||
|
||||
await User.destroy({
|
||||
where: { teamId },
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
force: true,
|
||||
transaction,
|
||||
});
|
||||
|
||||
await team.destroy({
|
||||
force: true,
|
||||
transaction,
|
||||
});
|
||||
|
||||
await Event.create(
|
||||
{
|
||||
name: "teams.destroy",
|
||||
modelId: teamId,
|
||||
},
|
||||
{ transaction }
|
||||
{
|
||||
transaction,
|
||||
}
|
||||
);
|
||||
|
||||
await transaction.commit();
|
||||
} catch (err) {
|
||||
if (transaction) {
|
||||
await transaction.rollback();
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
// @flow
|
||||
import { buildUser, buildTeam, buildInvite } from "../test/factories";
|
||||
import { flushdb } from "../test/support";
|
||||
import { buildUser, buildTeam, buildInvite } from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
import userCreator from "./userCreator";
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
|
||||
describe("userCreator", () => {
|
||||
const ip = "127.0.0.1";
|
||||
|
||||
@@ -14,7 +12,6 @@ describe("userCreator", () => {
|
||||
const existingAuth = authentications[0];
|
||||
const newEmail = "test@example.com";
|
||||
const newUsername = "tname";
|
||||
|
||||
const result = await userCreator({
|
||||
name: existing.name,
|
||||
email: newEmail,
|
||||
@@ -29,9 +26,7 @@ describe("userCreator", () => {
|
||||
scopes: ["read"],
|
||||
},
|
||||
});
|
||||
|
||||
const { user, authentication, isNewUser } = result;
|
||||
|
||||
expect(authentication.accessToken).toEqual("123");
|
||||
expect(authentication.scopes.length).toEqual(1);
|
||||
expect(authentication.scopes[0]).toEqual("read");
|
||||
@@ -45,9 +40,7 @@ describe("userCreator", () => {
|
||||
const authentications = await existing.getAuthentications();
|
||||
const existingAuth = authentications[0];
|
||||
const newEmail = "test@example.com";
|
||||
|
||||
await existing.destroy();
|
||||
|
||||
const result = await userCreator({
|
||||
name: "Test Name",
|
||||
email: "test@example.com",
|
||||
@@ -60,9 +53,7 @@ describe("userCreator", () => {
|
||||
scopes: ["read"],
|
||||
},
|
||||
});
|
||||
|
||||
const { user, authentication, isNewUser } = result;
|
||||
|
||||
expect(authentication.accessToken).toEqual("123");
|
||||
expect(authentication.scopes.length).toEqual(1);
|
||||
expect(authentication.scopes[0]).toEqual("read");
|
||||
@@ -100,7 +91,6 @@ describe("userCreator", () => {
|
||||
const team = await buildTeam();
|
||||
const authenticationProviders = await team.getAuthenticationProviders();
|
||||
const authenticationProvider = authenticationProviders[0];
|
||||
|
||||
const result = await userCreator({
|
||||
name: "Test Name",
|
||||
email: "test@example.com",
|
||||
@@ -114,9 +104,7 @@ describe("userCreator", () => {
|
||||
scopes: ["read"],
|
||||
},
|
||||
});
|
||||
|
||||
const { user, authentication, isNewUser } = result;
|
||||
|
||||
expect(authentication.accessToken).toEqual("123");
|
||||
expect(authentication.scopes.length).toEqual(1);
|
||||
expect(authentication.scopes[0]).toEqual("read");
|
||||
@@ -128,10 +116,11 @@ describe("userCreator", () => {
|
||||
});
|
||||
|
||||
it("should prefer isAdmin argument over defaultUserRole", async () => {
|
||||
const team = await buildTeam({ defaultUserRole: "viewer" });
|
||||
const team = await buildTeam({
|
||||
defaultUserRole: "viewer",
|
||||
});
|
||||
const authenticationProviders = await team.getAuthenticationProviders();
|
||||
const authenticationProvider = authenticationProviders[0];
|
||||
|
||||
const result = await userCreator({
|
||||
name: "Test Name",
|
||||
email: "test@example.com",
|
||||
@@ -146,17 +135,16 @@ describe("userCreator", () => {
|
||||
scopes: ["read"],
|
||||
},
|
||||
});
|
||||
|
||||
const { user } = result;
|
||||
|
||||
expect(user.isAdmin).toEqual(true);
|
||||
});
|
||||
|
||||
it("should prefer defaultUserRole when isAdmin is undefined or false", async () => {
|
||||
const team = await buildTeam({ defaultUserRole: "viewer" });
|
||||
const team = await buildTeam({
|
||||
defaultUserRole: "viewer",
|
||||
});
|
||||
const authenticationProviders = await team.getAuthenticationProviders();
|
||||
const authenticationProvider = authenticationProviders[0];
|
||||
|
||||
const result = await userCreator({
|
||||
name: "Test Name",
|
||||
email: "test@example.com",
|
||||
@@ -170,13 +158,10 @@ describe("userCreator", () => {
|
||||
scopes: ["read"],
|
||||
},
|
||||
});
|
||||
|
||||
const { user: tname } = result;
|
||||
|
||||
expect(tname.username).toEqual("tname");
|
||||
expect(tname.isAdmin).toEqual(false);
|
||||
expect(tname.isViewer).toEqual(true);
|
||||
|
||||
const tname2Result = await userCreator({
|
||||
name: "Test2 Name",
|
||||
email: "tes2@example.com",
|
||||
@@ -191,9 +176,7 @@ describe("userCreator", () => {
|
||||
scopes: ["read"],
|
||||
},
|
||||
});
|
||||
|
||||
const { user: tname2 } = tname2Result;
|
||||
|
||||
expect(tname2.username).toEqual("tname2");
|
||||
expect(tname2.isAdmin).toEqual(false);
|
||||
expect(tname2.isViewer).toEqual(true);
|
||||
@@ -201,10 +184,11 @@ describe("userCreator", () => {
|
||||
|
||||
it("should create a user from an invited user", async () => {
|
||||
const team = await buildTeam();
|
||||
const invite = await buildInvite({ teamId: team.id });
|
||||
const invite = await buildInvite({
|
||||
teamId: team.id,
|
||||
});
|
||||
const authenticationProviders = await team.getAuthenticationProviders();
|
||||
const authenticationProvider = authenticationProviders[0];
|
||||
|
||||
const result = await userCreator({
|
||||
name: invite.name,
|
||||
email: invite.email,
|
||||
@@ -217,9 +201,7 @@ describe("userCreator", () => {
|
||||
scopes: ["read"],
|
||||
},
|
||||
});
|
||||
|
||||
const { user, authentication, isNewUser } = result;
|
||||
|
||||
expect(authentication.accessToken).toEqual("123");
|
||||
expect(authentication.scopes.length).toEqual(1);
|
||||
expect(authentication.scopes[0]).toEqual("read");
|
||||
@@ -1,15 +1,15 @@
|
||||
// @flow
|
||||
import Sequelize from "sequelize";
|
||||
import { Event, Team, User, UserAuthentication } from "../models";
|
||||
import { Event, Team, User, UserAuthentication } from "@server/models";
|
||||
import { sequelize } from "../sequelize";
|
||||
|
||||
const Op = Sequelize.Op;
|
||||
|
||||
type UserCreatorResult = {|
|
||||
user: User,
|
||||
isNewUser: boolean,
|
||||
authentication: UserAuthentication,
|
||||
|};
|
||||
type UserCreatorResult = {
|
||||
// @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
|
||||
user: User;
|
||||
isNewUser: boolean;
|
||||
// @ts-expect-error ts-migrate(2749) FIXME: 'UserAuthentication' refers to a value, but is bei... Remove this comment to see the full error message
|
||||
authentication: UserAuthentication;
|
||||
};
|
||||
|
||||
export default async function userCreator({
|
||||
name,
|
||||
@@ -20,22 +20,22 @@ export default async function userCreator({
|
||||
teamId,
|
||||
authentication,
|
||||
ip,
|
||||
}: {|
|
||||
name: string,
|
||||
email: string,
|
||||
username?: string,
|
||||
isAdmin?: boolean,
|
||||
avatarUrl?: string,
|
||||
teamId: string,
|
||||
ip: string,
|
||||
authentication: {|
|
||||
authenticationProviderId: string,
|
||||
providerId: string,
|
||||
scopes: string[],
|
||||
accessToken?: string,
|
||||
refreshToken?: string,
|
||||
|},
|
||||
|}): Promise<UserCreatorResult> {
|
||||
}: {
|
||||
name: string;
|
||||
email: string;
|
||||
username?: string;
|
||||
isAdmin?: boolean;
|
||||
avatarUrl?: string;
|
||||
teamId: string;
|
||||
ip: string;
|
||||
authentication: {
|
||||
authenticationProviderId: string;
|
||||
providerId: string;
|
||||
scopes: string[];
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
};
|
||||
}): Promise<UserCreatorResult> {
|
||||
const { authenticationProviderId, providerId, ...rest } = authentication;
|
||||
const auth = await UserAuthentication.findOne({
|
||||
where: {
|
||||
@@ -65,10 +65,16 @@ export default async function userCreator({
|
||||
}
|
||||
|
||||
if (user) {
|
||||
await user.update({ email, username });
|
||||
await user.update({
|
||||
email,
|
||||
username,
|
||||
});
|
||||
await auth.update(rest);
|
||||
|
||||
return { user, authentication: auth, isNewUser: false };
|
||||
return {
|
||||
user,
|
||||
authentication: auth,
|
||||
isNewUser: false,
|
||||
};
|
||||
}
|
||||
|
||||
// We found an authentication record, but the associated user was deleted or
|
||||
@@ -100,15 +106,18 @@ export default async function userCreator({
|
||||
// We have an existing invite for his user, so we need to update it with our
|
||||
// new details and link up the authentication method
|
||||
if (invite && !invite.authentications.length) {
|
||||
let transaction = await sequelize.transaction();
|
||||
const transaction = await sequelize.transaction();
|
||||
let auth;
|
||||
|
||||
try {
|
||||
await invite.update(
|
||||
{
|
||||
name,
|
||||
avatarUrl,
|
||||
},
|
||||
{ transaction }
|
||||
{
|
||||
transaction,
|
||||
}
|
||||
);
|
||||
auth = await invite.createAuthentication(authentication, {
|
||||
transaction,
|
||||
@@ -119,18 +128,21 @@ export default async function userCreator({
|
||||
throw err;
|
||||
}
|
||||
|
||||
return { user: invite, authentication: auth, isNewUser: true };
|
||||
return {
|
||||
user: invite,
|
||||
authentication: auth,
|
||||
isNewUser: true,
|
||||
};
|
||||
}
|
||||
|
||||
// No auth, no user – this is an entirely new sign in.
|
||||
let transaction = await sequelize.transaction();
|
||||
const transaction = await sequelize.transaction();
|
||||
|
||||
try {
|
||||
const { defaultUserRole } = await Team.findByPk(teamId, {
|
||||
attributes: ["defaultUserRole"],
|
||||
transaction,
|
||||
});
|
||||
|
||||
const user = await User.create(
|
||||
{
|
||||
name,
|
||||
@@ -1,16 +1,13 @@
|
||||
// @flow
|
||||
import { buildUser, buildAdmin } from "../test/factories";
|
||||
import { flushdb } from "../test/support";
|
||||
import { buildUser, buildAdmin } from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
import userDestroyer from "./userDestroyer";
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
|
||||
describe("userDestroyer", () => {
|
||||
const ip = "127.0.0.1";
|
||||
|
||||
it("should prevent last user from deleting account", async () => {
|
||||
const user = await buildUser();
|
||||
|
||||
let error;
|
||||
|
||||
try {
|
||||
@@ -22,13 +19,15 @@ describe("userDestroyer", () => {
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
expect(error && error.message).toContain("Cannot delete last user");
|
||||
});
|
||||
|
||||
it("should prevent last admin from deleting account", async () => {
|
||||
const user = await buildAdmin();
|
||||
await buildUser({ teamId: user.teamId });
|
||||
|
||||
await buildUser({
|
||||
teamId: user.teamId,
|
||||
});
|
||||
let error;
|
||||
|
||||
try {
|
||||
@@ -40,13 +39,15 @@ describe("userDestroyer", () => {
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
expect(error && error.message).toContain("Cannot delete account");
|
||||
});
|
||||
|
||||
it("should not prevent multiple admin from deleting account", async () => {
|
||||
const actor = await buildAdmin();
|
||||
const user = await buildAdmin({ teamId: actor.teamId });
|
||||
|
||||
const user = await buildAdmin({
|
||||
teamId: actor.teamId,
|
||||
});
|
||||
let error;
|
||||
|
||||
try {
|
||||
@@ -58,14 +59,16 @@ describe("userDestroyer", () => {
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
expect(error).toBeFalsy();
|
||||
expect(user.deletedAt).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should not prevent last non-admin from deleting account", async () => {
|
||||
const user = await buildUser();
|
||||
await buildUser({ teamId: user.teamId });
|
||||
|
||||
await buildUser({
|
||||
teamId: user.teamId,
|
||||
});
|
||||
let error;
|
||||
|
||||
try {
|
||||
@@ -77,6 +80,7 @@ describe("userDestroyer", () => {
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
expect(error).toBeFalsy();
|
||||
expect(user.deletedAt).toBeTruthy();
|
||||
});
|
||||
@@ -1,6 +1,5 @@
|
||||
// @flow
|
||||
import { Event, User } from "@server/models";
|
||||
import { ValidationError } from "../errors";
|
||||
import { Event, User } from "../models";
|
||||
import { Op, sequelize } from "../sequelize";
|
||||
|
||||
export default async function userDestroyer({
|
||||
@@ -8,12 +7,13 @@ export default async function userDestroyer({
|
||||
actor,
|
||||
ip,
|
||||
}: {
|
||||
user: User,
|
||||
actor: User,
|
||||
ip: string,
|
||||
// @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
|
||||
user: User;
|
||||
// @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
|
||||
actor: User;
|
||||
ip: string;
|
||||
}) {
|
||||
const { teamId } = user;
|
||||
|
||||
const usersCount = await User.count({
|
||||
where: {
|
||||
teamId,
|
||||
@@ -21,7 +21,7 @@ export default async function userDestroyer({
|
||||
});
|
||||
|
||||
if (usersCount === 1) {
|
||||
throw new ValidationError("Cannot delete last user on the team.");
|
||||
throw ValidationError("Cannot delete last user on the team.");
|
||||
}
|
||||
|
||||
if (user.isAdmin) {
|
||||
@@ -29,34 +29,41 @@ export default async function userDestroyer({
|
||||
where: {
|
||||
isAdmin: true,
|
||||
teamId,
|
||||
id: { [Op.ne]: user.id },
|
||||
id: {
|
||||
[Op.ne]: user.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (otherAdminsCount === 0) {
|
||||
throw new ValidationError(
|
||||
throw ValidationError(
|
||||
"Cannot delete account as only admin. Please make another user admin and try again."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let transaction = await sequelize.transaction();
|
||||
const transaction = await sequelize.transaction();
|
||||
let response;
|
||||
|
||||
try {
|
||||
response = await user.destroy({ transaction });
|
||||
response = await user.destroy({
|
||||
transaction,
|
||||
});
|
||||
await Event.create(
|
||||
{
|
||||
name: "users.delete",
|
||||
actorId: actor.id,
|
||||
userId: user.id,
|
||||
teamId,
|
||||
data: { name: user.name },
|
||||
data: {
|
||||
name: user.name,
|
||||
},
|
||||
ip,
|
||||
},
|
||||
{ transaction }
|
||||
{
|
||||
transaction,
|
||||
}
|
||||
);
|
||||
|
||||
await transaction.commit();
|
||||
} catch (err) {
|
||||
await transaction.rollback();
|
||||
@@ -1,63 +0,0 @@
|
||||
/* eslint-disable flowtype/require-valid-file-annotation */
|
||||
import { buildUser } from "../test/factories";
|
||||
import { flushdb } from "../test/support";
|
||||
import userInviter from "./userInviter";
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
|
||||
describe("userInviter", () => {
|
||||
const ip = "127.0.0.1";
|
||||
|
||||
it("should return sent invites", async () => {
|
||||
const user = await buildUser();
|
||||
const response = await userInviter({
|
||||
invites: [{ email: "test@example.com", name: "Test" }],
|
||||
user,
|
||||
ip,
|
||||
});
|
||||
expect(response.sent.length).toEqual(1);
|
||||
});
|
||||
|
||||
it("should filter empty invites", async () => {
|
||||
const user = await buildUser();
|
||||
const response = await userInviter({
|
||||
invites: [{ email: " ", name: "Test" }],
|
||||
user,
|
||||
ip,
|
||||
});
|
||||
expect(response.sent.length).toEqual(0);
|
||||
});
|
||||
|
||||
it("should filter obviously bunk emails", async () => {
|
||||
const user = await buildUser();
|
||||
const response = await userInviter({
|
||||
invites: [{ email: "notanemail", name: "Test" }],
|
||||
user,
|
||||
ip,
|
||||
});
|
||||
expect(response.sent.length).toEqual(0);
|
||||
});
|
||||
|
||||
it("should not send duplicates", async () => {
|
||||
const user = await buildUser();
|
||||
const response = await userInviter({
|
||||
invites: [
|
||||
{ email: "the@same.com", name: "Test" },
|
||||
{ email: "the@SAME.COM", name: "Test" },
|
||||
],
|
||||
user,
|
||||
ip,
|
||||
});
|
||||
expect(response.sent.length).toEqual(1);
|
||||
});
|
||||
|
||||
it("should not send invites to existing team members", async () => {
|
||||
const user = await buildUser();
|
||||
const response = await userInviter({
|
||||
invites: [{ email: user.email, name: user.name }],
|
||||
user,
|
||||
ip,
|
||||
});
|
||||
expect(response.sent.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
93
server/commands/userInviter.test.ts
Normal file
93
server/commands/userInviter.test.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { buildUser } from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
import userInviter from "./userInviter";
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
describe("userInviter", () => {
|
||||
const ip = "127.0.0.1";
|
||||
|
||||
it("should return sent invites", async () => {
|
||||
const user = await buildUser();
|
||||
const response = await userInviter({
|
||||
invites: [
|
||||
// @ts-expect-error ts-migrate(2741) FIXME: Property 'role' is missing in type '{ email: strin... Remove this comment to see the full error message
|
||||
{
|
||||
email: "test@example.com",
|
||||
name: "Test",
|
||||
},
|
||||
],
|
||||
user,
|
||||
ip,
|
||||
});
|
||||
expect(response.sent.length).toEqual(1);
|
||||
});
|
||||
|
||||
it("should filter empty invites", async () => {
|
||||
const user = await buildUser();
|
||||
const response = await userInviter({
|
||||
invites: [
|
||||
// @ts-expect-error ts-migrate(2741) FIXME: Property 'role' is missing in type '{ email: strin... Remove this comment to see the full error message
|
||||
{
|
||||
email: " ",
|
||||
name: "Test",
|
||||
},
|
||||
],
|
||||
user,
|
||||
ip,
|
||||
});
|
||||
expect(response.sent.length).toEqual(0);
|
||||
});
|
||||
|
||||
it("should filter obviously bunk emails", async () => {
|
||||
const user = await buildUser();
|
||||
const response = await userInviter({
|
||||
invites: [
|
||||
// @ts-expect-error ts-migrate(2741) FIXME: Property 'role' is missing in type '{ email: strin... Remove this comment to see the full error message
|
||||
{
|
||||
email: "notanemail",
|
||||
name: "Test",
|
||||
},
|
||||
],
|
||||
user,
|
||||
ip,
|
||||
});
|
||||
expect(response.sent.length).toEqual(0);
|
||||
});
|
||||
|
||||
it("should not send duplicates", async () => {
|
||||
const user = await buildUser();
|
||||
const response = await userInviter({
|
||||
invites: [
|
||||
// @ts-expect-error ts-migrate(2741) FIXME: Property 'role' is missing in type '{ email: strin... Remove this comment to see the full error message
|
||||
{
|
||||
email: "the@same.com",
|
||||
name: "Test",
|
||||
},
|
||||
// @ts-expect-error ts-migrate(2741) FIXME: Property 'role' is missing in type '{ email: strin... Remove this comment to see the full error message
|
||||
{
|
||||
email: "the@SAME.COM",
|
||||
name: "Test",
|
||||
},
|
||||
],
|
||||
user,
|
||||
ip,
|
||||
});
|
||||
expect(response.sent.length).toEqual(1);
|
||||
});
|
||||
|
||||
it("should not send invites to existing team members", async () => {
|
||||
const user = await buildUser();
|
||||
const response = await userInviter({
|
||||
invites: [
|
||||
// @ts-expect-error ts-migrate(2741) FIXME: Property 'role' is missing in type '{ email: any; ... Remove this comment to see the full error message
|
||||
{
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
},
|
||||
],
|
||||
user,
|
||||
ip,
|
||||
});
|
||||
expect(response.sent.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
@@ -1,13 +1,12 @@
|
||||
// @flow
|
||||
import { uniqBy } from "lodash";
|
||||
import type { Role } from "shared/types";
|
||||
import { Role } from "@shared/types";
|
||||
import { User, Event, Team } from "@server/models";
|
||||
import mailer from "../mailer";
|
||||
import { User, Event, Team } from "../models";
|
||||
|
||||
type Invite = {
|
||||
name: string,
|
||||
email: string,
|
||||
role: Role,
|
||||
name: string;
|
||||
email: string;
|
||||
role: Role;
|
||||
};
|
||||
|
||||
export default async function userInviter({
|
||||
@@ -15,17 +14,20 @@ export default async function userInviter({
|
||||
invites,
|
||||
ip,
|
||||
}: {
|
||||
user: User,
|
||||
invites: Invite[],
|
||||
ip: string,
|
||||
}): Promise<{ sent: Invite[], users: User[] }> {
|
||||
// @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
|
||||
user: User;
|
||||
invites: Invite[];
|
||||
ip: string;
|
||||
}): Promise<{
|
||||
sent: Invite[];
|
||||
// @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
|
||||
users: User[];
|
||||
}> {
|
||||
const team = await Team.findByPk(user.teamId);
|
||||
|
||||
// filter out empties and obvious non-emails
|
||||
const compactedInvites = invites.filter(
|
||||
(invite) => !!invite.email.trim() && invite.email.match("@")
|
||||
);
|
||||
|
||||
// normalize to lowercase and remove duplicates
|
||||
const normalizedInvites = uniqBy(
|
||||
compactedInvites.map((invite) => ({
|
||||
@@ -34,7 +36,6 @@ export default async function userInviter({
|
||||
})),
|
||||
"email"
|
||||
);
|
||||
|
||||
// filter out any existing users in the system
|
||||
const emails = normalizedInvites.map((invite) => invite.email);
|
||||
const existingUsers = await User.findAll({
|
||||
@@ -43,12 +44,12 @@ export default async function userInviter({
|
||||
email: emails,
|
||||
},
|
||||
});
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'user' implicitly has an 'any' type.
|
||||
const existingEmails = existingUsers.map((user) => user.email);
|
||||
const filteredInvites = normalizedInvites.filter(
|
||||
(invite) => !existingEmails.includes(invite.email)
|
||||
);
|
||||
|
||||
let users = [];
|
||||
const users = [];
|
||||
|
||||
// send and record remaining invites
|
||||
for (const invite of filteredInvites) {
|
||||
@@ -61,7 +62,6 @@ export default async function userInviter({
|
||||
isViewer: invite.role === "viewer",
|
||||
});
|
||||
users.push(newUser);
|
||||
|
||||
await Event.create({
|
||||
name: "users.invite",
|
||||
actorId: user.id,
|
||||
@@ -73,7 +73,6 @@ export default async function userInviter({
|
||||
},
|
||||
ip,
|
||||
});
|
||||
|
||||
await mailer.sendTemplate("invite", {
|
||||
to: invite.email,
|
||||
name: invite.name,
|
||||
@@ -84,5 +83,8 @@ export default async function userInviter({
|
||||
});
|
||||
}
|
||||
|
||||
return { sent: filteredInvites, users };
|
||||
return {
|
||||
sent: filteredInvites,
|
||||
users,
|
||||
};
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
/* eslint-disable flowtype/require-valid-file-annotation */
|
||||
import { GroupUser } from "../models";
|
||||
import { buildGroup, buildAdmin, buildUser } from "../test/factories";
|
||||
import { flushdb } from "../test/support";
|
||||
import { GroupUser } from "@server/models";
|
||||
import { buildGroup, buildAdmin, buildUser } from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
import userSuspender from "./userSuspender";
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
|
||||
describe("userSuspender", () => {
|
||||
const ip = "127.0.0.1";
|
||||
|
||||
@@ -28,7 +26,9 @@ describe("userSuspender", () => {
|
||||
|
||||
it("should suspend the user", async () => {
|
||||
const admin = await buildAdmin();
|
||||
const user = await buildUser({ teamId: admin.teamId });
|
||||
const user = await buildUser({
|
||||
teamId: admin.teamId,
|
||||
});
|
||||
await userSuspender({
|
||||
actorId: admin.id,
|
||||
user,
|
||||
@@ -40,11 +40,17 @@ describe("userSuspender", () => {
|
||||
|
||||
it("should remove group memberships", async () => {
|
||||
const admin = await buildAdmin();
|
||||
const user = await buildUser({ teamId: admin.teamId });
|
||||
const group = await buildGroup({ teamId: user.teamId });
|
||||
|
||||
await group.addUser(user, { through: { createdById: user.id } });
|
||||
|
||||
const user = await buildUser({
|
||||
teamId: admin.teamId,
|
||||
});
|
||||
const group = await buildGroup({
|
||||
teamId: user.teamId,
|
||||
});
|
||||
await group.addUser(user, {
|
||||
through: {
|
||||
createdById: user.id,
|
||||
},
|
||||
});
|
||||
await userSuspender({
|
||||
actorId: admin.id,
|
||||
user,
|
||||
@@ -1,7 +1,6 @@
|
||||
// @flow
|
||||
import { Transaction } from "sequelize";
|
||||
import { User, Event, GroupUser } from "@server/models";
|
||||
import { ValidationError } from "../errors";
|
||||
import { User, Event, GroupUser } from "../models";
|
||||
import { sequelize } from "../sequelize";
|
||||
|
||||
export default async function userSuspender({
|
||||
@@ -9,12 +8,13 @@ export default async function userSuspender({
|
||||
actorId,
|
||||
ip,
|
||||
}: {
|
||||
user: User,
|
||||
actorId: string,
|
||||
ip: string,
|
||||
// @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
|
||||
user: User;
|
||||
actorId: string;
|
||||
ip: string;
|
||||
}): Promise<void> {
|
||||
if (user.id === actorId) {
|
||||
throw new ValidationError("Unable to suspend the current user");
|
||||
throw ValidationError("Unable to suspend the current user");
|
||||
}
|
||||
|
||||
await sequelize.transaction(async (transaction: Transaction) => {
|
||||
@@ -23,21 +23,30 @@ export default async function userSuspender({
|
||||
suspendedById: actorId,
|
||||
suspendedAt: new Date(),
|
||||
},
|
||||
{ transaction }
|
||||
{
|
||||
transaction,
|
||||
}
|
||||
);
|
||||
|
||||
await GroupUser.destroy({ where: { userId: user.id }, transaction });
|
||||
|
||||
await GroupUser.destroy({
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
transaction,
|
||||
});
|
||||
await Event.create(
|
||||
{
|
||||
name: "users.suspend",
|
||||
actorId,
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
data: { name: user.name },
|
||||
data: {
|
||||
name: user.name,
|
||||
},
|
||||
ip,
|
||||
},
|
||||
{ transaction }
|
||||
{
|
||||
transaction,
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user