feat: add dashboard persistence store
This commit is contained in:
parent
5eeec0dcb5
commit
c26ebb0f27
18 changed files with 1076 additions and 19 deletions
149
src/lib/server/db/dashboard-store.test.ts
Normal file
149
src/lib/server/db/dashboard-store.test.ts
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
import { existsSync, rmSync } from "node:fs";
|
||||
import { mkdtemp } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import { genericDashboardFixture } from "$lib/model/fixtures/generic";
|
||||
import type { DashboardDocument } from "$lib/model";
|
||||
import {
|
||||
DashboardPersistenceValidationError,
|
||||
createDashboardStore,
|
||||
type DashboardStore,
|
||||
} from "./dashboard-store";
|
||||
|
||||
const stores: DashboardStore[] = [];
|
||||
const tempRoots: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
stores.splice(0).forEach((store) => store.close());
|
||||
tempRoots.splice(0).forEach((path) => rmSync(path, { force: true, recursive: true }));
|
||||
});
|
||||
|
||||
describe("dashboard persistence store", () => {
|
||||
test("seeds the first active dashboard and creates the sqlite file", async () => {
|
||||
const { dbPath, store } = await createTestStore();
|
||||
|
||||
const seed = store.seedDashboardIfEmpty(genericDashboardFixture, {
|
||||
actor: "test-seed",
|
||||
message: "initial fixture",
|
||||
});
|
||||
|
||||
expect(existsSync(dbPath)).toBe(true);
|
||||
expect(seed.operation).toBe("seed");
|
||||
expect(seed.actor).toBe("test-seed");
|
||||
expect(seed.schemaVersion).toBe(genericDashboardFixture.schemaVersion);
|
||||
expect(seed.document.metadata.title).toBe("Operations Console");
|
||||
|
||||
const active = store.getActiveDashboard();
|
||||
expect(active?.currentRevisionId).toBe(seed.id);
|
||||
expect(active?.document.metadata.title).toBe("Operations Console");
|
||||
expect(store.listRevisions()).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("commits validated updates and records revision metadata", async () => {
|
||||
const { store } = await createTestStore();
|
||||
const seed = store.seedDashboardIfEmpty(genericDashboardFixture, {
|
||||
actor: "test-seed",
|
||||
});
|
||||
const updated = cloneDashboard({
|
||||
...genericDashboardFixture,
|
||||
metadata: {
|
||||
...genericDashboardFixture.metadata,
|
||||
title: "Operations Console Updated",
|
||||
},
|
||||
});
|
||||
|
||||
const revision = store.commitDashboard(updated, {
|
||||
actor: "agent",
|
||||
message: "rename dashboard",
|
||||
});
|
||||
|
||||
expect(revision.operation).toBe("commit");
|
||||
expect(revision.actor).toBe("agent");
|
||||
expect(revision.message).toBe("rename dashboard");
|
||||
expect(revision.document.metadata.title).toBe("Operations Console Updated");
|
||||
expect(store.getActiveDashboard()?.currentRevisionId).toBe(revision.id);
|
||||
expect(store.getRevision(seed.id)?.document.metadata.title).toBe("Operations Console");
|
||||
expect(store.listRevisions().map((item) => item.id)).toEqual([
|
||||
revision.id,
|
||||
seed.id,
|
||||
]);
|
||||
});
|
||||
|
||||
test("rejects invalid writes without changing the active revision", async () => {
|
||||
const { store } = await createTestStore();
|
||||
const seed = store.seedDashboardIfEmpty(genericDashboardFixture, {
|
||||
actor: "test-seed",
|
||||
});
|
||||
const invalid = cloneDashboard({
|
||||
...genericDashboardFixture,
|
||||
layout: {
|
||||
...genericDashboardFixture.layout,
|
||||
telemetry: ["missing-card"],
|
||||
},
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
store.commitDashboard(invalid, {
|
||||
actor: "agent",
|
||||
message: "invalid update",
|
||||
}),
|
||||
).toThrow(DashboardPersistenceValidationError);
|
||||
|
||||
const active = store.getActiveDashboard();
|
||||
expect(active?.currentRevisionId).toBe(seed.id);
|
||||
expect(active?.document.layout.telemetry).toEqual(genericDashboardFixture.layout.telemetry);
|
||||
expect(store.listRevisions()).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("loads specific revisions and rolls back to a prior valid document", async () => {
|
||||
const { store } = await createTestStore();
|
||||
const seed = store.seedDashboardIfEmpty(genericDashboardFixture, {
|
||||
actor: "test-seed",
|
||||
});
|
||||
const updated = cloneDashboard({
|
||||
...genericDashboardFixture,
|
||||
metadata: {
|
||||
...genericDashboardFixture.metadata,
|
||||
title: "Changed Dashboard",
|
||||
},
|
||||
});
|
||||
const change = store.commitDashboard(updated, {
|
||||
actor: "agent",
|
||||
message: "change title",
|
||||
});
|
||||
|
||||
const rollback = store.rollbackToRevision(seed.id, {
|
||||
actor: "operator",
|
||||
message: "restore seed",
|
||||
});
|
||||
|
||||
expect(store.getRevision(change.id)?.document.metadata.title).toBe("Changed Dashboard");
|
||||
expect(rollback.operation).toBe("rollback");
|
||||
expect(rollback.sourceRevisionId).toBe(seed.id);
|
||||
expect(rollback.document.metadata.title).toBe("Operations Console");
|
||||
expect(store.getActiveDashboard()?.currentRevisionId).toBe(rollback.id);
|
||||
expect(store.getActiveDashboard()?.document.metadata.title).toBe("Operations Console");
|
||||
expect(store.listRevisions().map((item) => item.operation)).toEqual([
|
||||
"rollback",
|
||||
"commit",
|
||||
"seed",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
async function createTestStore() {
|
||||
const root = await mkdtemp(join(tmpdir(), "dimensionlab-dashboard-store-"));
|
||||
tempRoots.push(root);
|
||||
const dbPath = join(root, "nested", "dashboard.sqlite");
|
||||
const store = createDashboardStore({
|
||||
databaseUrl: `file:${dbPath}`,
|
||||
});
|
||||
stores.push(store);
|
||||
|
||||
return { dbPath, store };
|
||||
}
|
||||
|
||||
function cloneDashboard(document: DashboardDocument): DashboardDocument {
|
||||
return structuredClone(document);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue