diff --git a/plugins/enterprise/client/translations.tsx b/plugins/enterprise/client/translations.tsx
index 1eee7afc76..ebc58a20e1 100644
--- a/plugins/enterprise/client/translations.tsx
+++ b/plugins/enterprise/client/translations.tsx
@@ -1,4 +1,4 @@
-import { Trans } from 'react-i18next';
+import { Trans } from "react-i18next";
export const Translations = () => (
<>
@@ -6,7 +6,9 @@ export const Translations = () => (
-
+
@@ -21,35 +23,59 @@ export const Translations = () => (
-
+
-
-
+
+
-
+
- Space Settings -> Manage space -> Export space and choose to export as HTML with the "Normal Export" option.`} />
-
+ Space Settings -> Manage space -> Export space and choose to export as HTML with the "Normal Export" option.`}
+ />
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -62,15 +88,25 @@ export const Translations = () => (
-
- Glean4> in realtime.`} />
+
+ Glean4> in realtime.`}
+ />
- priority@getoutline.com4>.`} />
-
-
+ priority@getoutline.com4>.`}
+ />
+
+
>
-)
\ No newline at end of file
+);
diff --git a/plugins/slack/server/processors/SlackProcessor.ts b/plugins/slack/server/processors/SlackProcessor.ts
index 95606f097b..4ebc33ea2d 100644
--- a/plugins/slack/server/processors/SlackProcessor.ts
+++ b/plugins/slack/server/processors/SlackProcessor.ts
@@ -82,8 +82,7 @@ export default class SlackProcessor extends BaseProcessor {
async documentUpdated(event: DocumentEvent | RevisionEvent) {
// never send notifications when batch importing documents
- // @ts-expect-error ts-migrate(2339) FIXME: Property 'data' does not exist on type 'DocumentEv... Remove this comment to see the full error message
- if (event.data && event.data.source === "import") {
+ if (event.name === "documents.publish" && event.data?.source === "import") {
return;
}
const [document, team] = await Promise.all([
diff --git a/server/commands/accountProvisioner.ts b/server/commands/accountProvisioner.ts
index 4d037a3903..097a5ac077 100644
--- a/server/commands/accountProvisioner.ts
+++ b/server/commands/accountProvisioner.ts
@@ -21,6 +21,7 @@ import { sequelize } from "@server/storage/database";
import teamProvisioner from "./teamProvisioner";
import userProvisioner from "./userProvisioner";
import { APIContext } from "@server/types";
+import { createContext } from "@server/context";
import { addSeconds } from "date-fns";
type Props = {
@@ -246,9 +247,9 @@ async function provisionFirstCollection(team: Team, user: User) {
document.content = await DocumentHelper.toJSON(document);
- await document.publish(user, collection.id, {
+ await document.publish(createContext({ user, transaction }), {
+ collectionId: collection.id,
silent: true,
- transaction,
});
}
});
diff --git a/server/commands/documentCreator.test.ts b/server/commands/documentCreator.test.ts
index 89ab9be3a5..fde1681e3a 100644
--- a/server/commands/documentCreator.test.ts
+++ b/server/commands/documentCreator.test.ts
@@ -23,13 +23,11 @@ describe("documentCreator", () => {
).toJSON();
const document = await withAPIContext(user, (ctx) =>
- documentCreator({
+ documentCreator(ctx, {
title: "Test Document",
text: testText,
content: testContent,
collectionId: collection.id,
- user,
- ctx,
})
);
@@ -48,12 +46,10 @@ describe("documentCreator", () => {
const testText = "This is plain text";
const document = await withAPIContext(user, (ctx) =>
- documentCreator({
+ documentCreator(ctx, {
title: "Test Document",
text: testText,
collectionId: collection.id,
- user,
- ctx,
})
);
@@ -68,11 +64,9 @@ describe("documentCreator", () => {
});
const document = await withAPIContext(user, (ctx) =>
- documentCreator({
+ documentCreator(ctx, {
title: "Empty Document",
collectionId: collection.id,
- user,
- ctx,
})
);
@@ -90,12 +84,10 @@ describe("documentCreator", () => {
});
const document = await withAPIContext(user, (ctx) =>
- documentCreator({
+ documentCreator(ctx, {
title: "Test Document",
text: "This is a test document",
collectionId: collection.id,
- user,
- ctx,
})
);
@@ -115,15 +107,13 @@ describe("documentCreator", () => {
});
const document = await withAPIContext(user, (ctx) =>
- documentCreator({
+ documentCreator(ctx, {
title: "Custom Document",
text: "Custom content",
icon: "📄",
color: "#FF0000",
fullWidth: true,
collectionId: collection.id,
- user,
- ctx,
})
);
@@ -140,13 +130,11 @@ describe("documentCreator", () => {
});
const document = await withAPIContext(user, (ctx) =>
- documentCreator({
+ documentCreator(ctx, {
title: "Draft Document",
text: "Draft content",
collectionId: collection.id,
publish: false,
- user,
- ctx,
})
);
@@ -161,13 +149,11 @@ describe("documentCreator", () => {
});
const document = await withAPIContext(user, (ctx) =>
- documentCreator({
+ documentCreator(ctx, {
title: "Published Document",
text: "Published content",
collectionId: collection.id,
publish: true,
- user,
- ctx,
})
);
@@ -179,12 +165,10 @@ describe("documentCreator", () => {
await expect(
withAPIContext(user, (ctx) =>
- documentCreator({
+ documentCreator(ctx, {
title: "Invalid Document",
text: "Content",
publish: true,
- user,
- ctx,
})
)
).rejects.toThrow("Collection ID is required to publish");
@@ -211,12 +195,10 @@ describe("documentCreator", () => {
});
const document = await withAPIContext(user, (ctx) =>
- documentCreator({
+ documentCreator(ctx, {
title: "From Template",
templateDocument,
collectionId: collection.id,
- user,
- ctx,
})
);
@@ -242,11 +224,9 @@ describe("documentCreator", () => {
});
const document = await withAPIContext(user, (ctx) =>
- documentCreator({
+ documentCreator(ctx, {
templateDocument,
collectionId: collection.id,
- user,
- ctx,
})
);
@@ -270,12 +250,10 @@ describe("documentCreator", () => {
await expect(
withAPIContext(user, (ctx) =>
- documentCreator({
+ documentCreator(ctx, {
templateDocument,
state: Buffer.from("some state"),
collectionId: collection.id,
- user,
- ctx,
})
)
).rejects.toThrow(
@@ -299,12 +277,10 @@ describe("documentCreator", () => {
});
const document = await withAPIContext(user, (ctx) =>
- documentCreator({
+ documentCreator(ctx, {
templateDocument,
template: true,
collectionId: collection.id,
- user,
- ctx,
})
);
@@ -330,13 +306,11 @@ describe("documentCreator", () => {
});
const document = await withAPIContext(user, (ctx) =>
- documentCreator({
+ documentCreator(ctx, {
title: "Child Document",
text: "Child content",
parentDocumentId: parentDocument.id,
collectionId: collection.id,
- user,
- ctx,
})
);
@@ -359,14 +333,12 @@ describe("documentCreator", () => {
const sourceMetadata = { fileName: "test" };
const document = await withAPIContext(user, (ctx) =>
- documentCreator({
+ documentCreator(ctx, {
title: "fileOperation Document",
text: "fileOperation content",
importId: fileOperation.id,
sourceMetadata,
collectionId: collection.id,
- user,
- ctx,
})
);
diff --git a/server/commands/documentCreator.ts b/server/commands/documentCreator.ts
index 1c31e29b64..a9cdf06013 100644
--- a/server/commands/documentCreator.ts
+++ b/server/commands/documentCreator.ts
@@ -1,7 +1,7 @@
import { Optional } from "utility-types";
import { ProsemirrorHelper as SharedProsemirrorHelper } from "@shared/utils/ProsemirrorHelper";
import { TextHelper } from "@shared/utils/TextHelper";
-import { Document, Event, User } from "@server/models";
+import { Document } from "@server/models";
import { DocumentHelper } from "@server/models/helpers/DocumentHelper";
import { ProsemirrorHelper } from "@server/models/helpers/ProsemirrorHelper";
import { APIContext } from "@server/types";
@@ -32,39 +32,41 @@ type Props = Optional<
state?: Buffer;
publish?: boolean;
templateDocument?: Document | null;
- user: User;
- ctx: APIContext;
};
-export default async function documentCreator({
- title,
- text,
- icon,
- color,
- state,
- id,
- urlId,
- publish,
- collectionId,
- parentDocumentId,
- content,
- template,
- templateDocument,
- fullWidth,
- importId,
- apiImportId,
- createdAt,
- // allows override for import
- updatedAt,
- user,
- editorVersion,
- publishedAt,
- sourceMetadata,
- ctx,
-}: Props): Promise {
- const { transaction, ip } = ctx.context;
+export default async function documentCreator(
+ ctx: APIContext,
+ {
+ title,
+ text,
+ icon,
+ color,
+ state,
+ id,
+ urlId,
+ publish,
+ collectionId,
+ parentDocumentId,
+ content,
+ template,
+ templateDocument,
+ fullWidth,
+ importId,
+ apiImportId,
+ createdAt,
+ // allows override for import
+ updatedAt,
+ editorVersion,
+ publishedAt,
+ sourceMetadata,
+ }: Props
+): Promise {
+ const { user } = ctx.state.auth;
+ const { transaction } = ctx.state;
const templateId = templateDocument ? templateDocument.id : undefined;
+ const eventData = importId || apiImportId ? { source: "import" } : undefined;
+
if (state && templateDocument) {
throw new Error(
"State cannot be set when creating a document from a template"
@@ -134,28 +136,12 @@ export default async function documentCreator({
includeTitle: false,
});
- await document.save({
- silent: !!createdAt,
- transaction,
- });
-
- await Event.create(
+ await document.saveWithCtx(
+ ctx,
{
- name: "documents.create",
- documentId: document.id,
- collectionId: document.collectionId,
- teamId: document.teamId,
- actorId: user.id,
- data: {
- source: importId || apiImportId ? "import" : undefined,
- title: document.title,
- templateId,
- },
- ip,
+ silent: !!createdAt,
},
- {
- transaction,
- }
+ { data: eventData }
);
if (publish) {
@@ -163,26 +149,12 @@ export default async function documentCreator({
throw new Error("Collection ID is required to publish");
}
- await document.publish(user, collectionId, { silent: true, transaction });
- if (document.title) {
- await Event.create(
- {
- name: "documents.publish",
- documentId: document.id,
- collectionId: document.collectionId,
- teamId: document.teamId,
- actorId: user.id,
- data: {
- source: importId ? "import" : undefined,
- title: document.title,
- },
- ip,
- },
- {
- transaction,
- }
- );
- }
+ await document.publish(ctx, {
+ collectionId,
+ silent: true,
+ event: !!document.title,
+ data: eventData,
+ });
}
// reload to get all of the data needed to present (user, collection etc)
diff --git a/server/commands/documentDuplicator.test.ts b/server/commands/documentDuplicator.test.ts
index ad721f70f7..3c5a636852 100644
--- a/server/commands/documentDuplicator.test.ts
+++ b/server/commands/documentDuplicator.test.ts
@@ -5,6 +5,7 @@ import {
buildDocument,
buildUser,
} from "@server/test/factories";
+import { withAPIContext } from "@server/test/support";
import documentDuplicator from "./documentDuplicator";
describe("documentDuplicator", () => {
@@ -15,12 +16,10 @@ describe("documentDuplicator", () => {
teamId: user.teamId,
});
- const response = await sequelize.transaction((transaction) =>
- documentDuplicator({
+ const response = await withAPIContext(user, (ctx) =>
+ documentDuplicator(ctx, {
document: original,
collection: original.collection,
- user,
- ctx: createContext({ user, transaction }),
})
);
@@ -40,13 +39,11 @@ describe("documentDuplicator", () => {
icon: "👋",
});
- const response = await sequelize.transaction((transaction) =>
- documentDuplicator({
+ const response = await withAPIContext(user, (ctx) =>
+ documentDuplicator(ctx, {
document: original,
collection: original.collection,
title: "New title",
- user,
- ctx: createContext({ user, transaction }),
})
);
@@ -99,14 +96,12 @@ describe("documentDuplicator", () => {
await collection.addDocumentToStructure(child2);
await collection.addDocumentToStructure(child3);
- await sequelize.transaction((transaction) =>
- documentDuplicator({
+ await withAPIContext(user, (ctx) =>
+ documentDuplicator(ctx, {
title: "duplicate",
document: original,
collection: original.collection,
- user,
recursive: true,
- ctx: createContext({ user, transaction }),
})
);
@@ -128,13 +123,11 @@ describe("documentDuplicator", () => {
teamId: user.teamId,
});
- const response = await sequelize.transaction((transaction) =>
- documentDuplicator({
+ const response = await withAPIContext(user, (ctx) =>
+ documentDuplicator(ctx, {
document: original,
collection: original.collection,
publish: false,
- user,
- ctx: createContext({ user, transaction }),
})
);
@@ -155,11 +148,9 @@ describe("documentDuplicator", () => {
});
const response = await sequelize.transaction((transaction) =>
- documentDuplicator({
+ documentDuplicator(createContext({ user, transaction }), {
document: original,
collection: original.collection,
- user,
- ctx: createContext({ user, transaction }),
})
);
@@ -187,12 +178,10 @@ describe("documentDuplicator", () => {
});
const response = await sequelize.transaction((transaction) =>
- documentDuplicator({
+ documentDuplicator(createContext({ user, transaction }), {
document: original,
collection: original.collection,
- user,
recursive: true,
- ctx: createContext({ user, transaction }),
})
);
diff --git a/server/commands/documentDuplicator.ts b/server/commands/documentDuplicator.ts
index 9d6ef572ec..a815ea0fc0 100644
--- a/server/commands/documentDuplicator.ts
+++ b/server/commands/documentDuplicator.ts
@@ -1,13 +1,11 @@
import { Op } from "sequelize";
-import { User, Collection, Document } from "@server/models";
+import { Collection, Document } from "@server/models";
import { DocumentHelper } from "@server/models/helpers/DocumentHelper";
import { ProsemirrorHelper } from "@server/models/helpers/ProsemirrorHelper";
import { APIContext } from "@server/types";
import documentCreator from "./documentCreator";
type Props = {
- /** The user who is creating the document */
- user: User;
/** The document to duplicate */
document: Document;
/** The collection to add the duplicated document to */
@@ -20,29 +18,19 @@ type Props = {
publish?: boolean;
/** Whether to duplicate child documents */
recursive?: boolean;
- /** The request context */
- ctx: APIContext;
};
-export default async function documentDuplicator({
- user,
- document,
- collection,
- parentDocumentId,
- title,
- publish,
- recursive,
- ctx,
-}: Props): Promise {
+export default async function documentDuplicator(
+ ctx: APIContext,
+ { document, collection, parentDocumentId, title, publish, recursive }: Props
+): Promise {
const newDocuments: Document[] = [];
const sharedProperties = {
- user,
collectionId: collection?.id,
publish: publish ?? !!document.publishedAt,
- ctx,
};
- const duplicated = await documentCreator({
+ const duplicated = await documentCreator(ctx, {
parentDocumentId,
icon: document.icon,
color: document.color,
@@ -93,7 +81,7 @@ export default async function documentDuplicator({
).reverse(); // we have to reverse since the child documents will be added in reverse order
for (const childDocument of sorted) {
- const duplicatedChildDocument = await documentCreator({
+ const duplicatedChildDocument = await documentCreator(ctx, {
parentDocumentId: duplicatedDocument.id,
icon: childDocument.icon,
color: childDocument.color,
diff --git a/server/commands/documentMover.test.ts b/server/commands/documentMover.test.ts
index 27ed38b74b..6530fbba68 100644
--- a/server/commands/documentMover.test.ts
+++ b/server/commands/documentMover.test.ts
@@ -1,5 +1,4 @@
import Pin from "@server/models/Pin";
-import { sequelize } from "@server/storage/database";
import {
buildDocument,
buildCollection,
@@ -7,10 +6,9 @@ import {
buildUser,
} from "@server/test/factories";
import documentMover from "./documentMover";
+import { withAPIContext } from "@server/test/support";
describe("documentMover", () => {
- const ip = "127.0.0.1";
-
it("should move within a collection", async () => {
const team = await buildTeam();
const user = await buildUser({ teamId: team.id });
@@ -23,12 +21,12 @@ describe("documentMover", () => {
collectionId: collection.id,
teamId: team.id,
});
- const response = await documentMover({
- user,
- document,
- collectionId: collection.id,
- ip,
- });
+ const response = await withAPIContext(user, (ctx) =>
+ documentMover(ctx, {
+ document,
+ collectionId: collection.id,
+ })
+ );
expect(response.collections.length).toEqual(1);
expect(response.documents.length).toEqual(1);
});
@@ -53,14 +51,14 @@ describe("documentMover", () => {
title: "Child document",
text: "content",
});
- const response = await documentMover({
- user,
- document,
- collectionId: collection.id,
- parentDocumentId: undefined,
- index: 0,
- ip,
- });
+ const response = await withAPIContext(user, (ctx) =>
+ documentMover(ctx, {
+ document,
+ collectionId: collection.id,
+ parentDocumentId: undefined,
+ index: 0,
+ })
+ );
expect(response.collections[0].documentStructure![0].children[0].id).toBe(
newDocument.id
);
@@ -91,14 +89,14 @@ describe("documentMover", () => {
text: "content",
});
await collection.addDocumentToStructure(newDocument);
- const response = await documentMover({
- user,
- document,
- collectionId: collection.id,
- parentDocumentId: undefined,
- index: 0,
- ip,
- });
+ const response = await withAPIContext(user, (ctx) =>
+ documentMover(ctx, {
+ document,
+ collectionId: collection.id,
+ parentDocumentId: undefined,
+ index: 0,
+ })
+ );
expect(response.collections[0].documentStructure![0].children[0].id).toBe(
newDocument.id
);
@@ -132,14 +130,14 @@ describe("documentMover", () => {
text: "content",
});
await collection.addDocumentToStructure(newDocument);
- const response = await documentMover({
- user,
- document,
- collectionId: newCollection.id,
- parentDocumentId: undefined,
- index: 0,
- ip,
- });
+ const response = await withAPIContext(user, (ctx) =>
+ documentMover(ctx, {
+ document,
+ collectionId: newCollection.id,
+ parentDocumentId: undefined,
+ index: 0,
+ })
+ );
// check document ids where updated
await newDocument.reload();
expect(newDocument.collectionId).toBe(newCollection.id);
@@ -181,15 +179,12 @@ describe("documentMover", () => {
teamId: collection.teamId,
});
- const response = await sequelize.transaction(async (transaction) =>
- documentMover({
- user,
+ const response = await withAPIContext(user, (ctx) =>
+ documentMover(ctx, {
document,
collectionId: newCollection.id,
parentDocumentId: undefined,
index: 0,
- ip,
- transaction,
})
);
@@ -223,14 +218,11 @@ describe("documentMover", () => {
teamId: team.id,
});
- const response = await sequelize.transaction(async (transaction) =>
- documentMover({
- user,
+ const response = await withAPIContext(user, (ctx) =>
+ documentMover(ctx, {
document,
collectionId: null,
index: 0,
- ip,
- transaction,
})
);
diff --git a/server/commands/documentMover.ts b/server/commands/documentMover.ts
index 82fc851408..6ca38090a0 100644
--- a/server/commands/documentMover.ts
+++ b/server/commands/documentMover.ts
@@ -1,11 +1,9 @@
import { Transaction } from "sequelize";
-import { createContext } from "@server/context";
import { traceFunction } from "@server/logging/tracing";
-import { User, Document, Collection, Pin, Event } from "@server/models";
+import { Document, Collection, Pin } from "@server/models";
+import { APIContext } from "@server/types";
type Props = {
- /** User attempting to move the document */
- user: User;
/** Document which is being moved */
document: Document;
/** Destination collection to which the document is moved */
@@ -14,10 +12,6 @@ type Props = {
parentDocumentId?: string | null;
/** Position of moved document within document structure */
index?: number;
- /** The IP address of the user moving the document */
- ip: string | null;
- /** The database transaction to run within */
- transaction?: Transaction;
};
type Result = {
@@ -26,16 +20,19 @@ type Result = {
collectionChanged: boolean;
};
-async function documentMover({
- user,
- document,
- collectionId = null,
- parentDocumentId = null,
- // convert undefined to null so parentId comparison treats them as equal
- index,
- ip,
- transaction,
-}: Props): Promise {
+async function documentMover(
+ ctx: APIContext,
+ {
+ document,
+ collectionId = null,
+ parentDocumentId = null,
+ // convert undefined to null so parentId comparison treats them as equal
+ index,
+ }: Props
+): Promise {
+ const { user } = ctx.state.auth;
+ const { transaction } = ctx.state;
+
const collectionChanged = collectionId !== document.collectionId;
const previousCollectionId = document.collectionId;
const result: Result = {
@@ -206,37 +203,19 @@ async function documentMover({
lock: Transaction.LOCK.UPDATE,
});
- await pin?.destroyWithCtx(
- createContext({
- user,
- ip,
- transaction,
- })
- );
+ await pin?.destroyWithCtx(ctx);
}
}
- await document.save({ transaction });
result.documents.push(document);
- 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,
+ await document.saveWithCtx(ctx, undefined, {
+ name: "move",
+ data: {
+ collectionIds: result.collections.map((c) => c.id),
+ documentIds: result.documents.map((d) => d.id),
},
- {
- transaction,
- }
- );
+ });
// we need to send all updated models back to the client
return result;
diff --git a/server/commands/documentUpdater.test.ts b/server/commands/documentUpdater.test.ts
index 27b950a901..65ee02ffbc 100644
--- a/server/commands/documentUpdater.test.ts
+++ b/server/commands/documentUpdater.test.ts
@@ -14,7 +14,6 @@ describe("documentUpdater", () => {
documentUpdater(ctx, {
text: "Changed",
document,
- user,
})
);
@@ -36,7 +35,6 @@ describe("documentUpdater", () => {
documentUpdater(ctx, {
title: document.title,
document,
- user,
})
);
@@ -53,7 +51,6 @@ describe("documentUpdater", () => {
documentUpdater(ctx, {
text: "Changed",
document,
- user,
})
);
diff --git a/server/commands/documentUpdater.ts b/server/commands/documentUpdater.ts
index cc97c383ad..8ab5dbe240 100644
--- a/server/commands/documentUpdater.ts
+++ b/server/commands/documentUpdater.ts
@@ -1,11 +1,9 @@
-import { Event, Document, User } from "@server/models";
+import { Event, Document } from "@server/models";
import { DocumentHelper } from "@server/models/helpers/DocumentHelper";
import { TextHelper } from "@server/models/helpers/TextHelper";
import { APIContext } from "@server/types";
type Props = {
- /** The user updating the document */
- user: User;
/** The existing document */
document: Document;
/** The new title */
@@ -44,7 +42,6 @@ type Props = {
export default async function documentUpdater(
ctx: APIContext,
{
- user,
document,
title,
icon,
@@ -60,8 +57,8 @@ export default async function documentUpdater(
done,
}: Props
): Promise {
+ const { user } = ctx.state.auth;
const { transaction } = ctx.state;
- const previousTitle = document.title;
const cId = collectionId || document.collectionId;
if (title !== undefined) {
@@ -96,33 +93,24 @@ export default async function documentUpdater(
}
const changed = document.changed();
+ const eventData = done !== undefined ? { done } : undefined;
const event = {
name: "documents.update",
documentId: document.id,
collectionId: cId,
- data: {
- done,
- title: document.title,
- },
+ data: eventData,
};
if (publish && (document.template || cId)) {
if (!document.collectionId) {
document.collectionId = cId;
}
- await document.publish(user, cId, { transaction });
-
- await Event.createFromContext(ctx, {
- ...event,
- name: "documents.publish",
- });
+ await document.publish(ctx, { collectionId: cId, data: eventData });
} else if (changed) {
document.lastModifiedById = user.id;
document.updatedBy = user;
- await document.save({ transaction });
-
- await Event.createFromContext(ctx, event);
+ await document.saveWithCtx(ctx, undefined, { data: eventData });
} else if (done) {
await Event.schedule({
...event,
@@ -131,21 +119,6 @@ export default async function documentUpdater(
});
}
- if (document.title !== previousTitle) {
- await Event.schedule({
- name: "documents.title_change",
- documentId: document.id,
- collectionId: cId,
- teamId: document.teamId,
- actorId: user.id,
- data: {
- previousTitle,
- title: document.title,
- },
- ip: ctx.request.ip,
- });
- }
-
return await Document.findByPk(document.id, {
userId: user.id,
rejectOnEmpty: true,
diff --git a/server/env.ts b/server/env.ts
index ea8e2aa1e2..dbee4fb19f 100644
--- a/server/env.ts
+++ b/server/env.ts
@@ -424,7 +424,9 @@ export class Environment {
* Setting secure to false therefore does not mean that you would not use an
* encrypted connection.
*/
- public SMTP_DISABLE_STARTTLS = this.toBoolean(environment.SMTP_DISABLE_STARTTLS ?? "false");
+ public SMTP_DISABLE_STARTTLS = this.toBoolean(
+ environment.SMTP_DISABLE_STARTTLS ?? "false"
+ );
/**
* Dropbox app key for embedding Dropbox files
diff --git a/server/models/Document.ts b/server/models/Document.ts
index 1f42361dc8..d16d50ad2f 100644
--- a/server/models/Document.ts
+++ b/server/models/Document.ts
@@ -39,7 +39,9 @@ import {
AllowNull,
BelongsToMany,
Unique,
+ AfterUpdate,
} from "sequelize-typescript";
+import { MaxLength } from "class-validator";
import isUUID from "validator/lib/isUUID";
import type {
NavigationNode,
@@ -52,6 +54,7 @@ import slugify from "@shared/utils/slugify";
import { DocumentValidation } from "@shared/validations";
import { ValidationError } from "@server/errors";
import { generateUrlId } from "@server/utils/url";
+import { createContext } from "@server/context";
import Collection from "./Collection";
import FileOperation from "./FileOperation";
import Group from "./Group";
@@ -70,7 +73,9 @@ import Fix from "./decorators/Fix";
import { DocumentHelper } from "./helpers/DocumentHelper";
import IsHexColor from "./validators/IsHexColor";
import Length from "./validators/Length";
-import { MaxLength } from "class-validator";
+import { APIContext } from "@server/types";
+import { SkipChangeset } from "./decorators/Changeset";
+import { HookContext } from "./base/Model";
export const DOCUMENT_VERSION = 2;
@@ -338,6 +343,7 @@ class Document extends ArchivableModel<
* This column will be removed in a future migration.
*/
@Column(DataType.TEXT)
+ @SkipChangeset
text: string;
/** The likely language of the content, in ISO 639-1 format. */
@@ -349,6 +355,7 @@ class Document extends ArchivableModel<
* The content of the document as JSON, this is a snapshot at the last time the state was saved.
*/
@Column(DataType.JSONB)
+ @SkipChangeset
content: ProsemirrorData | null;
/**
@@ -360,6 +367,7 @@ class Document extends ArchivableModel<
msg: `Document collaborative state is too large, you must create a new document`,
})
@Column(DataType.BLOB)
+ @SkipChangeset
state?: Uint8Array | null;
/** Whether this document is part of onboarding. */
@@ -564,6 +572,20 @@ class Document extends ArchivableModel<
}
}
+ @AfterUpdate
+ static async publishTitleChangeEvent(
+ model: Document,
+ ctx: APIContext["context"]
+ ) {
+ if (model.changed("title")) {
+ const hookContext = {
+ ...ctx,
+ event: { publish: true, persist: false },
+ } as HookContext;
+ await this.insertEvent("title_change", model, hookContext);
+ }
+ }
+
// associations
@BelongsTo(() => FileOperation, "importId")
@@ -965,16 +987,30 @@ class Document extends ArchivableModel<
};
publish = async (
- user: User,
- collectionId: string | null | undefined,
- options: SaveOptions
+ ctx: APIContext,
+ {
+ collectionId,
+ silent = false,
+ event = true,
+ data,
+ }: {
+ collectionId: string | null | undefined;
+ silent?: boolean;
+ event?: boolean;
+ data?: Record;
+ }
): Promise => {
- const { transaction } = options;
+ const { user } = ctx.state.auth;
+ const { transaction } = ctx.state;
// If the document is already published then calling publish should act like
// a regular save
if (this.publishedAt) {
- return this.save(options);
+ if (event) {
+ return this.saveWithCtx(ctx, { silent }, { name: "publish", data });
+ } else {
+ return this.save({ silent, transaction });
+ }
}
if (!this.collectionId) {
@@ -1017,7 +1053,12 @@ class Document extends ArchivableModel<
this.lastModifiedById = user.id;
this.updatedBy = user;
this.publishedAt = new Date();
- return this.save(options);
+
+ if (event) {
+ return this.saveWithCtx(ctx, { silent }, { name: "publish", data });
+ } else {
+ return this.save({ silent, transaction });
+ }
};
isCollectionDeleted = async () => {
@@ -1042,28 +1083,29 @@ class Document extends ArchivableModel<
* @param options.detach Whether to detach the document from the containing collection
* @returns Updated document
*/
- unpublish = async (user: User, options: { detach: boolean }) => {
+ unpublishWithCtx = async (ctx: APIContext, options: { detach: boolean }) => {
// If the document is already a draft then calling unpublish should act like save
if (!this.publishedAt) {
return this.save();
}
- await this.sequelize.transaction(async (transaction: Transaction) => {
- const collection = this.collectionId
- ? await Collection.findByPk(this.collectionId, {
- includeDocumentStructure: true,
- transaction,
- lock: transaction.LOCK.UPDATE,
- })
- : undefined;
+ const { user } = ctx.state.auth;
+ const { transaction } = ctx.state;
- if (collection) {
- await collection.removeDocumentInStructure(this, { transaction });
- if (this.collection) {
- this.collection.documentStructure = collection.documentStructure;
- }
+ const collection = this.collectionId
+ ? await Collection.findByPk(this.collectionId, {
+ includeDocumentStructure: true,
+ transaction,
+ lock: transaction.LOCK.UPDATE,
+ })
+ : undefined;
+
+ if (collection) {
+ await collection.removeDocumentInStructure(this, { transaction });
+ if (this.collection) {
+ this.collection.documentStructure = collection.documentStructure;
}
- });
+ }
// unpublishing a document converts the ownership to yourself, so that it
// will appear in your drafts rather than the original creators
@@ -1077,13 +1119,13 @@ class Document extends ArchivableModel<
this.collectionId = null;
}
- return this.save();
+ return this.saveWithCtx(ctx, undefined, { name: "unpublish" });
};
// Moves a document from being visible to the team within a collection
// to the archived area, where it can be subsequently restored.
- archive = async (user: User, options?: FindOptions) => {
- const { transaction } = { ...options };
+ archiveWithCtx = async (ctx: APIContext) => {
+ const { transaction } = ctx.state;
const collection = this.collectionId
? await Collection.findByPk(this.collectionId, {
includeDocumentStructure: true,
@@ -1099,16 +1141,16 @@ class Document extends ArchivableModel<
}
}
- await this.archiveWithChildren(user, { transaction });
+ await this.archiveWithChildren(ctx);
return this;
};
// Restore an archived document back to being visible to the team
restoreTo = async (
- collectionId: string,
- options: FindOptions & { user: User }
+ ctx: APIContext,
+ { collectionId }: { collectionId: string }
) => {
- const { transaction } = { ...options };
+ const { transaction } = ctx.state;
const collection = collectionId
? await Collection.findByPk(collectionId, {
includeDocumentStructure: true,
@@ -1141,13 +1183,11 @@ class Document extends ArchivableModel<
if (this.deletedAt) {
await this.restore({ transaction });
this.collectionId = collectionId;
- await this.save({ transaction });
+ await this.saveWithCtx(ctx, undefined, { name: "restore" });
}
if (this.archivedAt) {
- await this.restoreWithChildren(collectionId, options.user, {
- transaction,
- });
+ await this.restoreArchivedWithChildren(ctx, { collectionId });
}
if (this.collection && collection) {
@@ -1185,7 +1225,9 @@ class Document extends ArchivableModel<
this.lastModifiedById = user.id;
this.updatedBy = user;
- return this.save({ transaction });
+ return this.saveWithCtx(createContext({ user, transaction }), undefined, {
+ name: "delete",
+ });
});
getTimestamp = () => Math.round(new Date(this.updatedAt).getTime() / 1000);
@@ -1255,11 +1297,13 @@ class Document extends ArchivableModel<
};
};
- private restoreWithChildren = async (
- collectionId: string,
- user: User,
- options?: FindOptions
+ private restoreArchivedWithChildren = async (
+ ctx: APIContext,
+ { collectionId }: { collectionId: string }
) => {
+ const { user } = ctx.state.auth;
+ const { transaction } = ctx.state;
+
const restoreChildren = async (parentDocumentId: string) => {
const childDocuments = await (
this.constructor as typeof Document
@@ -1267,7 +1311,7 @@ class Document extends ArchivableModel<
where: {
parentDocumentId,
},
- ...options,
+ transaction,
});
for (const child of childDocuments) {
await restoreChildren(child.id);
@@ -1275,7 +1319,7 @@ class Document extends ArchivableModel<
child.lastModifiedById = user.id;
child.updatedBy = user;
child.collectionId = collectionId;
- await child.save(options);
+ await child.save({ transaction });
}
};
@@ -1284,13 +1328,12 @@ class Document extends ArchivableModel<
this.lastModifiedById = user.id;
this.updatedBy = user;
this.collectionId = collectionId;
- return this.save(options);
+ return this.saveWithCtx(ctx, undefined, { name: "unarchive" });
};
- private archiveWithChildren = async (
- user: User,
- options?: FindOptions
- ) => {
+ private archiveWithChildren = async (ctx: APIContext) => {
+ const { user } = ctx.state.auth;
+ const { transaction } = ctx.state;
const archivedAt = new Date();
// Helper to archive all child documents for a document
@@ -1301,14 +1344,14 @@ class Document extends ArchivableModel<
where: {
parentDocumentId,
},
- ...options,
+ transaction,
});
for (const child of childDocuments) {
await archiveChildren(child.id);
child.archivedAt = archivedAt;
child.lastModifiedById = user.id;
child.updatedBy = user;
- await child.save(options);
+ await child.save({ transaction });
}
};
@@ -1316,7 +1359,7 @@ class Document extends ArchivableModel<
this.archivedAt = archivedAt;
this.lastModifiedById = user.id;
this.updatedBy = user;
- return this.save(options);
+ return this.saveWithCtx(ctx, undefined, { name: "archive" });
};
}
diff --git a/server/models/Group.ts b/server/models/Group.ts
index fdba598396..48d77038ac 100644
--- a/server/models/Group.ts
+++ b/server/models/Group.ts
@@ -66,7 +66,11 @@ class Group extends ParanoidModel<
@Column
name: string;
- @Length({ min: 0, max: GroupValidation.maxDescriptionLength, msg: `description must be ${GroupValidation.maxDescriptionLength} characters or less` })
+ @Length({
+ min: 0,
+ max: GroupValidation.maxDescriptionLength,
+ msg: `description must be ${GroupValidation.maxDescriptionLength} characters or less`,
+ })
@Column(DataType.TEXT)
description: string;
diff --git a/server/models/helpers/DocumentHelper.tsx b/server/models/helpers/DocumentHelper.tsx
index 656d5b19b7..02bf2f6d30 100644
--- a/server/models/helpers/DocumentHelper.tsx
+++ b/server/models/helpers/DocumentHelper.tsx
@@ -537,7 +537,9 @@ export class DocumentHelper {
documents: Document[],
documentStructure: NavigationNode[]
): Document[] {
- if (!documentStructure.length) {return documents;}
+ if (!documentStructure.length) {
+ return documents;
+ }
const orderMap = new Map();
documentStructure.forEach((node, index) => {
diff --git a/server/queues/processors/BacklinksProcessor.test.ts b/server/queues/processors/BacklinksProcessor.test.ts
index b842f4b8bf..332d261eb6 100644
--- a/server/queues/processors/BacklinksProcessor.test.ts
+++ b/server/queues/processors/BacklinksProcessor.test.ts
@@ -21,7 +21,6 @@ describe("documents.publish", () => {
collectionId: document.collectionId!,
teamId: document.teamId,
actorId: document.createdById,
- data: { title: document.title },
ip,
});
const backlinks = await Relationship.findAll({
@@ -52,7 +51,6 @@ describe("documents.publish", () => {
collectionId: document.collectionId!,
teamId: document.teamId,
actorId: document.createdById,
- data: { title: document.title },
ip,
});
const backlinks = await Relationship.findAll({
@@ -80,7 +78,7 @@ describe("documents.update", () => {
teamId: document.teamId,
actorId: document.createdById,
createdAt: new Date().toISOString(),
- data: { title: document.title, autosave: false, done: true },
+ data: { done: true },
ip,
});
const backlinks = await Relationship.findAll({
@@ -111,7 +109,7 @@ describe("documents.update", () => {
teamId: document.teamId,
actorId: document.createdById,
createdAt: new Date().toISOString(),
- data: { title: document.title, autosave: false, done: true },
+ data: { done: true },
ip,
});
const backlinks = await Relationship.findAll({
@@ -139,7 +137,7 @@ describe("documents.update", () => {
teamId: document.teamId,
actorId: document.createdById,
createdAt: new Date().toISOString(),
- data: { title: document.title, autosave: false, done: true },
+ data: { done: true },
ip,
});
const backlinks = await Relationship.findAll({
@@ -167,7 +165,6 @@ describe("documents.update", () => {
collectionId: document.collectionId!,
teamId: document.teamId,
actorId: document.createdById,
- data: { title: document.title },
ip,
});
document.content = parser
@@ -186,7 +183,7 @@ describe("documents.update", () => {
teamId: document.teamId,
actorId: document.createdById,
createdAt: new Date().toISOString(),
- data: { title: document.title, autosave: false, done: true },
+ data: { done: true },
ip,
});
const backlinks = await Relationship.findAll({
@@ -217,7 +214,7 @@ describe("documents.delete", () => {
teamId: document.teamId,
actorId: document.createdById,
createdAt: new Date().toISOString(),
- data: { title: document.title, autosave: false, done: true },
+ data: { done: true },
ip,
});
@@ -227,7 +224,6 @@ describe("documents.delete", () => {
collectionId: document.collectionId!,
teamId: document.teamId,
actorId: document.createdById,
- data: { title: document.title },
ip,
});
const backlinks = await Relationship.findAll({
diff --git a/server/queues/processors/NotificationsProcessor.ts b/server/queues/processors/NotificationsProcessor.ts
index a2acddae3f..87a3f0d1a6 100644
--- a/server/queues/processors/NotificationsProcessor.ts
+++ b/server/queues/processors/NotificationsProcessor.ts
@@ -63,11 +63,7 @@ export default class NotificationsProcessor extends BaseProcessor {
async documentPublished(event: DocumentEvent) {
// never send notifications when batch importing
- if (
- "data" in event &&
- "source" in event.data &&
- event.data.source === "import"
- ) {
+ if (event.name === "documents.publish" && event.data?.source === "import") {
return;
}
diff --git a/server/queues/processors/RevisionsProcessor.test.ts b/server/queues/processors/RevisionsProcessor.test.ts
index e4188590a2..76dca6dcb0 100644
--- a/server/queues/processors/RevisionsProcessor.test.ts
+++ b/server/queues/processors/RevisionsProcessor.test.ts
@@ -17,7 +17,7 @@ describe("documents.update.debounced", () => {
teamId: document.teamId,
actorId: document.createdById,
createdAt: new Date().toISOString(),
- data: { title: document.title, autosave: false, done: true },
+ data: { done: true },
ip,
});
const amount = await Revision.count({
@@ -44,7 +44,7 @@ describe("documents.update.debounced", () => {
teamId: document.teamId,
actorId: document.createdById,
createdAt: new Date().toISOString(),
- data: { title: document.title, autosave: false, done: true },
+ data: { done: true },
ip,
});
const amount = await Revision.count({
diff --git a/server/queues/processors/RevisionsProcessor.ts b/server/queues/processors/RevisionsProcessor.ts
index 6fb50f79eb..acd3753bfc 100644
--- a/server/queues/processors/RevisionsProcessor.ts
+++ b/server/queues/processors/RevisionsProcessor.ts
@@ -17,7 +17,7 @@ export default class RevisionsProcessor extends BaseProcessor {
case "documents.publish":
case "documents.update.debounced":
case "documents.update": {
- if (event.name === "documents.update" && !event.data.done) {
+ if (event.name === "documents.update" && !event.data?.done) {
return;
}
diff --git a/server/queues/processors/WebsocketsProcessor.ts b/server/queues/processors/WebsocketsProcessor.ts
index dda4ee0cb0..f643797540 100644
--- a/server/queues/processors/WebsocketsProcessor.ts
+++ b/server/queues/processors/WebsocketsProcessor.ts
@@ -53,7 +53,7 @@ export default class WebsocketsProcessor {
}
if (
event.name === "documents.create" &&
- event.data.source === "import"
+ event.data?.source === "import"
) {
return;
}
@@ -94,18 +94,25 @@ export default class WebsocketsProcessor {
const channels = await this.getDocumentEventChannels(event, document);
// We need to add the collection channel to let the members update the doc structure.
- channels.push(`collection-${event.collectionId}`);
+ // In case draft is detached from a collection, fallback to previous attribute to get the right one.
+ const collectionId =
+ event.collectionId ?? event.changes?.previous.collectionId;
+
+ channels.push(`collection-${collectionId}`);
return socketio.to(channels).emit(event.name, {
document: documentToPresent,
- collectionId: event.collectionId,
+ collectionId,
});
}
case "documents.unarchive": {
+ const srcCollectionId =
+ event.changes?.previous.collectionId ?? event.collectionId;
+
const [document, srcCollection] = await Promise.all([
Document.findByPk(event.documentId, { paranoid: false }),
- Collection.findByPk(event.data.sourceCollectionId, {
+ Collection.findByPk(srcCollectionId, {
paranoid: false,
}),
]);
diff --git a/server/queues/tasks/DetachDraftsFromCollectionTask.ts b/server/queues/tasks/DetachDraftsFromCollectionTask.ts
index b6b720ebfb..c23a5763ef 100644
--- a/server/queues/tasks/DetachDraftsFromCollectionTask.ts
+++ b/server/queues/tasks/DetachDraftsFromCollectionTask.ts
@@ -3,6 +3,7 @@ import documentMover from "@server/commands/documentMover";
import { Collection, Document, User } from "@server/models";
import { sequelize } from "@server/storage/database";
import BaseTask from "./BaseTask";
+import { createContext } from "@server/context";
type Props = {
collectionId: string;
@@ -39,13 +40,16 @@ export default class DetachDraftsFromCollectionTask extends BaseTask {
});
return sequelize.transaction(async (transaction) => {
+ const ctx = createContext({
+ user: actor,
+ ip: props.ip,
+ transaction,
+ });
+
for (const document of documents) {
- await documentMover({
+ await documentMover(ctx, {
document,
- user: actor,
- ip: props.ip,
collectionId: null,
- transaction,
});
}
});
diff --git a/server/queues/tasks/DocumentImportTask.ts b/server/queues/tasks/DocumentImportTask.ts
index ec84028b60..a0339190fc 100644
--- a/server/queues/tasks/DocumentImportTask.ts
+++ b/server/queues/tasks/DocumentImportTask.ts
@@ -44,15 +44,17 @@ export default class DocumentImportTask extends BaseTask {
transaction,
});
+ const ctx = createContext({ user, transaction, ip });
+
const { text, state, title, icon } = await documentImporter({
user,
fileName: sourceMetadata.fileName,
mimeType: sourceMetadata.mimeType,
content,
- ctx: createContext({ user, transaction, ip }),
+ ctx,
});
- return documentCreator({
+ return documentCreator(ctx, {
sourceMetadata,
title,
icon,
@@ -61,8 +63,6 @@ export default class DocumentImportTask extends BaseTask {
publish,
collectionId,
parentDocumentId,
- user,
- ctx: createContext({ user, transaction, ip }),
});
});
return { documentId: document.id };
diff --git a/server/queues/tasks/DocumentPublishedNotificationsTask.test.ts b/server/queues/tasks/DocumentPublishedNotificationsTask.test.ts
index 17a513ee60..406aa7f313 100644
--- a/server/queues/tasks/DocumentPublishedNotificationsTask.test.ts
+++ b/server/queues/tasks/DocumentPublishedNotificationsTask.test.ts
@@ -31,9 +31,6 @@ describe("documents.publish", () => {
collectionId: document.collectionId!,
teamId: document.teamId,
actorId: document.createdById,
- data: {
- title: document.title,
- },
ip,
});
expect(spy).not.toHaveBeenCalled();
@@ -55,9 +52,6 @@ describe("documents.publish", () => {
collectionId: document.collectionId!,
teamId: document.teamId,
actorId: document.createdById,
- data: {
- title: document.title,
- },
ip,
});
expect(spy).toHaveBeenCalled();
@@ -95,9 +89,6 @@ describe("documents.publish", () => {
collectionId: document.collectionId!,
teamId: document.teamId,
actorId: document.createdById,
- data: {
- title: document.title,
- },
ip,
});
expect(spy).toHaveBeenCalledTimes(1);
@@ -124,9 +115,6 @@ describe("documents.publish", () => {
collectionId: document.collectionId!,
teamId: document.teamId,
actorId: document.createdById,
- data: {
- title: document.title,
- },
ip,
});
expect(spy).not.toHaveBeenCalled();
diff --git a/server/queues/tasks/ImportTask.ts b/server/queues/tasks/ImportTask.ts
index 96351f4c95..9d3ecbcd2d 100644
--- a/server/queues/tasks/ImportTask.ts
+++ b/server/queues/tasks/ImportTask.ts
@@ -435,7 +435,7 @@ export default abstract class ImportTask extends BaseTask {
);
}
- const document = await documentCreator({
+ const document = await documentCreator(ctx, {
sourceMetadata: {
fileName: path.basename(item.path),
mimeType: item.mimeType,
@@ -455,8 +455,6 @@ export default abstract class ImportTask extends BaseTask {
publishedAt: item.updatedAt ?? item.createdAt ?? new Date(),
parentDocumentId: item.parentDocumentId,
importId: fileOperation.id,
- user,
- ctx: createContext({ user, transaction }),
});
documents.set(item.id, document);
diff --git a/server/routes/api/documents/documents.test.ts b/server/routes/api/documents/documents.test.ts
index 1dcd71967f..0c0f8406ff 100644
--- a/server/routes/api/documents/documents.test.ts
+++ b/server/routes/api/documents/documents.test.ts
@@ -33,7 +33,7 @@ import {
buildGroup,
buildAdmin,
} from "@server/test/factories";
-import { getTestServer } from "@server/test/support";
+import { getTestServer, withAPIContext } from "@server/test/support";
const server = getTestServer();
@@ -91,7 +91,7 @@ describe("#documents.info", () => {
userId: user.id,
teamId: user.teamId,
});
- await document.archive(user);
+ await withAPIContext(user, (ctx) => document.archiveWithCtx(ctx));
const res = await server.post("/api/documents.info", {
body: {
token: user.getJwtToken(),
@@ -379,7 +379,7 @@ describe("#documents.info", () => {
teamId: document.teamId,
userId: user.id,
});
- await document.archive(user);
+ await withAPIContext(user, (ctx) => document.archiveWithCtx(ctx));
const res = await server.post("/api/documents.info", {
body: {
shareId: share.id,
@@ -553,7 +553,7 @@ describe("#documents.export", () => {
userId: user.id,
teamId: user.teamId,
});
- await document.archive(user);
+ await withAPIContext(user, (ctx) => document.archiveWithCtx(ctx));
const res = await server.post("/api/documents.export", {
body: {
token: user.getJwtToken(),
@@ -774,7 +774,7 @@ describe("#documents.list", () => {
collectionId: collection.id,
}),
]);
- await docs[0].archive(user);
+ await withAPIContext(user, (ctx) => docs[0].archiveWithCtx(ctx));
const res = await server.post("/api/documents.list", {
body: {
token: user.getJwtToken(),
@@ -815,7 +815,10 @@ describe("#documents.list", () => {
collectionId: collections[1].id,
}),
]);
- await Promise.all([docs[0].archive(user), docs[1].archive(user)]);
+ await Promise.all([
+ withAPIContext(user, (ctx) => docs[0].archiveWithCtx(ctx)),
+ withAPIContext(user, (ctx) => docs[1].archiveWithCtx(ctx)),
+ ]);
const res = await server.post("/api/documents.list", {
body: {
token: user.getJwtToken(),
@@ -1664,7 +1667,7 @@ describe("#documents.search", () => {
text: "search term",
teamId: user.teamId,
});
- await document.archive(user);
+ await withAPIContext(user, (ctx) => document.archiveWithCtx(ctx));
const res = await server.post("/api/documents.search", {
body: {
token: user.getJwtToken(),
@@ -1684,7 +1687,7 @@ describe("#documents.search", () => {
text: "search term",
teamId: user.teamId,
});
- await document.archive(user);
+ await withAPIContext(user, (ctx) => document.archiveWithCtx(ctx));
const res = await server.post("/api/documents.search", {
body: {
token: user.getJwtToken(),
@@ -2091,7 +2094,7 @@ describe("#documents.archived", () => {
userId: user.id,
teamId: user.teamId,
});
- await document.archive(user);
+ await withAPIContext(user, (ctx) => document.archiveWithCtx(ctx));
const res = await server.post("/api/documents.archived", {
body: {
token: user.getJwtToken(),
@@ -2127,8 +2130,12 @@ describe("#documents.archived", () => {
]);
await Promise.all([
- documentInFirstCollection.archive(user),
- documentInSecondCollection.archive(user),
+ withAPIContext(user, (ctx) =>
+ documentInFirstCollection.archiveWithCtx(ctx)
+ ),
+ withAPIContext(user, (ctx) =>
+ documentInSecondCollection.archiveWithCtx(ctx)
+ ),
]);
const res = await server.post("/api/documents.archived", {
@@ -2166,8 +2173,12 @@ describe("#documents.archived", () => {
]);
await Promise.all([
- documentInFirstCollection.archive(user),
- documentInSecondCollection.archive(user),
+ withAPIContext(user, (ctx) =>
+ documentInFirstCollection.archiveWithCtx(ctx)
+ ),
+ withAPIContext(user, (ctx) =>
+ documentInSecondCollection.archiveWithCtx(ctx)
+ ),
]);
const res = await server.post("/api/documents.archived", {
@@ -2186,7 +2197,7 @@ describe("#documents.archived", () => {
userId: user.id,
teamId: user.teamId,
});
- await document.archive(user);
+ await withAPIContext(user, (ctx) => document.archiveWithCtx(ctx));
const res = await server.post("/api/documents.archived", {
body: {
token: user.getJwtToken(),
@@ -2223,7 +2234,7 @@ describe("#documents.archived", () => {
teamId: user.teamId,
collectionId: collection.id,
});
- await document.archive(user);
+ await withAPIContext(user, (ctx) => document.archiveWithCtx(ctx));
const res = await server.post("/api/documents.archived", {
body: {
token: user.getJwtToken(),
@@ -3125,7 +3136,7 @@ describe("#documents.restore", () => {
userId: user.id,
teamId: user.teamId,
});
- await document.archive(user);
+ await withAPIContext(user, (ctx) => document.archiveWithCtx(ctx));
const res = await server.post("/api/documents.restore", {
body: {
token: user.getJwtToken(),
@@ -3149,8 +3160,8 @@ describe("#documents.restore", () => {
collectionId: document.collectionId,
parentDocumentId: document.id,
});
- await childDocument.archive(user);
- await document.archive(user);
+ await withAPIContext(user, (ctx) => childDocument.archiveWithCtx(ctx));
+ await withAPIContext(user, (ctx) => document.archiveWithCtx(ctx));
const res = await server.post("/api/documents.restore", {
body: {
token: user.getJwtToken(),
@@ -3979,7 +3990,7 @@ describe("#documents.update", () => {
userId: user.id,
teamId: user.teamId,
});
- await document.archive(user);
+ await withAPIContext(user, (ctx) => document.archiveWithCtx(ctx));
const res = await server.post("/api/documents.update", {
body: {
token: user.getJwtToken(),
@@ -4544,7 +4555,7 @@ describe("#documents.unpublish", () => {
teamId: user.teamId,
parentDocumentId: document.id,
});
- await child.archive(user);
+ await withAPIContext(user, (ctx) => child.archiveWithCtx(ctx));
const res = await server.post("/api/documents.unpublish", {
body: {
token: user.getJwtToken(),
@@ -4622,7 +4633,7 @@ describe("#documents.unpublish", () => {
userId: user.id,
teamId: user.teamId,
});
- await document.archive(user);
+ await withAPIContext(user, (ctx) => document.archiveWithCtx(ctx));
const res = await server.post("/api/documents.unpublish", {
body: {
token: user.getJwtToken(),
diff --git a/server/routes/api/documents/documents.ts b/server/routes/api/documents/documents.ts
index a4cff61fad..376e449d5e 100644
--- a/server/routes/api/documents/documents.ts
+++ b/server/routes/api/documents/documents.ts
@@ -912,45 +912,19 @@ router.post(
if (document.deletedAt && document.isWorkspaceTemplate) {
authorize(user, "restore", document);
-
- await document.restore({ transaction });
- await Event.createFromContext(ctx, {
- name: "documents.restore",
- documentId: document.id,
- collectionId: document.collectionId,
- data: {
- title: document.title,
- },
- });
+ await document.restoreWithCtx(ctx, { name: "restore" });
} else if (document.deletedAt) {
authorize(user, "restore", document);
authorize(user, "updateDocument", destCollection);
// restore a previously deleted document
- await document.restoreTo(destCollectionId!, { transaction, user }); // destCollectionId is guaranteed to be defined here
- await Event.createFromContext(ctx, {
- name: "documents.restore",
- documentId: document.id,
- collectionId: document.collectionId,
- data: {
- title: document.title,
- },
- });
+ await document.restoreTo(ctx, { collectionId: destCollectionId! }); // destCollectionId is guaranteed to be defined here
} else if (document.archivedAt) {
authorize(user, "unarchive", document);
authorize(user, "updateDocument", destCollection);
// restore a previously archived document
- await document.restoreTo(destCollectionId!, { transaction, user }); // destCollectionId is guaranteed to be defined here
- await Event.createFromContext(ctx, {
- name: "documents.unarchive",
- documentId: document.id,
- collectionId: document.collectionId,
- data: {
- title: document.title,
- sourceCollectionId,
- },
- });
+ await document.restoreTo(ctx, { collectionId: destCollectionId! }); // destCollectionId is guaranteed to be defined here
} else if (revisionId) {
// restore a document to a specific revision
authorize(user, "update", document);
@@ -958,16 +932,7 @@ router.post(
authorize(document, "restore", revision);
document.restoreFromRevision(revision);
- await document.save({ transaction });
-
- await Event.createFromContext(ctx, {
- name: "documents.restore",
- documentId: document.id,
- collectionId: document.collectionId,
- data: {
- title: document.title,
- },
- });
+ await document.saveWithCtx(ctx, undefined, { name: "restore" });
} else {
assertPresent(revisionId, "revisionId is required");
}
@@ -1204,33 +1169,19 @@ router.post(
authorize(user, "createTemplate", user.team);
}
- const document = await Document.create(
- {
- editorVersion: original.editorVersion,
- collectionId,
- teamId: user.teamId,
- publishedAt: publish ? new Date() : null,
- lastModifiedById: user.id,
- createdById: user.id,
- template: true,
- icon: original.icon,
- color: original.color,
- title: original.title,
- text: original.text,
- content: original.content,
- },
- {
- transaction,
- }
- );
- await Event.createFromContext(ctx, {
- name: "documents.create",
- documentId: document.id,
- collectionId: document.collectionId,
- data: {
- title: document.title,
- template: true,
- },
+ const document = await Document.createWithCtx(ctx, {
+ editorVersion: original.editorVersion,
+ collectionId,
+ teamId: user.teamId,
+ publishedAt: publish ? new Date() : null,
+ lastModifiedById: user.id,
+ createdById: user.id,
+ template: true,
+ icon: original.icon,
+ color: original.color,
+ title: original.title,
+ text: original.text,
+ content: original.content,
});
// reload to get all of the data needed to present (user, collection etc)
@@ -1307,7 +1258,6 @@ router.post(
document = await documentUpdater(ctx, {
document,
- user,
...input,
publish,
collectionId,
@@ -1364,15 +1314,13 @@ router.post(
}
}
- const response = await documentDuplicator({
- user,
+ const response = await documentDuplicator(ctx, {
collection,
document,
title,
publish,
recursive,
parentDocumentId,
- ctx,
});
ctx.body = {
@@ -1427,14 +1375,11 @@ router.post(
}
}
- const { documents, collectionChanged } = await documentMover({
- user,
+ const { documents, collectionChanged } = await documentMover(ctx, {
document,
collectionId: collectionId ?? null,
parentDocumentId,
index,
- ip: ctx.request.ip,
- transaction,
});
ctx.body = {
@@ -1467,15 +1412,7 @@ router.post(
});
authorize(user, "archive", document);
- await document.archive(user, { transaction });
- await Event.createFromContext(ctx, {
- name: "documents.archive",
- documentId: document.id,
- collectionId: document.collectionId,
- data: {
- title: document.title,
- },
- });
+ await document.archiveWithCtx(ctx);
ctx.body = {
data: await presentDocument(ctx, document),
@@ -1516,14 +1453,6 @@ router.post(
authorize(user, "delete", document);
await document.delete(user);
- await Event.createFromContext(ctx, {
- name: "documents.delete",
- documentId: document.id,
- collectionId: document.collectionId,
- data: {
- title: document.title,
- },
- });
}
ctx.body = {
@@ -1536,35 +1465,32 @@ router.post(
"documents.unpublish",
auth(),
validate(T.DocumentsUnpublishSchema),
+ transaction(),
async (ctx: APIContext) => {
const { id, detach } = ctx.input.body;
const { user } = ctx.state.auth;
+ const { transaction } = ctx.state;
const document = await Document.findByPk(id, {
userId: user.id,
});
authorize(user, "unpublish", document);
- const childDocumentIds = await document.findAllChildDocumentIds({
- archivedAt: {
- [Op.eq]: null,
+ const childDocumentIds = await document.findAllChildDocumentIds(
+ {
+ archivedAt: {
+ [Op.eq]: null,
+ },
},
- });
+ { transaction }
+ );
if (childDocumentIds.length > 0) {
throw InvalidRequestError(
"Cannot unpublish document with child documents"
);
}
- // detaching would unset collectionId from document, so save a ref to the affected collectionId.
- const collectionId = document.collectionId;
-
- await document.unpublish(user, { detach });
- await Event.createFromContext(ctx, {
- name: "documents.unpublish",
- documentId: document.id,
- collectionId,
- });
+ await document.unpublishWithCtx(ctx, { detach });
ctx.body = {
data: await presentDocument(ctx, document),
@@ -1713,7 +1639,7 @@ router.post(
authorize(user, "read", templateDocument);
}
- const document = await documentCreator({
+ const document = await documentCreator(ctx, {
id,
title,
text: !isNil(text)
@@ -1728,9 +1654,7 @@ router.post(
templateDocument,
template,
fullWidth,
- user,
editorVersion,
- ctx,
});
if (collection) {
diff --git a/server/routes/api/groups/schema.ts b/server/routes/api/groups/schema.ts
index aee4c07762..319693fcef 100644
--- a/server/routes/api/groups/schema.ts
+++ b/server/routes/api/groups/schema.ts
@@ -51,7 +51,10 @@ export const GroupsCreateSchema = z.object({
/** Group name */
name: z.string(),
/** Group description */
- description: z.string().max(GroupValidation.maxDescriptionLength).optional(),
+ description: z
+ .string()
+ .max(GroupValidation.maxDescriptionLength)
+ .optional(),
/** Optionally link this group to an external source. */
externalId: z.string().optional(),
/** Whether mentions are disabled for this group */
@@ -66,7 +69,10 @@ export const GroupsUpdateSchema = z.object({
/** Group name */
name: z.string().optional(),
/** Group description */
- description: z.string().max(GroupValidation.maxDescriptionLength).optional(),
+ description: z
+ .string()
+ .max(GroupValidation.maxDescriptionLength)
+ .optional(),
/** Optionally link this group to an external source. */
externalId: z.string().optional(),
/** Whether mentions are disabled for this group */
diff --git a/server/routes/app.ts b/server/routes/app.ts
index 91592c54aa..867cdb1797 100644
--- a/server/routes/app.ts
+++ b/server/routes/app.ts
@@ -195,7 +195,8 @@ export const renderShare = async (ctx: Context, next: Next) => {
}
// Allow shares to be embedded in iframes on other websites unless prevented by team preference
- const preventEmbedding = team?.getPreference(TeamPreference.PreventDocumentEmbedding) ?? false;
+ const preventEmbedding =
+ team?.getPreference(TeamPreference.PreventDocumentEmbedding) ?? false;
if (!preventEmbedding) {
ctx.remove("X-Frame-Options");
}
diff --git a/server/types.ts b/server/types.ts
index 21c98fdd27..f34315b468 100644
--- a/server/types.ts
+++ b/server/types.ts
@@ -202,25 +202,19 @@ export type DocumentEvent = BaseEvent &
| "documents.restore";
documentId: string;
collectionId: string;
- data: {
- title: string;
+ data?: {
source?: "import";
};
}
| {
name: "documents.unpublish";
documentId: string;
- collectionId: string;
+ collectionId?: string;
}
| {
name: "documents.unarchive";
documentId: string;
collectionId: string;
- data: {
- title: string;
- /** Id of collection from which the document is unarchived */
- sourceCollectionId: string;
- };
}
| {
name:
@@ -230,9 +224,7 @@ export type DocumentEvent = BaseEvent &
documentId: string;
collectionId: string;
createdAt: string;
- data: {
- title: string;
- autosave: boolean;
+ data?: {
done: boolean;
};
}
@@ -241,10 +233,6 @@ export type DocumentEvent = BaseEvent &
documentId: string;
collectionId: string;
createdAt: string;
- data: {
- title: string;
- previousTitle: string;
- };
}
| DocumentMovedEvent
);
diff --git a/server/utils/oauth.test.ts b/server/utils/oauth.test.ts
index 8a16d66bb7..03ac31fc7e 100644
--- a/server/utils/oauth.test.ts
+++ b/server/utils/oauth.test.ts
@@ -3,9 +3,9 @@ import OAuthClient from "./oauth";
class MinimalOAuthClient extends OAuthClient {
endpoints = {
- authorize: 'http://example.com/authorize',
- token: 'http://example.com/token',
- userinfo: 'http://example.com/userinfo',
+ authorize: "http://example.com/authorize",
+ token: "http://example.com/token",
+ userinfo: "http://example.com/userinfo",
};
}
@@ -15,17 +15,17 @@ beforeEach(() => {
describe("userInfo", () => {
it("should work with empty-body 401 Unauthorized responses", async () => {
- fetchMock.mockResponseOnce('', {
+ fetchMock.mockResponseOnce("", {
status: 401,
- statusText: 'unauthorized',
+ statusText: "unauthorized",
});
- const client = new MinimalOAuthClient('clientid', 'clientsecret');
+ const client = new MinimalOAuthClient("clientid", "clientsecret");
try {
expect.assertions(1);
- await client.userInfo('token');
+ await client.userInfo("token");
} catch (e) {
- expect(e.id).toBe('authentication_required');
+ expect(e.id).toBe("authentication_required");
}
});
});
diff --git a/server/validation.test.ts b/server/validation.test.ts
index aab1d68f79..fe92ac28e5 100644
--- a/server/validation.test.ts
+++ b/server/validation.test.ts
@@ -8,14 +8,16 @@ describe("#ValidateKey.isValid", () => {
ValidateKey.isValid(`${Buckets.uploads}/${randomUUID()}/${randomUUID()}`)
).toBe(false);
expect(
- ValidateKey.isValid(`${Buckets.uploads}/${randomUUID()}/${randomUUID()}/foo/bar`)
+ ValidateKey.isValid(
+ `${Buckets.uploads}/${randomUUID()}/${randomUUID()}/foo/bar`
+ )
).toBe(false);
});
it("should return false if the first key component is not a valid bucket", () => {
- expect(ValidateKey.isValid(`foo/${randomUUID()}/${randomUUID()}/bar.png`)).toBe(
- false
- );
+ expect(
+ ValidateKey.isValid(`foo/${randomUUID()}/${randomUUID()}/bar.png`)
+ ).toBe(false);
});
it("should return false if second and third key components are not UUID", () => {
@@ -29,10 +31,14 @@ describe("#ValidateKey.isValid", () => {
it("should return true successfully validating key", () => {
expect(
- ValidateKey.isValid(`${Buckets.public}/${randomUUID()}/${randomUUID()}/foo.png`)
+ ValidateKey.isValid(
+ `${Buckets.public}/${randomUUID()}/${randomUUID()}/foo.png`
+ )
).toBe(true);
expect(
- ValidateKey.isValid(`${Buckets.uploads}/${randomUUID()}/${randomUUID()}/foo.png`)
+ ValidateKey.isValid(
+ `${Buckets.uploads}/${randomUUID()}/${randomUUID()}/foo.png`
+ )
).toBe(true);
expect(
ValidateKey.isValid(`${Buckets.avatars}/${randomUUID()}/${randomUUID()}`)