Ctrl K
ComunidadGuías…

server/commands/subscriptionCreator.ts

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

Archivo original ↗Preguntar a KiBot
import { type InferAttributes, QueryTypes, type Transaction } from "sequelize";
import { SubscriptionType } from "@shared/types";
import { createContext } from "@server/context";
import type { Document, User } from "@server/models";
import { Subscription, Event } from "@server/models";
import { sequelize } from "@server/storage/database";
import type { APIContext, DocumentEvent, RevisionEvent } from "@server/types";

type Props = {
  /** The request context, which also contains the user creating the subscription */
  ctx: APIContext;
  /** The document to subscribe to */
  documentId?: string;
  /** The collection to subscribe to */
  collectionId?: string;
  /** Event to subscribe to */
  event: SubscriptionType;
  /** Whether the subscription should be restored if it exists in a deleted state  */
  resubscribe?: boolean;
};

/** A subscription row with a flag reporting whether the upsert inserted it. */
type SubscriptionRow = InferAttributes<Subscription> & { inserted: boolean };

/**
 * This command creates a subscription of a user to a document.
 *
 * @returns The subscription that was created
 */
export default async function subscriptionCreator({
  ctx,
  documentId,
  collectionId,
  event,
  resubscribe = true,
}: Props): Promise<Subscription> {
  const { user } = ctx.state.auth;

  let rows: SubscriptionRow[];
  const now = new Date();
  if (documentId) {
    rows = await sequelize.query<SubscriptionRow>(
      `INSERT INTO subscriptions ("id", "userId", "documentId", "event", "createdAt", "updatedAt")
       VALUES (gen_random_uuid(), :userId, :documentId, :event, :now, :now)
       ON CONFLICT ("userId", "documentId", "event")
       DO UPDATE SET "updatedAt" = EXCLUDED."updatedAt"
       RETURNING *, (xmax = 0) AS "inserted"`,
      {
        replacements: {
          userId: user.id,
          documentId,
          event,
          now,
        },
        type: QueryTypes.SELECT,
        transaction: ctx.state.transaction,
      }
    );
  } else if (collectionId) {
    rows = await sequelize.query<SubscriptionRow>(
      `INSERT INTO subscriptions ("id", "userId", "collectionId", "event", "createdAt", "updatedAt")
       VALUES (gen_random_uuid(), :userId, :collectionId, :event, :now, :now)
       ON CONFLICT ("userId", "collectionId", "event")
       DO UPDATE SET "updatedAt" = EXCLUDED."updatedAt"
       RETURNING *, (xmax = 0) AS "inserted"`,
      {
        replacements: {
          userId: user.id,
          collectionId,
          event,
          now,
        },
        type: QueryTypes.SELECT,
        transaction: ctx.state.transaction,
      }
    );
  } else {
    throw new Error("Either documentId or collectionId must be provided");
  }

  if (!rows || rows.length === 0) {
    throw new Error("Failed to create or find subscription");
  }

  // xmax is zero only for a row this statement inserted.
  const { inserted, ...row } = rows[0];
  const subscription = Subscription.build(row, {
    isNewRecord: false,
    include: [],
    raw: true,
  });

  if (inserted) {
    await Event.createFromContext(ctx, {
      name: "subscriptions.create",
      modelId: subscription.id,
      userId: subscription.userId,
      collectionId: subscription.collectionId,
      documentId: subscription.documentId,
    });
  }

  // If the subscription was deleted, then just restore the existing row.
  if (subscription.deletedAt && resubscribe) {
    await subscription.restoreWithCtx(ctx);
  }

  return subscription;
}

/**
 * Subscribe a single user to a document. The subscription is created if it
 * does not exist; an existing subscription that has been deleted is left as-is
 * so that the user's prior unsubscribe is respected.
 *
 * @param user The user to subscribe.
 * @param document The document to subscribe the user to.
 * @param event The event that triggered the subscription creation.
 * @param options.transaction An existing transaction to run within. When
 * subscribing many users in a row, callers should open a single transaction
 * and pass it in to avoid the overhead of one BEGIN/COMMIT per call.
 */
export const subscribeUserToDocument = async (
  user: User,
  document: Document,
  event: DocumentEvent | RevisionEvent,
  options: { transaction?: Transaction } = {}
): Promise<void> => {
  const run = (transaction: Transaction) =>
    subscriptionCreator({
      ctx: createContext({
        user,
        authType: event.authType,
        ip: event.ip,
        transaction,
      }),
      documentId: document.id,
      event: SubscriptionType.Document,
      resubscribe: false,
    });

  if (options.transaction) {
    await run(options.transaction);
    return;
  }

  await sequelize.transaction(run);
};

/**
 * Subscribe a batch of users to a document inside a single transaction.
 *
 * @param users The users to subscribe.
 * @param document The document to subscribe the users to.
 * @param event The event that triggered the subscription creation.
 */
export const subscribeUsersToDocument = async (
  users: User[],
  document: Document,
  event: DocumentEvent | RevisionEvent
): Promise<void> => {
  if (!users.length) {
    return;
  }

  await sequelize.transaction(async (transaction) => {
    for (const user of users) {
      await subscribeUserToDocument(user, document, event, { transaction });
    }
  });
};

/**
 * Create any new subscriptions that might be missing for collaborators in the
 * document on publish and revision creation. This does mean that there is a
 * short period of time where the user is not subscribed after editing until a
 * revision is created.
 *
 * @param document The document to create subscriptions for.
 * @param event The event that triggered the subscription creation.
 */
export const createSubscriptionsForDocument = async (
  document: Document,
  event: DocumentEvent | RevisionEvent
): Promise<void> => {
  const users = await document.collaborators();
  await subscribeUsersToDocument(users, document, event);
};