import crypto from "node:crypto"; import { and, eq } from "drizzle-orm"; import { ProjectRevisionConflictError } from "../../domain/errors/project-revision-conflict.error.js"; import { serializeProjectCommand } from "../../domain/models/project-command.model.js"; import type { AppendProjectRevisionInput, AppendedProjectRevision, } from "../../domain/ports/project-revision.store.js"; import type { AppDatabase } from "../database-context.js"; import { projectChangeSets } from "../schema/project-change-sets.js"; import { projectRevisions } from "../schema/project-revisions.js"; import { projects } from "../schema/projects.js"; import { captureAutomaticProjectSnapshot } from "./automatic-project-snapshot.persistence.js"; export function appendProjectRevision( database: AppDatabase, input: AppendProjectRevisionInput ): AppendedProjectRevision { validateInput(input); const forwardPayloadJson = serializeProjectCommand(input.forward); const inversePayloadJson = serializeProjectCommand(input.inverse); const revisionId = crypto.randomUUID(); const changeSetId = crypto.randomUUID(); const revisionNumber = input.expectedRevision + 1; const createdAtIso = new Date().toISOString(); const revisionUpdate = database .update(projects) .set({ currentRevision: revisionNumber }) .where( and( eq(projects.id, input.projectId), eq(projects.currentRevision, input.expectedRevision) ) ) .run(); if (revisionUpdate.changes !== 1) { const current = database .select({ currentRevision: projects.currentRevision }) .from(projects) .where(eq(projects.id, input.projectId)) .get(); throw new ProjectRevisionConflictError( input.projectId, input.expectedRevision, current?.currentRevision ?? null ); } database .insert(projectRevisions) .values({ id: revisionId, projectId: input.projectId, revisionNumber, createdAtIso, actorId: input.actorId, source: input.source, description: input.description, }) .run(); database .insert(projectChangeSets) .values({ id: changeSetId, projectRevisionId: revisionId, commandType: input.forward.type, payloadSchemaVersion: input.forward.schemaVersion, forwardPayloadJson, inversePayloadJson, }) .run(); captureAutomaticProjectSnapshot(database, { projectId: input.projectId, currentRevision: revisionNumber, createdAtIso, actorId: input.actorId, }); return { revisionId, changeSetId, projectId: input.projectId, revisionNumber, createdAtIso, }; } function validateInput(input: AppendProjectRevisionInput) { if (!Number.isSafeInteger(input.expectedRevision) || input.expectedRevision < 0) { throw new Error("Expected project revision must be a non-negative integer."); } if (input.forward.schemaVersion !== input.inverse.schemaVersion) { throw new Error( "Forward and inverse project commands require the same schema version." ); } }