import { and, desc, eq, max } from "drizzle-orm"; import type { ProjectRevisionSource } from "../../domain/ports/project-revision.store.js"; import type { AppDatabase } from "../database-context.js"; import { projectHistoryStackEntries } from "../schema/project-history-stack-entries.js"; export interface ApplyProjectHistoryTransitionInput { projectId: string; source: ProjectRevisionSource; recordedChangeSetId: string; targetChangeSetId?: string; } export function applyProjectHistoryTransition( database: AppDatabase, input: ApplyProjectHistoryTransitionInput ) { if (input.source === "user" || input.source === "restore") { database .delete(projectHistoryStackEntries) .where( and( eq(projectHistoryStackEntries.projectId, input.projectId), eq(projectHistoryStackEntries.stack, "redo") ) ) .run(); push( database, input.projectId, input.recordedChangeSetId, "undo" ); return; } if (input.source === "undo" || input.source === "redo") { if (!input.targetChangeSetId) { throw new Error( `${input.source} requires a target change set.` ); } const sourceStack = input.source; const targetStack = input.source === "undo" ? "redo" : "undo"; const top = database .select({ changeSetId: projectHistoryStackEntries.changeSetId, }) .from(projectHistoryStackEntries) .where( and( eq(projectHistoryStackEntries.projectId, input.projectId), eq(projectHistoryStackEntries.stack, sourceStack) ) ) .orderBy(desc(projectHistoryStackEntries.position)) .limit(1) .get(); if (!top || top.changeSetId !== input.targetChangeSetId) { throw new Error( `${input.source} target is not the top ${sourceStack} command.` ); } const nextPosition = getNextPosition( database, input.projectId, targetStack ); const moved = database .update(projectHistoryStackEntries) .set({ stack: targetStack, position: nextPosition }) .where( and( eq(projectHistoryStackEntries.projectId, input.projectId), eq( projectHistoryStackEntries.changeSetId, input.targetChangeSetId ) ) ) .run(); if (moved.changes !== 1) { throw new Error("Project history stack changed during transition."); } } } function push( database: AppDatabase, projectId: string, changeSetId: string, stack: "undo" | "redo" ) { database .insert(projectHistoryStackEntries) .values({ projectId, changeSetId, stack, position: getNextPosition(database, projectId, stack), }) .run(); } function getNextPosition( database: AppDatabase, projectId: string, stack: "undo" | "redo" ) { const current = database .select({ position: max(projectHistoryStackEntries.position) }) .from(projectHistoryStackEntries) .where( and( eq(projectHistoryStackEntries.projectId, projectId), eq(projectHistoryStackEntries.stack, stack) ) ) .get(); return (current?.position ?? 0) + 1; }