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; }