import { randomUUID } from "node:crypto"; import { desc, eq } from "drizzle-orm"; import { type DashboardDocument, type DashboardValidationFailure, } from "$lib/model"; import { type DashboardDatabaseConnection, openDashboardDatabase, } from "./connection"; import { dashboardDocuments, dashboardRevisions, type DashboardRevisionOperation, } from "./schema"; import { migrateDashboardDocumentForPersistence } from "./model-migrations"; const DEFAULT_DASHBOARD_ID = "primary"; const DEFAULT_ACTOR = "system"; export interface DashboardRevision { id: string; dashboardId: string; schemaVersion: string; document: DashboardDocument; actor: string; message: string | null; operation: DashboardRevisionOperation; sourceRevisionId: string | null; createdAt: Date; } export interface ActiveDashboard { dashboardId: string; currentRevisionId: string; document: DashboardDocument; revision: DashboardRevision; createdAt: Date; updatedAt: Date; } export interface DashboardWriteMetadata { actor?: string; message?: string; } export interface DashboardStoreOptions { databaseUrl?: string; dashboardId?: string; } export interface DashboardStore { getActiveDashboard(): ActiveDashboard | null; getRevision(revisionId: string): DashboardRevision | null; listRevisions(limit?: number): DashboardRevision[]; seedDashboardIfEmpty( document: DashboardDocument, metadata?: DashboardWriteMetadata, ): DashboardRevision; commitDashboard( document: DashboardDocument, metadata?: DashboardWriteMetadata, ): DashboardRevision; rollbackToRevision( revisionId: string, metadata?: DashboardWriteMetadata, ): DashboardRevision; close(): void; } export class DashboardPersistenceValidationError extends Error { readonly failure: DashboardValidationFailure; constructor(failure: DashboardValidationFailure) { super(`Invalid dashboard document: ${failure.errors.join("; ")}`); this.name = "DashboardPersistenceValidationError"; this.failure = failure; } } export class DashboardRevisionNotFoundError extends Error { constructor(revisionId: string) { super(`Dashboard revision not found: ${revisionId}`); this.name = "DashboardRevisionNotFoundError"; } } export function createDashboardStore( options: DashboardStoreOptions = {}, ): DashboardStore { const connection = openDashboardDatabase(options.databaseUrl); return new SqliteDashboardStore(connection, options.dashboardId || DEFAULT_DASHBOARD_ID); } class SqliteDashboardStore implements DashboardStore { constructor( private readonly connection: DashboardDatabaseConnection, private readonly dashboardId: string, ) {} getActiveDashboard(): ActiveDashboard | null { const dashboard = this.connection.db .select() .from(dashboardDocuments) .where(eq(dashboardDocuments.id, this.dashboardId)) .get(); if (!dashboard?.currentRevisionId) return null; const revision = this.getRevision(dashboard.currentRevisionId); if (!revision) return null; return { dashboardId: dashboard.id, currentRevisionId: dashboard.currentRevisionId, document: revision.document, revision, createdAt: dashboard.createdAt, updatedAt: dashboard.updatedAt, }; } getRevision(revisionId: string): DashboardRevision | null { const row = this.connection.db .select() .from(dashboardRevisions) .where(eq(dashboardRevisions.id, revisionId)) .get(); return row ? toRevision(row) : null; } listRevisions(limit = 50): DashboardRevision[] { return this.connection.db .select() .from(dashboardRevisions) .where(eq(dashboardRevisions.dashboardId, this.dashboardId)) .orderBy(desc(dashboardRevisions.createdAt)) .limit(limit) .all() .map(toRevision); } seedDashboardIfEmpty( document: DashboardDocument, metadata: DashboardWriteMetadata = {}, ): DashboardRevision { const active = this.getActiveDashboard(); if (active) return active.revision; return this.writeRevision(document, "seed", metadata); } commitDashboard( document: DashboardDocument, metadata: DashboardWriteMetadata = {}, ): DashboardRevision { return this.writeRevision(document, "commit", metadata); } rollbackToRevision( revisionId: string, metadata: DashboardWriteMetadata = {}, ): DashboardRevision { const revision = this.getRevision(revisionId); if (!revision || revision.dashboardId !== this.dashboardId) { throw new DashboardRevisionNotFoundError(revisionId); } return this.writeRevision(revision.document, "rollback", metadata, revision.id); } close() { this.connection.close(); } private writeRevision( document: DashboardDocument, operation: DashboardRevisionOperation, metadata: DashboardWriteMetadata, sourceRevisionId: string | null = null, ): DashboardRevision { const migration = migrateDashboardDocumentForPersistence(document); if (!migration.valid) { throw new DashboardPersistenceValidationError(migration.failure); } const now = this.nextRevisionTimestamp(); const revision: DashboardRevision = { id: randomUUID(), dashboardId: this.dashboardId, schemaVersion: migration.document.schemaVersion, document: structuredClone(migration.document), actor: metadata.actor || DEFAULT_ACTOR, message: metadata.message || null, operation, sourceRevisionId, createdAt: now, }; this.connection.db.transaction((tx) => { tx.insert(dashboardRevisions).values({ id: revision.id, dashboardId: revision.dashboardId, schemaVersion: revision.schemaVersion, document: revision.document, actor: revision.actor, message: revision.message, operation: revision.operation, sourceRevisionId: revision.sourceRevisionId, createdAt: revision.createdAt, }).run(); tx.insert(dashboardDocuments) .values({ id: this.dashboardId, currentRevisionId: revision.id, createdAt: now, updatedAt: now, }) .onConflictDoUpdate({ target: dashboardDocuments.id, set: { currentRevisionId: revision.id, updatedAt: now, }, }) .run(); }); return revision; } private nextRevisionTimestamp(): Date { const active = this.getActiveDashboard(); const activeUpdatedAtMs = active?.updatedAt.getTime() || 0; return new Date(Math.max(Date.now(), activeUpdatedAtMs + 1)); } } type DashboardRevisionRow = typeof dashboardRevisions.$inferSelect; function toRevision(row: DashboardRevisionRow): DashboardRevision { const migration = migrateDashboardDocumentForPersistence(row.document); if (!migration.valid) { throw new DashboardPersistenceValidationError(migration.failure); } return { id: row.id, dashboardId: row.dashboardId, schemaVersion: row.schemaVersion, document: migration.document, actor: row.actor, message: row.message, operation: row.operation as DashboardRevisionOperation, sourceRevisionId: row.sourceRevisionId, createdAt: row.createdAt, }; }