refactor(web): move website into turbo app workspace
This commit is contained in:
parent
2664804e91
commit
b4e626a868
66 changed files with 318 additions and 298 deletions
261
apps/web/src/lib/server/db/dashboard-store.ts
Normal file
261
apps/web/src/lib/server/db/dashboard-store.ts
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
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,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue