refactor: drop legacy db and scene fallback
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { Database } from "bun:sqlite";
|
||||||
import { mkdtempSync, rmSync } from "node:fs";
|
import { mkdtempSync, rmSync } from "node:fs";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
@@ -59,6 +60,36 @@ describe("api", () => {
|
|||||||
cleanup();
|
cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("returns 500 when stored scene payload is invalid", () => {
|
||||||
|
const dir = mkdtempSync(join(tmpdir(), "excali-api-"));
|
||||||
|
const databasePath = join(dir, "test.sqlite");
|
||||||
|
const db = new Database(databasePath, { create: true, strict: true });
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE drawings (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
data TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
revision INTEGER NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO drawings (id, title, data, revision)
|
||||||
|
VALUES ('broken', 'Broken', '{"appState":{},"files":{}}', 0);
|
||||||
|
`);
|
||||||
|
db.close(false);
|
||||||
|
|
||||||
|
const store = createDrawingStore(databasePath);
|
||||||
|
const api = createApi(store);
|
||||||
|
const response = api.getDrawing(
|
||||||
|
Object.assign(new Request("http://local/api/drawings/broken"), { params: { id: "broken" } }),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(500);
|
||||||
|
store.close();
|
||||||
|
rmSync(dir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
test("updates a drawing scene", async () => {
|
test("updates a drawing scene", async () => {
|
||||||
const { api, cleanup } = withApi();
|
const { api, cleanup } = withApi();
|
||||||
const created = await api.createDrawing().json();
|
const created = await api.createDrawing().json();
|
||||||
|
|||||||
+13
-9
@@ -68,17 +68,21 @@ export function createApi(store: DrawingStore) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getDrawing(request: RouteRequest) {
|
getDrawing(request: RouteRequest) {
|
||||||
const id = request.params?.id;
|
try {
|
||||||
const drawing = id ? store.getDrawing(id) : null;
|
const id = request.params?.id;
|
||||||
|
const drawing = id ? store.getDrawing(id) : null;
|
||||||
|
|
||||||
if (!drawing) {
|
if (!drawing) {
|
||||||
return json({ ok: false, error: "Drawing not found" }, { status: 404 });
|
return json({ ok: false, error: "Drawing not found" }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return json({
|
||||||
|
...toMeta(drawing),
|
||||||
|
...drawing.scene,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return json({
|
|
||||||
...toMeta(drawing),
|
|
||||||
...drawing.scene,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateDrawing(request: RouteRequest) {
|
async updateDrawing(request: RouteRequest) {
|
||||||
|
|||||||
+26
-5
@@ -83,7 +83,7 @@ describe("drawing store", () => {
|
|||||||
expect(store.getDrawing(created.id)?.scene.elements).toEqual([{ id: "first" }]);
|
expect(store.getDrawing(created.id)?.scene.elements).toEqual([{ id: "first" }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("migrates existing databases to include revision", () => {
|
test("fails to open databases missing the revision column", () => {
|
||||||
const dir = mkdtempSync(join(tmpdir(), "excali-"));
|
const dir = mkdtempSync(join(tmpdir(), "excali-"));
|
||||||
cleanup.push(dir);
|
cleanup.push(dir);
|
||||||
const databasePath = join(dir, "test.sqlite");
|
const databasePath = join(dir, "test.sqlite");
|
||||||
@@ -102,10 +102,31 @@ describe("drawing store", () => {
|
|||||||
`);
|
`);
|
||||||
db.close(false);
|
db.close(false);
|
||||||
|
|
||||||
const store = createDrawingStore(databasePath);
|
expect(() => createDrawingStore(databasePath)).toThrow(/revision/i);
|
||||||
expect(store.getDrawing("legacy")?.revision).toBe(0);
|
});
|
||||||
|
|
||||||
const updated = store.updateDrawing("legacy", { title: "Migrated" });
|
test("throws when reading drawings with invalid stored scene payloads", () => {
|
||||||
expect(updated.ok ? updated.drawing.revision : null).toBe(1);
|
const dir = mkdtempSync(join(tmpdir(), "excali-"));
|
||||||
|
cleanup.push(dir);
|
||||||
|
const databasePath = join(dir, "test.sqlite");
|
||||||
|
const db = new Database(databasePath, { create: true, strict: true });
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE drawings (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
data TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
revision INTEGER NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO drawings (id, title, data, revision)
|
||||||
|
VALUES ('broken', 'Broken', '{"appState":{},"files":{}}', 0);
|
||||||
|
`);
|
||||||
|
db.close(false);
|
||||||
|
|
||||||
|
const store = createDrawingStore(databasePath);
|
||||||
|
expect(() => store.getDrawing("broken")).toThrow(/Invalid stored scene/);
|
||||||
|
store.close();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto";
|
|||||||
import { mkdirSync } from "node:fs";
|
import { mkdirSync } from "node:fs";
|
||||||
import { dirname } from "node:path";
|
import { dirname } from "node:path";
|
||||||
import { DEFAULT_TITLE, type DrawingMeta, type DrawingRecord, type ScenePayload } from "./shared";
|
import { DEFAULT_TITLE, type DrawingMeta, type DrawingRecord, type ScenePayload } from "./shared";
|
||||||
import { coerceStoredScene, emptyScene, normalizeTitle } from "./scene";
|
import { emptyScene, normalizeScene, normalizeTitle } from "./scene";
|
||||||
|
|
||||||
type DrawingRow = {
|
type DrawingRow = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -59,9 +59,17 @@ function readRow(row: DrawingRow | null | undefined): DrawingRecord | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let scene: ScenePayload;
|
||||||
|
try {
|
||||||
|
scene = normalizeScene(JSON.parse(row.data));
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : "Unknown scene parsing error";
|
||||||
|
throw new Error(`Invalid stored scene for drawing ${row.id}: ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...readMeta(row)!,
|
...readMeta(row)!,
|
||||||
scene: coerceStoredScene(JSON.parse(row.data)),
|
scene,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,11 +93,6 @@ export function createDrawingStore(databasePath = resolveDatabasePath()) {
|
|||||||
ON drawings(updated_at DESC, created_at DESC);
|
ON drawings(updated_at DESC, created_at DESC);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const columns = db.query("PRAGMA table_info(drawings)").all() as Array<{ name: string }>;
|
|
||||||
if (!columns.some((column) => column.name === "revision")) {
|
|
||||||
db.exec("ALTER TABLE drawings ADD COLUMN revision INTEGER NOT NULL DEFAULT 0");
|
|
||||||
}
|
|
||||||
|
|
||||||
const listQuery = db.query(`
|
const listQuery = db.query(`
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
|||||||
@@ -87,11 +87,3 @@ export function parseJsonBody<T = unknown>(text: string): T {
|
|||||||
export function parseSceneText(text: string): ScenePayload {
|
export function parseSceneText(text: string): ScenePayload {
|
||||||
return normalizeScene(parseJsonBody(text));
|
return normalizeScene(parseJsonBody(text));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function coerceStoredScene(input: unknown): ScenePayload {
|
|
||||||
try {
|
|
||||||
return normalizeScene(input);
|
|
||||||
} catch {
|
|
||||||
return emptyScene();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user