Files
leistungsbilanz-ts/src/db/repositories/project-revision.persistence.ts
T

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."
);
}
}