112 lines
3.4 KiB
TypeScript
112 lines
3.4 KiB
TypeScript
import crypto from "node:crypto";
|
|
import { and, desc, eq, inArray } from "drizzle-orm";
|
|
import {
|
|
automaticProjectSnapshotIntervalRevisions,
|
|
automaticProjectSnapshotRetentionCount,
|
|
shouldCaptureAutomaticProjectSnapshot,
|
|
} from "../../domain/models/project-snapshot-policy.model.js";
|
|
import { projectStateSnapshotSchemaVersion } from "../../domain/models/project-state-snapshot.model.js";
|
|
import type { ProjectSnapshotMetadata } from "../../domain/ports/project-snapshot.store.js";
|
|
import type { AppDatabase } from "../database-context.js";
|
|
import { projectSnapshots } from "../schema/project-snapshots.js";
|
|
import { readProjectStateSnapshot } from "./project-state-snapshot.persistence.js";
|
|
|
|
interface CaptureAutomaticProjectSnapshotInput {
|
|
projectId: string;
|
|
currentRevision: number;
|
|
createdAtIso: string;
|
|
actorId?: string;
|
|
}
|
|
|
|
export function captureAutomaticProjectSnapshot(
|
|
database: AppDatabase,
|
|
input: CaptureAutomaticProjectSnapshotInput
|
|
): ProjectSnapshotMetadata | null {
|
|
const latest = database
|
|
.select({ sourceRevision: projectSnapshots.sourceRevision })
|
|
.from(projectSnapshots)
|
|
.where(
|
|
and(
|
|
eq(projectSnapshots.projectId, input.projectId),
|
|
eq(projectSnapshots.kind, "automatic")
|
|
)
|
|
)
|
|
.orderBy(desc(projectSnapshots.sourceRevision))
|
|
.get();
|
|
if (
|
|
!shouldCaptureAutomaticProjectSnapshot(
|
|
input.currentRevision,
|
|
latest?.sourceRevision ?? null
|
|
)
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
const current = readProjectStateSnapshot(database, input.projectId);
|
|
if (!current || current.currentRevision !== input.currentRevision) {
|
|
throw new Error(
|
|
"Automatic snapshot state does not match the appended revision."
|
|
);
|
|
}
|
|
|
|
const id = crypto.randomUUID();
|
|
const preferredName = `Automatisch · Revision ${input.currentRevision}`;
|
|
const conflictingName = database
|
|
.select({ id: projectSnapshots.id })
|
|
.from(projectSnapshots)
|
|
.where(
|
|
and(
|
|
eq(projectSnapshots.projectId, input.projectId),
|
|
eq(projectSnapshots.name, preferredName)
|
|
)
|
|
)
|
|
.get();
|
|
const metadata: ProjectSnapshotMetadata = {
|
|
id,
|
|
projectId: input.projectId,
|
|
sourceRevision: input.currentRevision,
|
|
schemaVersion: projectStateSnapshotSchemaVersion,
|
|
kind: "automatic",
|
|
name: conflictingName
|
|
? `${preferredName} · ${id}`
|
|
: preferredName,
|
|
description: `Automatischer Sicherungspunkt nach jeweils ${automaticProjectSnapshotIntervalRevisions} Projektänderungen.`,
|
|
payloadSha256: current.payloadSha256,
|
|
createdAtIso: input.createdAtIso,
|
|
createdByActorId: input.actorId ?? null,
|
|
};
|
|
database
|
|
.insert(projectSnapshots)
|
|
.values({
|
|
...metadata,
|
|
payloadJson: current.payloadJson,
|
|
})
|
|
.run();
|
|
|
|
const automaticSnapshots = database
|
|
.select({ id: projectSnapshots.id })
|
|
.from(projectSnapshots)
|
|
.where(
|
|
and(
|
|
eq(projectSnapshots.projectId, input.projectId),
|
|
eq(projectSnapshots.kind, "automatic")
|
|
)
|
|
)
|
|
.orderBy(
|
|
desc(projectSnapshots.sourceRevision),
|
|
desc(projectSnapshots.createdAtIso),
|
|
desc(projectSnapshots.id)
|
|
)
|
|
.all();
|
|
const expiredIds = automaticSnapshots
|
|
.slice(automaticProjectSnapshotRetentionCount)
|
|
.map((snapshot) => snapshot.id);
|
|
if (expiredIds.length) {
|
|
database
|
|
.delete(projectSnapshots)
|
|
.where(inArray(projectSnapshots.id, expiredIds))
|
|
.run();
|
|
}
|
|
return metadata;
|
|
}
|