ConusAI vendor v0.1.0-dev.20261027+9b1131fb
ci / gate (push) Waiting to run

This commit is contained in:
ConusAI vendor v0.1.0-dev.20261027+9b1131fb
2026-10-27 18:05:17 +00:00
commit 3102273ec7
538 changed files with 80203 additions and 0 deletions
+294
View File
@@ -0,0 +1,294 @@
// What the notes module does, as pure TypeScript over the ports in ports.ts. Every write is one
// transaction with the event that announces it, so the audit event and the usage fact of a change
// exist exactly when the change does (core/subscribers.ts reads the events).
//
// The two things this module does not own are the file and the permission to see it: an attachment
// is a row of the files module, reached through `getFile`, which answers NotFound for another
// tenant's file and for one that does not exist alike (spec §3.3).
import { conflict, forbidden, notFound, validationFailed } from "@conusai-kit/core"
import { type FileView, getFile } from "@conusai-kit/files"
import type { z } from "zod"
import {
ATTACHMENT_CONTENT_TYPES,
type AttachmentView,
attachmentAdded,
attachmentRemoved,
attachments,
MAX_ATTACHMENTS,
type NoteView,
noteCreated,
noteDeleted,
notes,
noteUpdated,
} from "../contract/index.js"
import { attachmentsEnabled, type NoteScope, type NotesDeps } from "./ports.js"
type NoteRow = z.output<typeof notes.schema>
const PAGE = 200
class AttachmentConcurrencyConflict extends Error {
constructor(readonly original: unknown) {
super("the note changed while another attachment was being added")
}
}
const ids = (scope: NoteScope) => ({
orgId: scope.orgId,
workspaceId: scope.workspaceId,
projectId: scope.projectId,
})
const noteView = (row: NoteRow, attachmentCount: number): NoteView => ({
noteId: row.id,
title: row.title,
body: row.body,
version: row.version,
createdBy: row.createdBy,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
attachmentCount,
})
function attachmentView(
row: { id: string; fileId: string; createdAt: string },
file: Pick<FileView, "filename" | "contentType" | "size">,
): AttachmentView {
return {
attachmentId: row.id,
fileId: row.fileId,
filename: file.filename,
contentType: file.contentType,
size: file.size,
attachedAt: row.createdAt,
}
}
export async function listNotes(
deps: NotesDeps,
scope: NoteScope,
options: { cursor?: string; limit?: number },
): Promise<{ notes: NoteView[]; cursor?: string; hasMore: boolean }> {
const repos = deps.data.forScope(scope)
const page = await repos.of(notes).list({
order: [{ field: "createdAt", direction: "desc" }],
...(options.limit === undefined ? {} : { limit: options.limit }),
...(options.cursor === undefined ? {} : { cursor: options.cursor }),
})
// The attachment counts of the page's notes in one read over the note index, not one count per
// note (up to 200 queries for one list).
const counts = new Map<string, number>()
if (page.items.length > 0) {
let linksCursor: string | undefined
do {
const attachmentsPage = await repos.of(attachments).list({
where: [{ field: "noteId", op: "in", values: page.items.map((row) => row.id) }],
limit: PAGE,
...(linksCursor === undefined ? {} : { cursor: linksCursor }),
})
for (const link of attachmentsPage.items) counts.set(link.noteId, (counts.get(link.noteId) ?? 0) + 1)
linksCursor = attachmentsPage.hasMore ? attachmentsPage.cursor : undefined
} while (linksCursor !== undefined)
}
return {
notes: page.items.map((row) => noteView(row, counts.get(row.id) ?? 0)),
...(page.cursor === undefined ? {} : { cursor: page.cursor }),
hasMore: page.hasMore,
}
}
async function attachmentRows(deps: NotesDeps, scope: NoteScope, noteId: string) {
const found = []
let cursor: string | undefined
do {
const page = await deps.data
.forScope(scope)
.of(attachments)
.list({
where: [{ field: "noteId", op: "eq", value: noteId }],
order: [{ field: "createdAt", direction: "asc" }],
limit: PAGE,
...(cursor === undefined ? {} : { cursor }),
})
found.push(...page.items)
cursor = page.hasMore ? page.cursor : undefined
} while (cursor !== undefined)
return found
}
export async function getNote(
deps: NotesDeps,
scope: NoteScope,
noteId: string,
): Promise<{ note: NoteView; attachments: AttachmentView[]; attachmentsEnabled: boolean }> {
// NotFound for another tenant's note and for one nobody wrote: the repository answers both the same.
const row = await deps.data.forScope(scope).of(notes).get(noteId)
if (!row) throw notFound("no such note")
const [links, enabled] = await Promise.all([attachmentRows(deps, scope, row.id), attachmentsEnabled(deps, scope)])
// The files are read together, not one after another: a note carries at most MAX_ATTACHMENTS, and
// a deleted one (before the relay removed its links) is not a reason to fail the page.
const files = await Promise.all(
links.map((link) =>
getFile(deps.files, scope, link.fileId).catch((error: unknown) => {
if ((error as { status?: number }).status === 404) return undefined
throw error
}),
),
)
const views: AttachmentView[] = []
for (const [at, link] of links.entries()) {
const file = files[at]
if (file) views.push(attachmentView(link, file))
}
return {
note: noteView(row, views.length),
attachments: views,
attachmentsEnabled: enabled,
}
}
export async function createNote(
deps: NotesDeps,
scope: NoteScope,
input: { title: string; body: string },
): Promise<NoteView> {
return deps.data.unitOfWork.run(scope, async (tx) => {
const row = await tx.of(notes).insert({ title: input.title, body: input.body, createdBy: scope.actor.userId })
await tx.publish(noteCreated, { ...ids(scope), noteId: row.id, createdBy: scope.actor.userId })
return noteView(row, 0)
})
}
export async function updateNote(
deps: NotesDeps,
scope: NoteScope,
input: { noteId: string; title: string; body: string; expectedVersion: number },
): Promise<NoteView> {
return deps.data.unitOfWork.run(scope, async (tx) => {
const current = await tx.of(notes).get(input.noteId)
if (!current) throw notFound("no such note")
// The repository answers 409 when the version is not the row's: two editors never overwrite each other silently.
const row = await tx.of(notes).update(current.id, { title: input.title, body: input.body }, input.expectedVersion)
await tx.publish(noteUpdated, {
...ids(scope),
noteId: row.id,
version: row.version,
updatedBy: scope.actor.userId,
})
const count = await tx.of(attachments).count({ where: [{ field: "noteId", op: "eq", value: row.id }] })
return noteView(row, count)
})
}
/**
* Deletes the note and its links. The files stay: they are the project's, they may be attached to
* another note, and removing one is `files.file.delete`'s decision, with its own permission.
*/
export async function deleteNote(deps: NotesDeps, scope: NoteScope, noteId: string): Promise<void> {
await deps.data.unitOfWork.run(scope, async (tx) => {
const row = await tx.of(notes).get(noteId)
if (!row) throw notFound("no such note")
let removed = 0
for (;;) {
const page = await tx.of(attachments).list({ where: [{ field: "noteId", op: "eq", value: row.id }], limit: PAGE })
for (const link of page.items) {
await tx.of(attachments).delete(link.id)
removed += 1
}
if (!page.hasMore) break
}
await tx.of(notes).delete(row.id)
await tx.publish(noteDeleted, {
...ids(scope),
noteId: row.id,
deletedBy: scope.actor.userId,
attachmentCount: removed,
})
})
}
/**
* Links an uploaded file to a note. Idempotent: attaching a file twice answers with the first link
* and publishes nothing, so a retried request audits once.
*/
export async function addAttachment(
deps: NotesDeps,
scope: NoteScope,
input: { noteId: string; fileId: string },
): Promise<AttachmentView> {
if (!(await attachmentsEnabled(deps, scope))) throw forbidden("attachments are switched off")
const file = await getFile(deps.files, scope, input.fileId)
// The files module accepts more than a note should carry (spreadsheets, say); this module's list is its own.
if (!(ATTACHMENT_CONTENT_TYPES as readonly string[]).includes(file.contentType)) {
throw validationFailed([{ path: "fileId", message: `${file.contentType} cannot be attached to a note` }])
}
// The count and insert are one transaction, but that alone does not serialize two transactions
// that attach different files to the same note. Optimistically update the note first: its
// expected-version write is the portable per-note lock across SQL, MongoDB and Convex. A loser
// retries the transaction and rechecks both idempotency and the quota against the winner's row.
for (let attempt = 0; ; attempt += 1) {
try {
return await deps.data.unitOfWork.run(scope, async (tx) => {
const noteRepo = tx.of(notes)
const note = await noteRepo.get(input.noteId)
if (!note) throw notFound("no such note")
const links = tx.of(attachments)
const existing = (
await links.list({
where: [
{ field: "noteId", op: "eq", value: note.id },
{ field: "fileId", op: "eq", value: file.fileId },
],
limit: 1,
})
).items[0]
if (existing) return attachmentView(existing, file)
try {
await noteRepo.update(note.id, {}, note.version)
} catch (error) {
if ((error as { status?: number }).status === 409) throw new AttachmentConcurrencyConflict(error)
throw error
}
if ((await links.count({ where: [{ field: "noteId", op: "eq", value: note.id }] })) >= MAX_ATTACHMENTS) {
throw conflict(`a note carries at most ${MAX_ATTACHMENTS} attachments`)
}
const link = await links.insert({ noteId: note.id, fileId: file.fileId, attachedBy: scope.actor.userId })
await tx.publish(attachmentAdded, {
...ids(scope),
noteId: note.id,
attachmentId: link.id,
fileId: file.fileId,
attachedBy: scope.actor.userId,
})
return attachmentView(link, file)
})
} catch (error) {
// A retry that loses the optimistic lock re-reads the winner's link or sees the full quota.
// Bound retries so sustained contention returns the repository's ordinary conflict response.
if (!(error instanceof AttachmentConcurrencyConflict)) throw error
if (attempt >= 7) throw error.original
}
}
}
export async function removeAttachment(
deps: NotesDeps,
scope: NoteScope,
input: { noteId: string; attachmentId: string },
): Promise<void> {
await deps.data.unitOfWork.run(scope, async (tx) => {
const link = await tx.of(attachments).get(input.attachmentId)
// The note id is part of the lookup, so a link of another note of this project is not found either.
if (!link || link.noteId !== input.noteId) throw notFound("no such attachment")
await tx.of(attachments).delete(link.id)
await tx.publish(attachmentRemoved, {
...ids(scope),
noteId: link.noteId,
attachmentId: link.id,
fileId: link.fileId,
removedBy: scope.actor.userId,
})
})
}
+39
View File
@@ -0,0 +1,39 @@
// The ports the notes module's use cases are written against, and the one it adds: the feature flags.
//
// The audit and usage sinks and the scope of an event delivery are the kernel's own tokens
// (`FILES_AUDIT`, `FILES_USAGE`, `FILES_SCOPE`): the worker binds them once for every module, so a
// module that audits and meters needs no binding of its own and no line in the worker.
import { type Container, type TenantScope, token } from "@conusai-kit/core"
import type { DataSource } from "@conusai-kit/data"
import type { FilesDeps } from "@conusai-kit/files"
import { ATTACHMENTS_FLAG, NOTES_FLAG_DEFAULTS } from "../contract/index.js"
/** A project scope: the only level a note can live at. */
export type NoteScope = TenantScope & { workspaceId: string; projectId: string }
export interface NotesDeps {
readonly data: DataSource
/** The files module's ports: an attachment is one of its files. */
readonly files: FilesDeps
readonly flags: NotesFlags
}
/** What a flag service answers for the project a call runs in. */
export interface NotesFlags {
enabled(flag: keyof typeof NOTES_FLAG_DEFAULTS, scope: NoteScope): boolean | Promise<boolean>
}
/**
* Bound by an app that evaluates flags per organization, project or user (the platform's flag
* service, through `@conusai-sdk/node-sdk/flags`). With nothing bound, a flag is its manifest default.
*/
export const NOTES_FLAGS = token<NotesFlags>("notes.flags")
export const defaultFlags: NotesFlags = { enabled: (flag) => NOTES_FLAG_DEFAULTS[flag] }
export const flagsOf = (container: Container): NotesFlags =>
container.has(NOTES_FLAGS) ? container.resolve(NOTES_FLAGS) : defaultFlags
export const attachmentsEnabled = async (deps: NotesDeps, scope: NoteScope): Promise<boolean> =>
deps.flags.enabled(ATTACHMENTS_FLAG, scope)
+165
View File
@@ -0,0 +1,165 @@
// What happens exactly once per change.
//
// The audit event and the usage fact hang off the outbox record the changing transaction wrote, and
// the relay delivers a record exactly once per subscriber, so a note created by a retried request
// is audited and metered once, and one whose process died before the relay ran still is, because
// the record committed with the row. The platform's audit and usage APIs are HTTP and cannot join
// the transaction: each call carries a key derived from the record, so a delivery the relay repeats
// after a crash is the same event to the platform and not a second one.
//
// Ids, counts and versions only: no title and no body in an audit event (spec §9.4).
import { type EventContext, internal } from "@conusai-kit/core"
import { DATA_SOURCE } from "@conusai-kit/data"
import { auditEventId, FILES_AUDIT, FILES_SCOPE, FILES_USAGE } from "@conusai-kit/files"
import {
ATTACHMENT_ADDED,
ATTACHMENT_REMOVED,
attachments,
NOTE_CREATED,
NOTE_DELETED,
NOTE_UPDATED,
NOTES_CREATED_METER,
} from "../contract/index.js"
interface InProject {
orgId: string
workspaceId: string
projectId: string
}
/**
* The organization a delivery belongs to comes from the container -- the scope the relay rebuilt
* from the outbox record's own `orgId` -- and is compared with the payload's: a record that named
* another tenant is refused here rather than written there.
*/
function assertSameOrg(ctx: EventContext, payload: InProject): void {
const ambient = ctx.container.resolve(FILES_SCOPE)
if (ambient.orgId !== payload.orgId) {
throw internal(`a notes event of organization ${payload.orgId} was delivered in ${ambient.orgId}`)
}
}
interface AuditShape<P extends InProject> {
action: string
actor: (payload: P) => string
target: (payload: P) => { id: string; type: string }
/** What makes two events of one action on one target two events: a version, a link's id. */
occurrence: (payload: P) => string
data: (payload: P) => Record<string, unknown>
}
/** A subscriber that writes one audit event per record. */
export function auditing<P extends InProject>(shape: AuditShape<P>) {
return async (event: { name: string; payload: P }, ctx: EventContext): Promise<void> => {
const { payload } = event
assertSameOrg(ctx, payload)
await ctx.container.resolve(FILES_AUDIT).log(shape.action, {
actor: { kind: "human", id: shape.actor(payload) },
target: shape.target(payload),
event_id: await auditEventId(shape.action, shape.occurrence(payload)),
data: { ...shape.data(payload), projectId: payload.projectId },
})
}
}
type Payloads = {
created: InProject & { noteId: string; createdBy: string }
updated: InProject & { noteId: string; version: number; updatedBy: string }
deleted: InProject & { noteId: string; deletedBy: string; attachmentCount: number }
added: InProject & { noteId: string; attachmentId: string; fileId: string; attachedBy: string }
removed: InProject & { noteId: string; attachmentId: string; fileId: string; removedBy: string }
}
// The organization has to be in the target or the in-app viewer could not show one
// organization's events without another's (apps/web/src/server/audit.ts): a resource target is
// `<org id>/<resource id>` here, the id being what an outbox payload carries; the transport
// writes the same rule with the slug it has, and the viewer matches either.
const note = (payload: InProject & { noteId: string }) => ({ id: `${payload.orgId}/${payload.noteId}`, type: "note" })
const auditCreated = auditing<Payloads["created"]>({
action: NOTE_CREATED,
actor: (p) => p.createdBy,
target: note,
occurrence: (p) => p.noteId,
data: () => ({}),
})
const auditUpdated = auditing<Payloads["updated"]>({
action: NOTE_UPDATED,
actor: (p) => p.updatedBy,
target: note,
occurrence: (p) => `${p.noteId}:${p.version}`,
data: (p) => ({ version: p.version }),
})
const auditDeleted = auditing<Payloads["deleted"]>({
action: NOTE_DELETED,
actor: (p) => p.deletedBy,
target: note,
occurrence: (p) => p.noteId,
data: (p) => ({ attachmentCount: p.attachmentCount }),
})
const auditAdded = auditing<Payloads["added"]>({
action: ATTACHMENT_ADDED,
actor: (p) => p.attachedBy,
target: note,
occurrence: (p) => p.attachmentId,
data: (p) => ({ attachmentId: p.attachmentId, fileId: p.fileId }),
})
const auditRemoved = auditing<Payloads["removed"]>({
action: ATTACHMENT_REMOVED,
actor: (p) => p.removedBy,
target: note,
occurrence: (p) => p.attachmentId,
data: (p) => ({ attachmentId: p.attachmentId, fileId: p.fileId }),
})
/** One audit event and one usage fact per note created. */
export async function onNoteCreated(event: { name: string; payload: Payloads["created"] }, ctx: EventContext) {
await auditCreated(event, ctx)
const usage = ctx.container.resolve(FILES_USAGE)
await usage.track({
customer: { externalId: event.payload.orgId },
meter: NOTES_CREATED_METER,
quantity: 1,
idempotencyKey: `${NOTES_CREATED_METER}.${event.payload.noteId}`,
})
// Awaited: a sink that sends per call is then durable before the relay marks the record published.
await usage.flush()
}
export const onNoteUpdated = auditUpdated
export const onNoteDeleted = auditDeleted
export const onAttachmentAdded = auditAdded
export const onAttachmentRemoved = auditRemoved
/**
* A file that stopped being stored takes its links with it, so a note never lists an attachment
* nobody can open. The scope is the delivery's own organization and a system actor with the two
* deeper ids of the record the files module wrote in that organization's transaction.
*/
export async function onFileDeleted(
event: { name: string; payload: InProject & { fileId: string } },
ctx: EventContext,
): Promise<void> {
const { payload } = event
assertSameOrg(ctx, payload)
const ambient = ctx.container.resolve(FILES_SCOPE)
const scope = {
level: "project" as const,
actor: ambient.actor,
orgId: ambient.orgId,
workspaceId: payload.workspaceId,
projectId: payload.projectId,
}
await ctx.container.resolve(DATA_SOURCE).unitOfWork.run(scope, async (tx) => {
for (;;) {
const page = await tx.of(attachments).list({ where: [{ field: "fileId", op: "eq", value: payload.fileId }] })
for (const link of page.items) await tx.of(attachments).delete(link.id)
if (!page.hasMore) break
}
})
}