103 lines
3.0 KiB
TypeScript
103 lines
3.0 KiB
TypeScript
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."
|
|
);
|
|
}
|
|
}
|