feat(housekeeping): deliver content vertical
This commit is contained in:
1 parent
bdab924bdf
commit
fd68819d9b
67 files changed
+5742
-1916
No files matched your search
@@ -0,0 +1,212 @@
|
||||
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",
|
||||
"navigation.update",
|
||||
"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;
|
||||
|
||||
export interface ContentProductionMutationDependencies {
|
||||
transaction<T>(run: (transaction: TransactionToken) => Promise<T>): Promise<T>;
|
||||
writeAudit(entry: AuditEntry, transaction?: TransactionToken): Promise<void>;
|
||||
executeOperation(
|
||||
operation: ContentMutationOperation,
|
||||
input: unknown,
|
||||
context: ContentMutationContext,
|
||||
transaction?: TransactionToken,
|
||||
): Promise<ContentMutationSnapshot>;
|
||||
}
|
||||
|
||||
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<AuditEntry["outcome"]>,
|
||||
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<ContentMutationSnapshot> {
|
||||
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)) {
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
const { db } = await import("@/lib/db");
|
||||
return db.transaction((transaction) => run(transaction));
|
||||
},
|
||||
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,
|
||||
);
|
||||
},
|
||||
});
|
||||
Reference in new issue
Block a user