Ctrl K
ComunidadGuías…

server/commands/documentUpdater.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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;
}