import type { AuditEntry } from "@/lib/services/audit"; import type { ContentMutationAdapter, ContentMutationContext, ContentMutationOperation, ContentMutationSnapshot, } from "./mutations"; export const CONTENT_DATABASE_OPERATIONS = [ "article.change", "ad.change", "banner.change", "event-type.change", "event.change", "event-prize.change", "event-winner.add", "poll.change", "poll-question.change", "tag.change", "prefix.change", "prefix-blacklist.change", "prefix-settings.update", "help-question.change", "writeable-box.change", "email-template.change", ] as const satisfies readonly ContentMutationOperation[]; export const CONTENT_EXTERNAL_OPERATIONS = [ "media.upload", "media.delete", "translation.cms.save", "translation.client.save", ] as const satisfies readonly ContentMutationOperation[]; export const CONTENT_MIXED_OPERATIONS = [ "photo.delete", "theme.update", "theme.apply-preset", "theme.custom-change", "theme.apply-custom", "favicon.save", "favicon.delete", "logo.save", "translation.emulator.save", ] as const satisfies readonly ContentMutationOperation[]; type TransactionToken = unknown; interface ContentTransactionOptions { isolationLevel: "read committed"; } export interface ContentProductionMutationDependencies { transaction( run: (transaction: TransactionToken) => Promise, options?: ContentTransactionOptions, ): Promise; writeAudit(entry: AuditEntry, transaction?: TransactionToken): Promise; executeOperation( operation: ContentMutationOperation, input: unknown, context: ContentMutationContext, transaction?: TransactionToken, ): Promise; } export class ContentCommittedExternalFailure extends Error { constructor(readonly snapshot: ContentMutationSnapshot) { super("Content database change committed but external effect failed"); this.name = "ContentCommittedExternalFailure"; } } function auditEntry( operation: ContentMutationOperation, context: ContentMutationContext, outcome: NonNullable, snapshot?: ContentMutationSnapshot, ): AuditEntry { return { userId: context.capability.actor.id, action: `content.${operation}`, target: "Content", correlationId: context.correlationId, domain: "content", outcome, before: snapshot?.before === null || snapshot?.before === undefined ? undefined : { ...snapshot.before }, after: snapshot?.after === null || snapshot?.after === undefined ? undefined : { ...snapshot.after }, }; } function includesOperation( operations: readonly ContentMutationOperation[], operation: ContentMutationOperation, ): boolean { return operations.includes(operation); } async function writeOutcomeOrMarkUnavailable( dependencies: ContentProductionMutationDependencies, operation: ContentMutationOperation, context: ContentMutationContext, snapshot: ContentMutationSnapshot, outcome: "success" | "partial", ): Promise { try { await dependencies.writeAudit( auditEntry(operation, context, outcome, snapshot), ); return snapshot; } catch { const completion = { status: "partial" as const, external: outcome === "partial" ? ("failed" as const) : ("completed" as const), audit: "unavailable" as const, }; return { ...snapshot, completion }; } } export function createContentProductionMutationAdapter( dependencies: ContentProductionMutationDependencies, ): ContentMutationAdapter { return { async execute(operation, input, context) { if (includesOperation(CONTENT_DATABASE_OPERATIONS, operation)) { const transactionOptions = operation === "poll.change" ? ({ isolationLevel: "read committed" } as const) : undefined; return dependencies.transaction(async (transaction) => { const snapshot = await dependencies.executeOperation( operation, input, context, transaction, ); await dependencies.writeAudit( auditEntry(operation, context, "success", snapshot), transaction, ); return snapshot; }, transactionOptions); } await dependencies.writeAudit(auditEntry(operation, context, "intent")); try { const snapshot = await dependencies.executeOperation( operation, input, context, ); return writeOutcomeOrMarkUnavailable( dependencies, operation, context, snapshot, "success", ); } catch (error) { if ( includesOperation(CONTENT_MIXED_OPERATIONS, operation) && error instanceof ContentCommittedExternalFailure ) { const snapshot: ContentMutationSnapshot = { ...error.snapshot, completion: { status: "partial", external: "failed", audit: "persisted", }, }; return writeOutcomeOrMarkUnavailable( dependencies, operation, context, snapshot, "partial", ); } try { await dependencies.writeAudit( auditEntry(operation, context, "failure"), ); } catch { // Preserve the original dependency failure; the missing outcome is observable // through the durable correlated intent. } throw error; } }, }; } export const contentProductionMutationAdapter = createContentProductionMutationAdapter({ async transaction(run, options) { const { db } = await import("@/lib/db"); return db.transaction((transaction) => run(transaction), options); }, async writeAudit(entry, transaction) { const { logAudit } = await import("@/lib/services/audit"); await logAudit(entry, transaction as never); }, async executeOperation(operation, input, context, transaction) { const { executeContentMutationOperation } = await import( "./mutations-runtime" ); return executeContentMutationOperation( operation, input, context, transaction, ); }, });