server/commands/documentUpdater.ts
Rama main · réplica de lectura del repositorio original.
import type { DocumentPreferences, TextEditMode } from "@shared/types";
import { DocumentConflictError, ValidationError } from "@server/errors";
import { Event, Document } from "@server/models";
import { DocumentHelper } from "@server/models/helpers/DocumentHelper";
import { TextHelper } from "@server/models/helpers/TextHelper";
import type { APIContext } from "@server/types";
type Props = {
/** The existing document */
document: Document;
/** The new title */
title?: string;
/** The document icon */
icon?: string | null;
/** The document icon's color */
color?: string | null;
/** The new text content */
text?: string;
/** Whether the editing session is complete */
done?: boolean;
/** The version of the client editor that was used */
editorVersion?: string;
/** The ID of the template that was used */
templateId?: string | null;
/** If the document should be displayed full-width on the screen */
fullWidth?: boolean;
/** Display preferences for the document, merged with existing values */
preferences?: DocumentPreferences | null;
/** Whether insights should be visible on the document */
insightsEnabled?: boolean;
/** The edit mode: "replace", "append", "prepend", or "patch" */
editMode?: TextEditMode;
/** The document revision the changes are based on, the update is rejected if it no longer matches */
lastRevision?: number;
/** The markdown text to find when using "patch" edit mode */
findText?: string;
/** Whether the document should be published to the collection */
publish?: boolean;
/** The ID of the collection to publish the document to */
collectionId?: string | null;
/** The reason the document is archived or deleted. */
deprecatedReason?: string | null;
};
/**
* This command updates document properties. To update collaborative text state
* use documentCollaborativeUpdater.
*
* @param Props The properties of the document to update
* @returns Document The updated document
*/
export default async function documentUpdater(
ctx: APIContext,
{
document,
title,
icon,
color,
text,
editorVersion,
templateId,
fullWidth,
preferences,
insightsEnabled,
editMode,
findText,
lastRevision,
publish,
collectionId,
deprecatedReason,
done,
}: Props
): Promise<Document> {
const { user } = ctx.state.auth;
const { transaction } = ctx.state;
const cId = collectionId || document.collectionId;
document.fill({
title: title?.trim(),
icon,
color,
editorVersion: editorVersion || undefined,
templateId: templateId || undefined,
fullWidth,
insightsEnabled,
});
if (preferences) {
document.preferences = {
...document.preferences,
...preferences,
};
}
if (text !== undefined) {
document = DocumentHelper.applyMarkdownToDocument(
document,
await TextHelper.replaceImagesWithAttachments(ctx, text, user, {
base64Only: true,
}),
editMode,
findText,
user.id
);
}
// Serialize concurrent updates to the same document by taking a row-level
// lock before writing. The wait is already bounded by the transaction's
// statement_timeout. When lastRevision is provided it becomes part of the
// predicate, so a document modified since that revision matches no row.
if (transaction) {
const locked = await Document.unscoped().findOne({
attributes: ["id"],
where: {
id: document.id,
...(lastRevision !== undefined && { revisionCount: lastRevision }),
},
transaction,
lock: transaction.LOCK.UPDATE,
paranoid: false,
});
if (!locked && lastRevision !== undefined) {
throw DocumentConflictError();
}
}
if (deprecatedReason !== undefined) {
// Reload under the lock so a concurrent restore cannot leave a stale note.
await document.reload({ transaction, paranoid: false });
if (document.isActive) {
throw ValidationError("The document must be archived or deleted");
}
document.deprecatedReason = deprecatedReason?.trim() || null;
// Keep the archive attribution and time shown in the banner unchanged.
if (document.changed("deprecatedReason")) {
await document.saveWithCtx(ctx, { silent: true });
}
return document;
}
const changed = document.changed();
const eventData = done !== undefined ? { done } : undefined;
const { collection } = document;
const event = {
name: "documents.update",
documentId: document.id,
collectionId: cId,
data: eventData,
};
if (publish && cId) {
if (!document.collectionId) {
document.collectionId = cId;
}
await document.publish(ctx, { collectionId: cId, data: eventData });
} else if (changed) {
document.lastModifiedById = user.id;
document.updatedBy = user;
await document.saveWithCtx(ctx, undefined, { data: eventData });
} else if (done) {
await Event.schedule({
...event,
actorId: user.id,
teamId: document.teamId,
});
}
// Publishing can move the document to another collection, and the save
// hooks can attach a collection that has no membership data. Reload in
// those cases so that policies stay accurate.
if (publish || document.collection !== collection) {
return await Document.findByPk(document.id, {
userId: user.id,
rejectOnEmpty: true,
transaction,
});
}
return document;
}