mirror of
https://github.com/outline/outline.git
synced 2026-06-13 03:14:59 +03:00
Move Document event writing to model layer (#9790)
* documents.restore, documents.unarchive * documents.templatize * documents.archive * documents.unpublish * documents.create, documents.update * documents.title_change event * documents.move * documents.delete * tsc, tests * tsc * Copilot feedback --------- Co-authored-by: Tom Moor <tom@getoutline.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Trans } from 'react-i18next';
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
export const Translations = () => (
|
||||
<>
|
||||
@@ -6,7 +6,9 @@ export const Translations = () => (
|
||||
<Trans defaults={`Paper size`} />
|
||||
<Trans defaults={`Ask AI "{{question}}"`} />
|
||||
<Trans defaults={`Are you sure you want to delete?`} />
|
||||
<Trans defaults={`Deleting this version of the document will permanently and irrevocably remove it from the history.`} />
|
||||
<Trans
|
||||
defaults={`Deleting this version of the document will permanently and irrevocably remove it from the history.`}
|
||||
/>
|
||||
<Trans defaults={`Format`} />
|
||||
<Trans defaults={`Add option`} />
|
||||
<Trans defaults={`Optional`} />
|
||||
@@ -21,35 +23,59 @@ export const Translations = () => (
|
||||
<Trans defaults={`Yes`} />
|
||||
<Trans defaults={`No`} />
|
||||
<Trans defaults={`Search or ask a question`} />
|
||||
<Trans defaults={`Invited {{roleName}} will not receive access to any collections or documents unless explicitly shared.`} />
|
||||
<Trans
|
||||
defaults={`Invited {{roleName}} will not receive access to any collections or documents unless explicitly shared.`}
|
||||
/>
|
||||
<Trans defaults={`Can view only what is explicitly shared`} />
|
||||
<Trans defaults={`SAML assertion was invalid or missing fields, please check your configuration`} />
|
||||
<Trans defaults={`AI generated answer based on related documents in your workspace`} />
|
||||
<Trans
|
||||
defaults={`SAML assertion was invalid or missing fields, please check your configuration`}
|
||||
/>
|
||||
<Trans
|
||||
defaults={`AI generated answer based on related documents in your workspace`}
|
||||
/>
|
||||
<Trans defaults={`References`} />
|
||||
<Trans defaults={`Enable AI answers to get direct answers to searched questions.`} />
|
||||
<Trans
|
||||
defaults={`Enable AI answers to get direct answers to searched questions.`}
|
||||
/>
|
||||
<Trans defaults={`Go to settings`} />
|
||||
<Trans defaults={`Where do I find the file?`} />
|
||||
<Trans defaults={`In a Confluence space, navigate to <em>Space Settings -> Manage space -> Export space</em> and choose to export as HTML with the "Normal Export" option.`} />
|
||||
<Trans defaults={`Drag and drop the zip file from Confluence's HTML export option, or click to upload`} />
|
||||
<Trans
|
||||
defaults={`In a Confluence space, navigate to <em>Space Settings -> Manage space -> Export space</em> and choose to export as HTML with the "Normal Export" option.`}
|
||||
/>
|
||||
<Trans
|
||||
defaults={`Drag and drop the zip file from Confluence's HTML export option, or click to upload`}
|
||||
/>
|
||||
<Trans defaults={`Guests`} />
|
||||
<Trans defaults={`New Attribute`} />
|
||||
<Trans defaults={`Attributes allow you to define data to be stored with your documents. They can be used to store custom properties, metadata, or any other structured information that is common across documents.`} />
|
||||
<Trans
|
||||
defaults={`Attributes allow you to define data to be stored with your documents. They can be used to store custom properties, metadata, or any other structured information that is common across documents.`}
|
||||
/>
|
||||
<Trans defaults={`Custom domain`} />
|
||||
<Trans defaults={`AI answers`} />
|
||||
<Trans defaults={`Use AI to directly answer searched questions using content in your workspace.`} />
|
||||
<Trans
|
||||
defaults={`Use AI to directly answer searched questions using content in your workspace.`}
|
||||
/>
|
||||
<Trans defaults={`API access`} />
|
||||
<Trans defaults={`Allow members to create API keys for programmatic access`} />
|
||||
<Trans
|
||||
defaults={`Allow members to create API keys for programmatic access`}
|
||||
/>
|
||||
<Trans defaults={`Public document embedding`} />
|
||||
<Trans defaults={`When enabled, publicly shared documents can be embedded in third-party websites`} />
|
||||
<Trans
|
||||
defaults={`When enabled, publicly shared documents can be embedded in third-party websites`}
|
||||
/>
|
||||
<Trans defaults={`Include previews in emails`} />
|
||||
<Trans defaults={`When enabled, email notifications will include content previews`} />
|
||||
<Trans
|
||||
defaults={`When enabled, email notifications will include content previews`}
|
||||
/>
|
||||
<Trans defaults={`Boolean`} />
|
||||
<Trans defaults={`Number`} />
|
||||
<Trans defaults={`Text`} />
|
||||
<Trans defaults={`List`} />
|
||||
<Trans defaults={`Could not load events`} />
|
||||
<Trans defaults={`Audit Log`} />
|
||||
<Trans defaults={`The audit log details the history of security related and other events across your knowledge base.`} />
|
||||
<Trans
|
||||
defaults={`The audit log details the history of security related and other events across your knowledge base.`}
|
||||
/>
|
||||
<Trans defaults={`IP address`} />
|
||||
<Trans defaults={`Actor`} />
|
||||
<Trans defaults={`Event`} />
|
||||
@@ -62,15 +88,25 @@ export const Translations = () => (
|
||||
<Trans defaults={`Sharing enabled`} />
|
||||
<Trans defaults={`Date archived`} />
|
||||
<Trans defaults={`Could not load collections`} />
|
||||
<Trans defaults={`Manage the permissions and settings of all collections in the knowledge base. As a workspace admin you can also administer private collections.`} />
|
||||
<Trans defaults={`Automatically index and search document content from {{appName}} inside <4>Glean</4> in realtime.`} />
|
||||
<Trans
|
||||
defaults={`Manage the permissions and settings of all collections in the knowledge base. As a workspace admin you can also administer private collections.`}
|
||||
/>
|
||||
<Trans
|
||||
defaults={`Automatically index and search document content from {{appName}} inside <4>Glean</4> in realtime.`}
|
||||
/>
|
||||
<Trans defaults={`API Endpoint`} />
|
||||
<Trans defaults={`API Secret`} />
|
||||
<Trans defaults={`Datasource`} />
|
||||
<Trans defaults={`Details of the current {{appName}} license. To arrange contract renewal as expiry or seat limits approach or increase licensed seats please contact your account manager or email <4>priority@getoutline.com</4>.`} />
|
||||
<Trans defaults={`Sorry, an answer could not be found in the collection, try widening your search.`} />
|
||||
<Trans defaults={`Sorry, an answer could not be found in the workspace, try widening your search.`} />
|
||||
<Trans
|
||||
defaults={`Details of the current {{appName}} license. To arrange contract renewal as expiry or seat limits approach or increase licensed seats please contact your account manager or email <4>priority@getoutline.com</4>.`}
|
||||
/>
|
||||
<Trans
|
||||
defaults={`Sorry, an answer could not be found in the collection, try widening your search.`}
|
||||
/>
|
||||
<Trans
|
||||
defaults={`Sorry, an answer could not be found in the workspace, try widening your search.`}
|
||||
/>
|
||||
<Trans defaults={`Looking for answers`} />
|
||||
<Trans defaults={`Answer to "{{ query }}"`} />
|
||||
</>
|
||||
)
|
||||
);
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -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<Document> {
|
||||
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<Document> {
|
||||
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)
|
||||
|
||||
@@ -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 }),
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -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<Document[]> {
|
||||
export default async function documentDuplicator(
|
||||
ctx: APIContext,
|
||||
{ document, collection, parentDocumentId, title, publish, recursive }: Props
|
||||
): Promise<Document[]> {
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -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<Result> {
|
||||
async function documentMover(
|
||||
ctx: APIContext,
|
||||
{
|
||||
document,
|
||||
collectionId = null,
|
||||
parentDocumentId = null,
|
||||
// convert undefined to null so parentId comparison treats them as equal
|
||||
index,
|
||||
}: Props
|
||||
): Promise<Result> {
|
||||
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;
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -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<Document> {
|
||||
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,
|
||||
|
||||
+3
-1
@@ -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
|
||||
|
||||
+91
-48
@@ -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<string, unknown>;
|
||||
}
|
||||
): Promise<this> => {
|
||||
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<Document>
|
||||
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<Document>
|
||||
) => {
|
||||
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" });
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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<string, number>();
|
||||
documentStructure.forEach((node, index) => {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -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<Props> {
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -44,15 +44,17 @@ export default class DocumentImportTask extends BaseTask<Props> {
|
||||
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<Props> {
|
||||
publish,
|
||||
collectionId,
|
||||
parentDocumentId,
|
||||
user,
|
||||
ctx: createContext({ user, transaction, ip }),
|
||||
});
|
||||
});
|
||||
return { documentId: document.id };
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -435,7 +435,7 @@ export default abstract class ImportTask extends BaseTask<Props> {
|
||||
);
|
||||
}
|
||||
|
||||
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<Props> {
|
||||
publishedAt: item.updatedAt ?? item.createdAt ?? new Date(),
|
||||
parentDocumentId: item.parentDocumentId,
|
||||
importId: fileOperation.id,
|
||||
user,
|
||||
ctx: createContext({ user, transaction }),
|
||||
});
|
||||
documents.set(item.id, document);
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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<T.DocumentsUnpublishReq>) => {
|
||||
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) {
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
+3
-15
@@ -202,25 +202,19 @@ export type DocumentEvent = BaseEvent<Document> &
|
||||
| "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<Document> &
|
||||
documentId: string;
|
||||
collectionId: string;
|
||||
createdAt: string;
|
||||
data: {
|
||||
title: string;
|
||||
autosave: boolean;
|
||||
data?: {
|
||||
done: boolean;
|
||||
};
|
||||
}
|
||||
@@ -241,10 +233,6 @@ export type DocumentEvent = BaseEvent<Document> &
|
||||
documentId: string;
|
||||
collectionId: string;
|
||||
createdAt: string;
|
||||
data: {
|
||||
title: string;
|
||||
previousTitle: string;
|
||||
};
|
||||
}
|
||||
| DocumentMovedEvent
|
||||
);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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()}`)
|
||||
|
||||
Reference in New Issue
Block a user