// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0 // A module event, delivered: `notes.NoteCreated` tells the project's other members that a note was added. // // The event is a record of the transaction that wrote the note (core/notes.ts), so a note that rolled back // tells nobody. The relay delivers at least once per subscriber. On the Postgres, MariaDB and MongoDB // profiles it hands the record to this subscriber inside its own transaction, bound as // `NOTIFICATIONS_REPOS` (JOB_REPOS), and records the delivery in that transaction too, so the inbox rows and // the claim commit together: exactly once. On Convex, delivery is at least once only; there, and after any // repeat, the record-derived `key` makes a second delivery find the row and add nothing. // The inbox, its read state and its rendering are the notifications kit's; a producer supplies only the // recipient, the occurrence, the text and a link inside the organization. // // Who is told: the project's named members (`project_memberships`, which a restricted project always has, // its creator first), never the author. A project open to the whole workspace has none, and telling every // member of the workspace about every note would be noise. import { type EventContext, internal } from "@conusai-kit/core" import { DATA_SOURCE } from "@conusai-kit/data" import { FILES_REGISTRY, FILES_SCOPE } from "@conusai-kit/files" import { createI18n, moduleCatalogues, negotiateLocale } from "@conusai-kit/i18n" import { deliverNotification, NOTIFICATIONS_REPOS } from "@conusai-kit/notifications" import { organizations, projectMemberships, projects, users, workspaces } from "@conusai-kit/tenancy" type NoteCreated = { orgId: string; workspaceId: string; projectId: string; noteId: string; createdBy: string } /** A message lookup relative to the module (`notification.created`), as the pages' label builders take. */ export type Translate = (key: string, values?: Record) => string /** The texts of the notification, named here so `__tests__/i18n.test.ts` checks them in both languages. */ export function notificationLabels(t: Translate, project: string) { return { created: t("notification.created", { project }) } } export async function notifyNoteCreated(event: { name: string; payload: NoteCreated }, ctx: EventContext) { const { payload } = event const tx = ctx.container.resolve(NOTIFICATIONS_REPOS) if (tx.scope.orgId !== payload.orgId) { throw internal(`a notes event of organization ${payload.orgId} was delivered in ${tx.scope.orgId}`) } const recipients: string[] = [] for (let cursor: string | undefined; ; ) { const page = await tx.of(projectMemberships).list({ where: [{ field: "projectId", op: "eq", value: payload.projectId }], ...(cursor === undefined ? {} : { cursor }), }) for (const row of page.items) if (row.userId !== payload.createdBy) recipients.push(row.userId) if (!page.hasMore || page.cursor === undefined) break cursor = page.cursor } if (recipients.length === 0) return // The link names the project by its slugs. A project is a workspace's row, so it is read in the // workspace's scope, as the relay's system actor: a read, outside the delivery's transaction. const relay = ctx.container.resolve(FILES_SCOPE) const inWorkspace = { level: "workspace" as const, actor: relay.actor, orgId: payload.orgId, workspaceId: payload.workspaceId, } const [org, workspace, project] = await Promise.all([ tx.global(organizations).require(payload.orgId), tx.of(workspaces).require(payload.workspaceId), ctx.container.resolve(DATA_SOURCE).forScope(inWorkspace).of(projects).require(payload.projectId), ]) const href = `/${org.slug}/${workspace.slug}/${project.slug}/notes/${payload.noteId}` const catalogues = moduleCatalogues(ctx.container.resolve(FILES_REGISTRY)) for (const recipientId of recipients) { const person = await tx.global(users).get(recipientId) const i18n = createI18n(negotiateLocale({ ...(person ? { userLocale: person.locale } : {}) }), catalogues) const labels = notificationLabels((key, values) => i18n.message(`notes.${key}`, values), project.name) // `custom` is the kit's code for an app's own copy, which it stores as written: in the recipient's language. await deliverNotification(tx, { recipientId, key: `notes:${payload.noteId}:created`, category: "notice", code: "custom", title: labels.created, href, }) } }