85 lines
4.4 KiB
TypeScript
85 lines
4.4 KiB
TypeScript
// 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, string | number>) => 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,
|
|
})
|
|
}
|
|
}
|