Compare commits

..

1 commit

Author SHA1 Message Date
vince
5e42ee8869 feat: add dashboard persistence store 2026-06-18 18:16:21 +02:00
3 changed files with 70 additions and 7 deletions

View file

@ -37,10 +37,12 @@ DATABASE_URL=file:./data/dimensionlab.sqlite
SQLite files under `data/` are ignored. Drizzle schema lives in
`src/lib/server/db/schema.ts`; tracked migrations live in `drizzle/`. Runtime
startup applies the checked-in dashboard migrations before reads or writes. The
current driver is `bun:sqlite`, which keeps this repo installable in the Bun
workflow. The store boundary is isolated so a later Postgres driver can replace
the SQLite connection without changing the dashboard model or renderer.
startup applies the checked-in dashboard migrations before reads or writes. If
the app is launched from outside the repo tree, set `DASHBOARD_MIGRATIONS_DIR`
to the tracked migrations directory. The current driver is `bun:sqlite`, which
keeps this repo installable in the Bun workflow. The store boundary is isolated
so a later Postgres driver can replace the SQLite connection without changing
the dashboard model or renderer.
Stored dashboard documents pass through a version migration boundary before
reads or writes; the MVP supports `dashboard.v1` and fails unsupported versions
with an explicit migration error.

View file

@ -13,9 +13,11 @@ import {
const stores: DashboardStore[] = [];
const tempRoots: string[] = [];
const originalCwd = process.cwd();
afterEach(() => {
vi.useRealTimers();
process.chdir(originalCwd);
stores.splice(0).forEach((store) => store.close());
tempRoots.splice(0).forEach((path) => rmSync(path, { force: true, recursive: true }));
});
@ -165,6 +167,25 @@ describe("dashboard persistence store", () => {
seed.id,
]);
});
test("applies migrations when launched outside the repository root", async () => {
const root = await mkdtemp(join(tmpdir(), "dimensionlab-dashboard-cwd-"));
tempRoots.push(root);
process.chdir(root);
vi.resetModules();
const { createDashboardStore: createStore } = await import("./dashboard-store");
const store = createStore({
databaseUrl: `file:${join(root, "dashboard.sqlite")}`,
});
stores.push(store);
const seed = store.seedDashboardIfEmpty(genericDashboardFixture, {
actor: "test-seed",
});
expect(seed.document.metadata.title).toBe("Operations Console");
expect(store.getActiveDashboard()?.currentRevisionId).toBe(seed.id);
});
});
async function createTestStore() {

View file

@ -1,12 +1,52 @@
import { resolve } from "node:path";
import { existsSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { migrate } from "drizzle-orm/bun-sqlite/migrator";
import type { BunSQLiteDatabase } from "drizzle-orm/bun-sqlite";
export const DASHBOARD_MIGRATIONS_FOLDER = resolve("drizzle");
const MIGRATION_JOURNAL_PATH = join("meta", "_journal.json");
export function applyDashboardMigrations(
database: BunSQLiteDatabase<Record<string, unknown>>,
migrationsFolder = DASHBOARD_MIGRATIONS_FOLDER,
migrationsFolder = resolveDashboardMigrationsFolder(),
) {
migrate(database, { migrationsFolder });
}
export function resolveDashboardMigrationsFolder(
explicitFolder = process.env.DASHBOARD_MIGRATIONS_DIR,
): string {
if (explicitFolder) return assertMigrationFolder(resolve(explicitFolder));
const cwdFolder = findMigrationFolder(process.cwd());
if (cwdFolder) return cwdFolder;
const moduleFolder = findMigrationFolder(dirname(fileURLToPath(import.meta.url)));
if (moduleFolder) return moduleFolder;
throw new Error(
"Unable to locate dashboard Drizzle migrations. Set DASHBOARD_MIGRATIONS_DIR.",
);
}
function findMigrationFolder(startPath: string): string | null {
let currentPath = resolve(startPath);
while (true) {
const candidate = join(currentPath, "drizzle");
if (hasMigrationJournal(candidate)) return candidate;
const parentPath = dirname(currentPath);
if (parentPath === currentPath) return null;
currentPath = parentPath;
}
}
function assertMigrationFolder(folder: string): string {
if (hasMigrationJournal(folder)) return folder;
throw new Error(`Dashboard migrations not found in ${folder}`);
}
function hasMigrationJournal(folder: string): boolean {
return existsSync(join(folder, MIGRATION_JOURNAL_PATH));
}