pmx-canvas 0.6.5 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/extensions/pmx-canvas/extension.mjs +67 -3
- package/CHANGELOG.md +54 -0
- package/Readme.md +29 -5
- package/dist/canvas/global.css +440 -24
- package/dist/canvas/index.js +90 -75
- package/dist/types/cli/agent.d.ts +3 -0
- package/dist/types/cli/commands/backup.d.ts +1 -0
- package/dist/types/cli/commands/boards.d.ts +1 -0
- package/dist/types/cli/commands/export.d.ts +1 -0
- package/dist/types/client/canvas/BoardCopyDialog.d.ts +5 -0
- package/dist/types/client/canvas/BoardFolderDialog.d.ts +7 -0
- package/dist/types/client/canvas/BoardLinkDialog.d.ts +3 -0
- package/dist/types/client/canvas/BoardSwitcher.d.ts +8 -0
- package/dist/types/client/canvas/ExportDialog.d.ts +7 -0
- package/dist/types/client/canvas/HomeView.d.ts +9 -0
- package/dist/types/client/nodes/AttachmentNode.d.ts +6 -0
- package/dist/types/client/nodes/BoardNode.d.ts +5 -0
- package/dist/types/client/state/boards-store.d.ts +79 -0
- package/dist/types/client/state/canvas-store.d.ts +2 -0
- package/dist/types/client/state/intent-bridge.d.ts +1 -0
- package/dist/types/client/state/session-store.d.ts +7 -0
- package/dist/types/client/types.d.ts +2 -2
- package/dist/types/json-render/catalog.d.ts +1 -1
- package/dist/types/json-render/charts/definitions.d.ts +1 -1
- package/dist/types/json-render/server.d.ts +6 -0
- package/dist/types/mcp/canvas-access.d.ts +10 -3
- package/dist/types/mcp/context-uri-template.d.ts +6 -0
- package/dist/types/server/agent-presence.d.ts +1 -1
- package/dist/types/server/attribution.d.ts +11 -0
- package/dist/types/server/ax-context.d.ts +3 -1
- package/dist/types/server/ax-interaction.d.ts +2 -2
- package/dist/types/server/ax-state.d.ts +13 -3
- package/dist/types/server/backup.d.ts +36 -0
- package/dist/types/server/board-export.d.ts +45 -0
- package/dist/types/server/canvas-db.d.ts +63 -9
- package/dist/types/server/canvas-operations.d.ts +23 -0
- package/dist/types/server/canvas-provenance.d.ts +1 -1
- package/dist/types/server/canvas-state.d.ts +140 -3
- package/dist/types/server/context-brief.d.ts +111 -0
- package/dist/types/server/context-reads.d.ts +55 -0
- package/dist/types/server/document-import.d.ts +66 -0
- package/dist/types/server/index.d.ts +103 -4
- package/dist/types/server/mutation-history.d.ts +1 -0
- package/dist/types/server/operations/invoker.d.ts +2 -0
- package/dist/types/server/operations/mcp.d.ts +7 -1
- package/dist/types/server/operations/ops/ax-shared.d.ts +2 -2
- package/dist/types/server/operations/ops/backup.d.ts +2 -0
- package/dist/types/server/operations/ops/boards.d.ts +10 -0
- package/dist/types/server/operations/ops/export.d.ts +2 -0
- package/dist/types/server/operations/ops/imports.d.ts +2 -0
- package/dist/types/server/operations/ops/nodes.d.ts +1 -1
- package/dist/types/server/operations/ops/query.d.ts +2 -0
- package/dist/types/server/operations/registry.d.ts +9 -1
- package/dist/types/server/operations/types.d.ts +1 -1
- package/dist/types/server/server.d.ts +1 -0
- package/dist/types/server/trace-manager.d.ts +7 -0
- package/dist/types/server/webpage-node.d.ts +22 -1
- package/dist/types/server/workbench-auth.d.ts +3 -0
- package/dist/types/shared/boards.d.ts +6 -0
- package/docs/RELEASE.md +5 -0
- package/docs/cli.md +68 -0
- package/docs/http-api.md +198 -2
- package/docs/mcp.md +71 -6
- package/docs/node-types.md +26 -0
- package/docs/plans/plan-010-attributed-ask-answers.md +39 -0
- package/docs/plans/plan-011-context-read-instrumentation.md +54 -0
- package/docs/plans/plan-012-many-boards-stage-1.md +98 -0
- package/docs/plans/plan-013-static-board-export.md +47 -0
- package/docs/plans/plan-014-connected-board-memory.md +140 -0
- package/docs/product-vision-2026-09.md +118 -18
- package/docs/sdk.md +50 -0
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +53 -4
- package/skills/pmx-canvas/references/full-reference.md +43 -7
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +3 -0
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +4 -4
- package/skills/pmx-canvas-orchestration/SKILL.md +5 -0
- package/skills/pmx-canvas-testing/SKILL.md +4 -0
- package/src/cli/agent.ts +4 -0
- package/src/cli/commands/ax.ts +13 -0
- package/src/cli/commands/backup.ts +66 -0
- package/src/cli/commands/boards.ts +71 -0
- package/src/cli/commands/export.ts +30 -0
- package/src/cli/commands/nodes.ts +4 -1
- package/src/cli/commands/pins.ts +6 -2
- package/src/cli/commands/pump.ts +3 -0
- package/src/cli/commands/query.ts +33 -7
- package/src/cli/commands/view.ts +7 -5
- package/src/cli/index.ts +19 -9
- package/src/client/App.tsx +21 -3
- package/src/client/canvas/BoardCopyDialog.tsx +153 -0
- package/src/client/canvas/BoardFolderDialog.tsx +120 -0
- package/src/client/canvas/BoardLinkDialog.tsx +93 -0
- package/src/client/canvas/BoardSwitcher.tsx +149 -0
- package/src/client/canvas/CanvasViewport.tsx +3 -0
- package/src/client/canvas/CommandPalette.tsx +107 -4
- package/src/client/canvas/ContextMenu.tsx +4 -0
- package/src/client/canvas/ExpandedNodeOverlay.tsx +3 -0
- package/src/client/canvas/ExportDialog.tsx +197 -0
- package/src/client/canvas/HomeView.tsx +277 -0
- package/src/client/canvas/SessionPanel.tsx +28 -1
- package/src/client/canvas/ToolRail.tsx +38 -3
- package/src/client/canvas/TopBar.tsx +34 -18
- package/src/client/canvas/import-files.ts +19 -1
- package/src/client/canvas/kind-colors.ts +1 -0
- package/src/client/icons.tsx +2 -0
- package/src/client/nodes/AttachmentNode.tsx +148 -0
- package/src/client/nodes/BoardNode.tsx +35 -0
- package/src/client/nodes/FileNode.tsx +6 -0
- package/src/client/nodes/MarkdownNode.tsx +47 -19
- package/src/client/nodes/ax-node-actions.ts +2 -5
- package/src/client/nodes/use-ax-surface-bridge.ts +1 -1
- package/src/client/state/boards-store.ts +167 -0
- package/src/client/state/canvas-store.ts +22 -0
- package/src/client/state/intent-bridge.ts +30 -2
- package/src/client/state/node-factory.ts +1 -0
- package/src/client/state/session-store.ts +5 -0
- package/src/client/state/sse-bridge.ts +49 -5
- package/src/client/theme/global.css +440 -24
- package/src/client/types.ts +2 -0
- package/src/json-render/server.ts +16 -9
- package/src/mcp/canvas-access.ts +59 -22
- package/src/mcp/context-uri-template.ts +28 -0
- package/src/mcp/server.ts +122 -15
- package/src/server/agent-context.ts +116 -29
- package/src/server/attribution.ts +29 -0
- package/src/server/ax-context.ts +4 -2
- package/src/server/ax-interaction.ts +1 -0
- package/src/server/ax-state-manager.ts +72 -3
- package/src/server/ax-state.ts +41 -0
- package/src/server/backup.ts +178 -0
- package/src/server/board-export.ts +686 -0
- package/src/server/canvas-db.ts +599 -107
- package/src/server/canvas-operations.ts +68 -9
- package/src/server/canvas-provenance.ts +1 -0
- package/src/server/canvas-schema.ts +12 -0
- package/src/server/canvas-state.ts +774 -45
- package/src/server/context-brief.ts +372 -0
- package/src/server/context-reads.ts +238 -0
- package/src/server/demo-state.json +659 -2
- package/src/server/document-import.ts +223 -0
- package/src/server/index.ts +231 -5
- package/src/server/mutation-history.ts +10 -13
- package/src/server/operations/composites.ts +26 -4
- package/src/server/operations/http.ts +9 -3
- package/src/server/operations/index.ts +8 -0
- package/src/server/operations/invoker.ts +14 -3
- package/src/server/operations/mcp.ts +50 -11
- package/src/server/operations/ops/app.ts +5 -0
- package/src/server/operations/ops/ax-read.ts +10 -6
- package/src/server/operations/ops/ax-timeline.ts +75 -0
- package/src/server/operations/ops/backup.ts +134 -0
- package/src/server/operations/ops/batch.ts +8 -1
- package/src/server/operations/ops/boards.ts +351 -0
- package/src/server/operations/ops/export.ts +74 -0
- package/src/server/operations/ops/ext-app.ts +4 -0
- package/src/server/operations/ops/imports.ts +220 -0
- package/src/server/operations/ops/nodes.ts +57 -5
- package/src/server/operations/ops/query.ts +206 -9
- package/src/server/operations/registry.ts +123 -31
- package/src/server/operations/types.ts +1 -1
- package/src/server/server.ts +146 -2
- package/src/server/spatial-analysis.ts +9 -17
- package/src/server/trace-manager.ts +19 -0
- package/src/server/web-artifacts.ts +2 -1
- package/src/server/webpage-node.ts +173 -13
- package/src/server/workbench-auth.ts +11 -0
- package/src/shared/boards.ts +18 -0
- package/src/shared/node-sizes.ts +1 -0
package/src/server/canvas-db.ts
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { Database } from 'bun:sqlite';
|
|
9
|
+
import { CONTEXT_READS_SCHEMA_SQL } from './context-reads.js';
|
|
10
|
+
import { MIGRATED_SNAPSHOT_CATEGORY, normalizeBoardCategory } from '../shared/boards.js';
|
|
9
11
|
import { tourSchema, type Tour } from '../shared/tour.js';
|
|
10
12
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
11
13
|
import { dirname } from 'node:path';
|
|
@@ -42,7 +44,7 @@ import {
|
|
|
42
44
|
|
|
43
45
|
// ── Schema ──────────────────────────────────────────────────────
|
|
44
46
|
|
|
45
|
-
const SCHEMA_VERSION =
|
|
47
|
+
const SCHEMA_VERSION = 2;
|
|
46
48
|
|
|
47
49
|
export type CanvasTheme = CanvasThemeName;
|
|
48
50
|
|
|
@@ -56,8 +58,29 @@ const SCHEMA_SQL = `
|
|
|
56
58
|
value TEXT NOT NULL
|
|
57
59
|
);
|
|
58
60
|
|
|
59
|
-
CREATE TABLE IF NOT EXISTS
|
|
61
|
+
CREATE TABLE IF NOT EXISTS context_brief_cursors (
|
|
62
|
+
board_id TEXT NOT NULL,
|
|
63
|
+
consumer TEXT NOT NULL,
|
|
64
|
+
revision INTEGER NOT NULL,
|
|
65
|
+
PRIMARY KEY (board_id, consumer)
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
CREATE TABLE IF NOT EXISTS boards (
|
|
60
69
|
id TEXT PRIMARY KEY,
|
|
70
|
+
name TEXT NOT NULL,
|
|
71
|
+
created_at TEXT NOT NULL,
|
|
72
|
+
last_opened_at TEXT,
|
|
73
|
+
viewport_x REAL NOT NULL DEFAULT 0,
|
|
74
|
+
viewport_y REAL NOT NULL DEFAULT 0,
|
|
75
|
+
viewport_scale REAL NOT NULL DEFAULT 1,
|
|
76
|
+
tour TEXT NOT NULL DEFAULT 'null',
|
|
77
|
+
category TEXT,
|
|
78
|
+
readme_node_id TEXT
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
CREATE TABLE IF NOT EXISTS nodes (
|
|
82
|
+
board_id TEXT NOT NULL,
|
|
83
|
+
id TEXT NOT NULL,
|
|
61
84
|
type TEXT NOT NULL,
|
|
62
85
|
pos_x REAL NOT NULL,
|
|
63
86
|
pos_y REAL NOT NULL,
|
|
@@ -66,21 +89,25 @@ const SCHEMA_SQL = `
|
|
|
66
89
|
z_index INTEGER NOT NULL DEFAULT 0,
|
|
67
90
|
collapsed INTEGER NOT NULL DEFAULT 0,
|
|
68
91
|
pinned INTEGER NOT NULL DEFAULT 0,
|
|
69
|
-
data TEXT NOT NULL
|
|
92
|
+
data TEXT NOT NULL,
|
|
93
|
+
PRIMARY KEY (board_id, id)
|
|
70
94
|
);
|
|
71
95
|
|
|
72
96
|
CREATE TABLE IF NOT EXISTS edges (
|
|
73
|
-
|
|
97
|
+
board_id TEXT NOT NULL,
|
|
98
|
+
id TEXT NOT NULL,
|
|
74
99
|
from_node TEXT NOT NULL,
|
|
75
100
|
to_node TEXT NOT NULL,
|
|
76
101
|
type TEXT NOT NULL,
|
|
77
102
|
label TEXT,
|
|
78
103
|
style TEXT,
|
|
79
|
-
animated INTEGER NOT NULL DEFAULT 0
|
|
104
|
+
animated INTEGER NOT NULL DEFAULT 0,
|
|
105
|
+
PRIMARY KEY (board_id, id)
|
|
80
106
|
);
|
|
81
107
|
|
|
82
108
|
CREATE TABLE IF NOT EXISTS annotations (
|
|
83
|
-
|
|
109
|
+
board_id TEXT NOT NULL,
|
|
110
|
+
id TEXT NOT NULL,
|
|
84
111
|
type TEXT NOT NULL,
|
|
85
112
|
points TEXT NOT NULL,
|
|
86
113
|
bounds TEXT NOT NULL,
|
|
@@ -88,20 +115,26 @@ const SCHEMA_SQL = `
|
|
|
88
115
|
width REAL NOT NULL,
|
|
89
116
|
text TEXT,
|
|
90
117
|
label TEXT,
|
|
91
|
-
created_at TEXT NOT NULL
|
|
118
|
+
created_at TEXT NOT NULL,
|
|
119
|
+
PRIMARY KEY (board_id, id)
|
|
92
120
|
);
|
|
93
121
|
|
|
94
122
|
CREATE TABLE IF NOT EXISTS context_pins (
|
|
95
|
-
|
|
123
|
+
board_id TEXT NOT NULL,
|
|
124
|
+
node_id TEXT NOT NULL,
|
|
125
|
+
PRIMARY KEY (board_id, node_id)
|
|
96
126
|
);
|
|
97
127
|
|
|
98
128
|
CREATE TABLE IF NOT EXISTS ax_state (
|
|
99
|
-
|
|
100
|
-
|
|
129
|
+
board_id TEXT NOT NULL,
|
|
130
|
+
key TEXT NOT NULL,
|
|
131
|
+
value TEXT NOT NULL,
|
|
132
|
+
PRIMARY KEY (board_id, key)
|
|
101
133
|
);
|
|
102
134
|
|
|
103
135
|
CREATE TABLE IF NOT EXISTS snapshots (
|
|
104
136
|
id TEXT PRIMARY KEY,
|
|
137
|
+
board_id TEXT,
|
|
105
138
|
name TEXT NOT NULL,
|
|
106
139
|
created_at TEXT NOT NULL,
|
|
107
140
|
node_count INTEGER NOT NULL,
|
|
@@ -168,6 +201,39 @@ const SCHEMA_SQL = `
|
|
|
168
201
|
json_bytes INTEGER NOT NULL
|
|
169
202
|
);
|
|
170
203
|
|
|
204
|
+
CREATE TABLE IF NOT EXISTS attachment_bytes (
|
|
205
|
+
sha256 TEXT PRIMARY KEY,
|
|
206
|
+
data BLOB NOT NULL,
|
|
207
|
+
size INTEGER NOT NULL
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
CREATE TABLE IF NOT EXISTS attachments (
|
|
211
|
+
id TEXT PRIMARY KEY,
|
|
212
|
+
board_id TEXT NOT NULL,
|
|
213
|
+
sha256 TEXT NOT NULL,
|
|
214
|
+
name TEXT NOT NULL,
|
|
215
|
+
mime TEXT NOT NULL,
|
|
216
|
+
size INTEGER NOT NULL,
|
|
217
|
+
created_at TEXT NOT NULL
|
|
218
|
+
);
|
|
219
|
+
CREATE INDEX IF NOT EXISTS idx_attachments_board ON attachments (board_id);
|
|
220
|
+
|
|
221
|
+
CREATE TABLE IF NOT EXISTS document_imports (
|
|
222
|
+
id TEXT PRIMARY KEY,
|
|
223
|
+
board_id TEXT NOT NULL,
|
|
224
|
+
attachment_id TEXT NOT NULL,
|
|
225
|
+
status TEXT NOT NULL,
|
|
226
|
+
position_x REAL NOT NULL,
|
|
227
|
+
position_y REAL NOT NULL,
|
|
228
|
+
sections TEXT,
|
|
229
|
+
warnings TEXT,
|
|
230
|
+
agent_description TEXT,
|
|
231
|
+
reason TEXT,
|
|
232
|
+
committed_node_ids TEXT,
|
|
233
|
+
created_at TEXT NOT NULL,
|
|
234
|
+
updated_at TEXT NOT NULL
|
|
235
|
+
);
|
|
236
|
+
|
|
171
237
|
CREATE TABLE IF NOT EXISTS ax_events (
|
|
172
238
|
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
173
239
|
id TEXT NOT NULL UNIQUE,
|
|
@@ -270,6 +336,33 @@ export interface PersistedCanvasState {
|
|
|
270
336
|
annotations?: CanvasAnnotation[];
|
|
271
337
|
contextPins: string[];
|
|
272
338
|
ax?: PmxAxState;
|
|
339
|
+
revisionState?: PersistedRevisionState;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export interface PersistedRevisionState {
|
|
343
|
+
revision: number;
|
|
344
|
+
floor: number;
|
|
345
|
+
tombstones: Array<{ nodeId: string; revision: number; deletedBy: import('./attribution.js').ActorAttribution }>;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Durable, delivery-based context cursor. This is separate from the bounded diagnostic read log. */
|
|
349
|
+
export function readContextBriefCursor(db: Database, boardId: string, consumer: string): number | null {
|
|
350
|
+
return (
|
|
351
|
+
db
|
|
352
|
+
.query<{ revision: number }, [string, string]>(
|
|
353
|
+
'SELECT revision FROM context_brief_cursors WHERE board_id = ? AND consumer = ?',
|
|
354
|
+
)
|
|
355
|
+
.get(boardId, consumer)?.revision ?? null
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Monotonic update prevents a stale concurrent response from regressing a consumer. */
|
|
360
|
+
export function advanceContextBriefCursor(db: Database, boardId: string, consumer: string, revision: number): void {
|
|
361
|
+
db.run(
|
|
362
|
+
`INSERT INTO context_brief_cursors (board_id, consumer, revision) VALUES (?, ?, ?)
|
|
363
|
+
ON CONFLICT(board_id, consumer) DO UPDATE SET revision = MAX(revision, excluded.revision)`,
|
|
364
|
+
[boardId, consumer, revision],
|
|
365
|
+
);
|
|
273
366
|
}
|
|
274
367
|
|
|
275
368
|
function readTour(raw: string | undefined): Tour | undefined {
|
|
@@ -279,7 +372,12 @@ function readTour(raw: string | undefined): Tour | undefined {
|
|
|
279
372
|
|
|
280
373
|
// ── Database Management ─────────────────────────────────────────
|
|
281
374
|
|
|
282
|
-
export
|
|
375
|
+
export interface OpenCanvasDbOptions {
|
|
376
|
+
/** Name for the board a pre-boards (0.6.x) database migrates into. */
|
|
377
|
+
migratedBoardName?: string;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function openCanvasDb(dbPath: string, options: OpenCanvasDbOptions = {}): Database {
|
|
283
381
|
const dir = dirname(dbPath);
|
|
284
382
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
285
383
|
|
|
@@ -293,12 +391,22 @@ export function openCanvasDb(dbPath: string): Database {
|
|
|
293
391
|
// worth the fsync.
|
|
294
392
|
db.exec('PRAGMA synchronous=FULL');
|
|
295
393
|
db.exec('PRAGMA busy_timeout=5000');
|
|
394
|
+
if (needsBoardsMigration(db)) migrateToBoards(db, dbPath, options.migratedBoardName ?? 'Board');
|
|
296
395
|
db.exec(SCHEMA_SQL);
|
|
396
|
+
db.exec(CONTEXT_READS_SCHEMA_SQL);
|
|
297
397
|
|
|
298
398
|
// Additive columns for pre-existing DBs (fresh installs already get them via SCHEMA_SQL above).
|
|
299
399
|
ensureColumn(db, 'ax_events', 'agent_id', 'agent_id TEXT');
|
|
300
400
|
ensureColumn(db, 'ax_steering', 'agent_id', 'agent_id TEXT');
|
|
301
401
|
ensureColumn(db, 'ax_steering', 'target', 'target TEXT');
|
|
402
|
+
ensureColumn(db, 'context_reads', 'board_id', 'board_id TEXT');
|
|
403
|
+
ensureColumn(db, 'boards', 'category', 'category TEXT');
|
|
404
|
+
ensureColumn(db, 'boards', 'readme_node_id', 'readme_node_id TEXT');
|
|
405
|
+
ensureColumn(db, 'nodes', 'attribution', "attribution TEXT NOT NULL DEFAULT '{}' ");
|
|
406
|
+
ensureColumn(db, 'nodes', 'content_revision', 'content_revision INTEGER NOT NULL DEFAULT 0');
|
|
407
|
+
ensureColumn(db, 'snapshot_nodes', 'attribution', "attribution TEXT NOT NULL DEFAULT '{}' ");
|
|
408
|
+
ensureColumn(db, 'snapshot_nodes', 'content_revision', 'content_revision INTEGER NOT NULL DEFAULT 0');
|
|
409
|
+
ensureColumn(db, 'document_imports', 'committed_node_ids', 'committed_node_ids TEXT');
|
|
302
410
|
|
|
303
411
|
// Set schema version if not present
|
|
304
412
|
const row = db.query<{ value: string }, [string]>('SELECT value FROM meta WHERE key = ?').get('schema_version');
|
|
@@ -309,6 +417,151 @@ export function openCanvasDb(dbPath: string): Database {
|
|
|
309
417
|
return db;
|
|
310
418
|
}
|
|
311
419
|
|
|
420
|
+
// ── Many boards migration (schema 1 → 2) ────────────────────────
|
|
421
|
+
|
|
422
|
+
/** A pre-boards database has a `nodes` table without a `board_id` column. */
|
|
423
|
+
function needsBoardsMigration(db: Database): boolean {
|
|
424
|
+
const columns = db.query<{ name: string }, []>('PRAGMA table_info(nodes)').all();
|
|
425
|
+
return columns.length > 0 && !columns.some((column) => column.name === 'board_id');
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export function createBoardId(): string {
|
|
429
|
+
return `board-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Moves a one-board (0.6.x) database into the boards layout. The current board
|
|
434
|
+
* becomes a named board and stays the open one. Every snapshot becomes a board
|
|
435
|
+
* of its own: in 0.6 each session replaced the one board, so a snapshot is
|
|
436
|
+
* effectively a past board (the real work of earlier sessions survives only
|
|
437
|
+
* there). SQLite cannot change a primary key in place, so the board-scoped
|
|
438
|
+
* tables are rebuilt. A copy of the file is written first (`<db>.pre-boards`),
|
|
439
|
+
* and everything runs in one transaction.
|
|
440
|
+
*/
|
|
441
|
+
function migrateToBoards(db: Database, dbPath: string, boardName: string): void {
|
|
442
|
+
const metaValue = (key: string): string | undefined =>
|
|
443
|
+
db.query<{ value: string }, [string]>('SELECT value FROM meta WHERE key = ?').get(key)?.value;
|
|
444
|
+
// Schema 1 always created `snapshots` alongside `nodes`.
|
|
445
|
+
const snapshotCount = db.query<{ n: number }, []>('SELECT COUNT(*) AS n FROM snapshots').get()?.n ?? 0;
|
|
446
|
+
const hasBoard = metaValue('state_populated') === '1';
|
|
447
|
+
|
|
448
|
+
if ((hasBoard || snapshotCount > 0) && dbPath !== ':memory:') {
|
|
449
|
+
const backupPath = `${dbPath}.pre-boards`;
|
|
450
|
+
if (!existsSync(backupPath)) db.run('VACUUM INTO ?', [backupPath]);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const boardId = createBoardId();
|
|
454
|
+
const rebuilt: Array<{ table: string; columns: string }> = [
|
|
455
|
+
{ table: 'nodes', columns: 'id, type, pos_x, pos_y, width, height, z_index, collapsed, pinned, data' },
|
|
456
|
+
{ table: 'edges', columns: 'id, from_node, to_node, type, label, style, animated' },
|
|
457
|
+
{ table: 'annotations', columns: 'id, type, points, bounds, color, width, text, label, created_at' },
|
|
458
|
+
{ table: 'context_pins', columns: 'node_id' },
|
|
459
|
+
{ table: 'ax_state', columns: 'key, value' },
|
|
460
|
+
];
|
|
461
|
+
|
|
462
|
+
const migrate = db.transaction(() => {
|
|
463
|
+
const existing = new Set(
|
|
464
|
+
db
|
|
465
|
+
.query<{ name: string }, []>("SELECT name FROM sqlite_master WHERE type = 'table'")
|
|
466
|
+
.all()
|
|
467
|
+
.map((r) => r.name),
|
|
468
|
+
);
|
|
469
|
+
for (const { table } of rebuilt) {
|
|
470
|
+
if (existing.has(table)) db.exec(`ALTER TABLE ${table} RENAME TO ${table}_v1`);
|
|
471
|
+
}
|
|
472
|
+
if (existing.has('snapshots')) ensureColumn(db, 'snapshots', 'board_id', 'board_id TEXT');
|
|
473
|
+
db.exec(SCHEMA_SQL);
|
|
474
|
+
|
|
475
|
+
if (hasBoard) {
|
|
476
|
+
const now = new Date().toISOString();
|
|
477
|
+
db.run(
|
|
478
|
+
'INSERT INTO boards (id, name, created_at, last_opened_at, viewport_x, viewport_y, viewport_scale, tour) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
|
479
|
+
[
|
|
480
|
+
boardId,
|
|
481
|
+
boardName,
|
|
482
|
+
now,
|
|
483
|
+
now,
|
|
484
|
+
Number(metaValue('viewport_x') ?? 0),
|
|
485
|
+
Number(metaValue('viewport_y') ?? 0),
|
|
486
|
+
Number(metaValue('viewport_scale') ?? 1) || 1,
|
|
487
|
+
metaValue('tour') ?? 'null',
|
|
488
|
+
],
|
|
489
|
+
);
|
|
490
|
+
for (const { table, columns } of rebuilt) {
|
|
491
|
+
if (!existing.has(table)) continue;
|
|
492
|
+
db.run(`INSERT INTO ${table} (board_id, ${columns}) SELECT ?, ${columns} FROM ${table}_v1`, [boardId]);
|
|
493
|
+
}
|
|
494
|
+
db.run(
|
|
495
|
+
"INSERT INTO meta (key, value) VALUES ('active_board', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value",
|
|
496
|
+
[boardId],
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
snapshotsToBoards(db);
|
|
500
|
+
for (const { table } of rebuilt) {
|
|
501
|
+
if (existing.has(table)) db.exec(`DROP TABLE ${table}_v1`);
|
|
502
|
+
}
|
|
503
|
+
db.run("DELETE FROM meta WHERE key IN ('viewport_x', 'viewport_y', 'viewport_scale', 'tour', 'state_populated')");
|
|
504
|
+
db.run("UPDATE meta SET value = ? WHERE key = 'schema_version'", [String(SCHEMA_VERSION)]);
|
|
505
|
+
});
|
|
506
|
+
migrate();
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** Each schema-1 snapshot becomes a board named after it; the snapshot rows are then removed. */
|
|
510
|
+
function snapshotsToBoards(db: Database): void {
|
|
511
|
+
interface SnapshotRow {
|
|
512
|
+
id: string;
|
|
513
|
+
name: string;
|
|
514
|
+
created_at: string;
|
|
515
|
+
}
|
|
516
|
+
const snapshots = db
|
|
517
|
+
.query<SnapshotRow, []>('SELECT id, name, created_at FROM snapshots WHERE board_id IS NULL ORDER BY created_at')
|
|
518
|
+
.all();
|
|
519
|
+
for (const snapshot of snapshots) {
|
|
520
|
+
const meta = new Map(
|
|
521
|
+
db
|
|
522
|
+
.query<{ key: string; value: string }, [string]>('SELECT key, value FROM snapshot_meta WHERE snapshot_id = ?')
|
|
523
|
+
.all(snapshot.id)
|
|
524
|
+
.map((row) => [row.key, row.value]),
|
|
525
|
+
);
|
|
526
|
+
const boardId = createBoardId();
|
|
527
|
+
db.run(
|
|
528
|
+
'INSERT INTO boards (id, name, category, created_at, viewport_x, viewport_y, viewport_scale, tour) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
|
529
|
+
[
|
|
530
|
+
boardId,
|
|
531
|
+
snapshot.name,
|
|
532
|
+
MIGRATED_SNAPSHOT_CATEGORY,
|
|
533
|
+
snapshot.created_at,
|
|
534
|
+
Number(meta.get('viewport_x') ?? 0),
|
|
535
|
+
Number(meta.get('viewport_y') ?? 0),
|
|
536
|
+
Number(meta.get('viewport_scale') ?? 1) || 1,
|
|
537
|
+
meta.get('tour') ?? 'null',
|
|
538
|
+
],
|
|
539
|
+
);
|
|
540
|
+
const nodeColumns = 'id, type, pos_x, pos_y, width, height, z_index, collapsed, pinned, data';
|
|
541
|
+
const edgeColumns = 'id, from_node, to_node, type, label, style, animated';
|
|
542
|
+
const annotationColumns = 'id, type, points, bounds, color, width, text, label, created_at';
|
|
543
|
+
db.run(
|
|
544
|
+
`INSERT INTO nodes (board_id, ${nodeColumns}) SELECT ?, ${nodeColumns} FROM snapshot_nodes WHERE snapshot_id = ?`,
|
|
545
|
+
[boardId, snapshot.id],
|
|
546
|
+
);
|
|
547
|
+
db.run(
|
|
548
|
+
`INSERT INTO edges (board_id, ${edgeColumns}) SELECT ?, ${edgeColumns} FROM snapshot_edges WHERE snapshot_id = ?`,
|
|
549
|
+
[boardId, snapshot.id],
|
|
550
|
+
);
|
|
551
|
+
db.run(
|
|
552
|
+
`INSERT INTO annotations (board_id, ${annotationColumns}) SELECT ?, ${annotationColumns} FROM snapshot_annotations WHERE snapshot_id = ?`,
|
|
553
|
+
[boardId, snapshot.id],
|
|
554
|
+
);
|
|
555
|
+
db.run('INSERT INTO context_pins (board_id, node_id) SELECT ?, node_id FROM snapshot_pins WHERE snapshot_id = ?', [
|
|
556
|
+
boardId,
|
|
557
|
+
snapshot.id,
|
|
558
|
+
]);
|
|
559
|
+
const ax = meta.get('ax_state');
|
|
560
|
+
if (ax) db.run("INSERT INTO ax_state (board_id, key, value) VALUES (?, 'state', ?)", [boardId, ax]);
|
|
561
|
+
deleteSnapshotFromDB(db, snapshot.id);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
312
565
|
export function checkpointCanvasDb(db: Database): void {
|
|
313
566
|
db.exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
314
567
|
}
|
|
@@ -374,17 +627,36 @@ function deleteMissingRows(
|
|
|
374
627
|
db: Database,
|
|
375
628
|
table: 'nodes' | 'edges' | 'annotations' | 'context_pins',
|
|
376
629
|
idColumn: 'id' | 'node_id',
|
|
630
|
+
boardId: string,
|
|
377
631
|
keep: Set<string>,
|
|
378
632
|
): void {
|
|
379
|
-
const rows = db
|
|
633
|
+
const rows = db
|
|
634
|
+
.query<{ id: string }, [string]>(`SELECT ${idColumn} AS id FROM ${table} WHERE board_id = ?`)
|
|
635
|
+
.all(boardId);
|
|
380
636
|
if (rows.length === 0) return;
|
|
381
|
-
const remove = db.prepare(`DELETE FROM ${table} WHERE ${idColumn} = ?`);
|
|
637
|
+
const remove = db.prepare(`DELETE FROM ${table} WHERE board_id = ? AND ${idColumn} = ?`);
|
|
382
638
|
for (const row of rows) {
|
|
383
|
-
if (!keep.has(row.id)) remove.run(row.id);
|
|
639
|
+
if (!keep.has(row.id)) remove.run(boardId, row.id);
|
|
384
640
|
}
|
|
385
641
|
}
|
|
386
642
|
|
|
387
|
-
|
|
643
|
+
/** The theme is workspace-wide: it is saved even while no board is open. */
|
|
644
|
+
export function saveThemeToDB(db: Database, theme: CanvasTheme | undefined): void {
|
|
645
|
+
db.run(
|
|
646
|
+
`INSERT INTO meta (key, value) VALUES ('theme', ?)
|
|
647
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
648
|
+
WHERE value IS NOT excluded.value`,
|
|
649
|
+
[normalizeCanvasTheme(theme)],
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
export function readThemeFromDB(db: Database): CanvasTheme | undefined {
|
|
654
|
+
const value = db.query<{ value: string }, [string]>('SELECT value FROM meta WHERE key = ?').get('theme')?.value;
|
|
655
|
+
return value ? normalizeCanvasTheme(value) : undefined;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/** Saves one board's state; every other board's rows are untouched. */
|
|
659
|
+
export function saveStateToDB(db: Database, boardId: string, state: PersistedCanvasState): void {
|
|
388
660
|
const transaction = db.transaction(() => {
|
|
389
661
|
// Rows are upserted only when a column actually differs, and rows that are
|
|
390
662
|
// gone are deleted by id. This used to be DELETE-all + INSERT-all, which
|
|
@@ -395,35 +667,43 @@ export function saveStateToDB(db: Database, state: PersistedCanvasState): void {
|
|
|
395
667
|
// no-op — SQLite performs no update, so no page is written. The comparison
|
|
396
668
|
// is against the DB itself rather than an in-memory cache, so nothing can
|
|
397
669
|
// go stale and silently skip a write that was actually needed.
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
670
|
+
saveThemeToDB(db, state.theme);
|
|
671
|
+
const tour = JSON.stringify(state.tour ?? null);
|
|
672
|
+
db.run(
|
|
673
|
+
`UPDATE boards SET viewport_x = ?, viewport_y = ?, viewport_scale = ?, tour = ?
|
|
674
|
+
WHERE id = ? AND (viewport_x IS NOT ? OR viewport_y IS NOT ? OR viewport_scale IS NOT ? OR tour IS NOT ?)`,
|
|
675
|
+
[
|
|
676
|
+
state.viewport.x,
|
|
677
|
+
state.viewport.y,
|
|
678
|
+
state.viewport.scale,
|
|
679
|
+
tour,
|
|
680
|
+
boardId,
|
|
681
|
+
state.viewport.x,
|
|
682
|
+
state.viewport.y,
|
|
683
|
+
state.viewport.scale,
|
|
684
|
+
tour,
|
|
685
|
+
],
|
|
402
686
|
);
|
|
403
|
-
upsertMeta.run('theme', normalizeCanvasTheme(state.theme));
|
|
404
|
-
upsertMeta.run('tour', JSON.stringify(state.tour ?? null));
|
|
405
|
-
upsertMeta.run('viewport_x', String(state.viewport.x));
|
|
406
|
-
upsertMeta.run('viewport_y', String(state.viewport.y));
|
|
407
|
-
upsertMeta.run('viewport_scale', String(state.viewport.scale));
|
|
408
|
-
// Mark DB as populated (for migration detection)
|
|
409
|
-
upsertMeta.run('state_populated', '1');
|
|
410
687
|
|
|
411
688
|
// Save nodes
|
|
412
689
|
const upsertNode = db.prepare(
|
|
413
|
-
`INSERT INTO nodes (id, type, pos_x, pos_y, width, height, z_index, collapsed, pinned, data)
|
|
414
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
415
|
-
ON CONFLICT(id) DO UPDATE SET
|
|
690
|
+
`INSERT INTO nodes (board_id, id, type, pos_x, pos_y, width, height, z_index, collapsed, pinned, data, attribution, content_revision)
|
|
691
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
692
|
+
ON CONFLICT(board_id, id) DO UPDATE SET
|
|
416
693
|
type = excluded.type, pos_x = excluded.pos_x, pos_y = excluded.pos_y,
|
|
417
694
|
width = excluded.width, height = excluded.height, z_index = excluded.z_index,
|
|
418
|
-
collapsed = excluded.collapsed, pinned = excluded.pinned, data = excluded.data
|
|
695
|
+
collapsed = excluded.collapsed, pinned = excluded.pinned, data = excluded.data,
|
|
696
|
+
attribution = excluded.attribution, content_revision = excluded.content_revision
|
|
419
697
|
WHERE type IS NOT excluded.type OR pos_x IS NOT excluded.pos_x
|
|
420
698
|
OR pos_y IS NOT excluded.pos_y OR width IS NOT excluded.width
|
|
421
699
|
OR height IS NOT excluded.height OR z_index IS NOT excluded.z_index
|
|
422
700
|
OR collapsed IS NOT excluded.collapsed OR pinned IS NOT excluded.pinned
|
|
423
|
-
OR data IS NOT excluded.data
|
|
701
|
+
OR data IS NOT excluded.data OR attribution IS NOT excluded.attribution
|
|
702
|
+
OR content_revision IS NOT excluded.content_revision`,
|
|
424
703
|
);
|
|
425
704
|
for (const node of state.nodes) {
|
|
426
705
|
upsertNode.run(
|
|
706
|
+
boardId,
|
|
427
707
|
node.id,
|
|
428
708
|
node.type,
|
|
429
709
|
node.position.x,
|
|
@@ -434,15 +714,17 @@ export function saveStateToDB(db: Database, state: PersistedCanvasState): void {
|
|
|
434
714
|
node.collapsed ? 1 : 0,
|
|
435
715
|
node.pinned ? 1 : 0,
|
|
436
716
|
JSON.stringify(node.data),
|
|
717
|
+
JSON.stringify({ createdBy: node.createdBy, lastEditedBy: node.lastEditedBy }),
|
|
718
|
+
node.contentRevision ?? 0,
|
|
437
719
|
);
|
|
438
720
|
}
|
|
439
|
-
deleteMissingRows(db, 'nodes', 'id', new Set(state.nodes.map((node) => node.id)));
|
|
721
|
+
deleteMissingRows(db, 'nodes', 'id', boardId, new Set(state.nodes.map((node) => node.id)));
|
|
440
722
|
|
|
441
723
|
// Save edges
|
|
442
724
|
const upsertEdge = db.prepare(
|
|
443
|
-
`INSERT INTO edges (id, from_node, to_node, type, label, style, animated)
|
|
444
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
445
|
-
ON CONFLICT(id) DO UPDATE SET
|
|
725
|
+
`INSERT INTO edges (board_id, id, from_node, to_node, type, label, style, animated)
|
|
726
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
727
|
+
ON CONFLICT(board_id, id) DO UPDATE SET
|
|
446
728
|
from_node = excluded.from_node, to_node = excluded.to_node, type = excluded.type,
|
|
447
729
|
label = excluded.label, style = excluded.style, animated = excluded.animated
|
|
448
730
|
WHERE from_node IS NOT excluded.from_node OR to_node IS NOT excluded.to_node
|
|
@@ -451,6 +733,7 @@ export function saveStateToDB(db: Database, state: PersistedCanvasState): void {
|
|
|
451
733
|
);
|
|
452
734
|
for (const edge of state.edges) {
|
|
453
735
|
upsertEdge.run(
|
|
736
|
+
boardId,
|
|
454
737
|
edge.id,
|
|
455
738
|
edge.from,
|
|
456
739
|
edge.to,
|
|
@@ -460,14 +743,14 @@ export function saveStateToDB(db: Database, state: PersistedCanvasState): void {
|
|
|
460
743
|
edge.animated ? 1 : 0,
|
|
461
744
|
);
|
|
462
745
|
}
|
|
463
|
-
deleteMissingRows(db, 'edges', 'id', new Set(state.edges.map((edge) => edge.id)));
|
|
746
|
+
deleteMissingRows(db, 'edges', 'id', boardId, new Set(state.edges.map((edge) => edge.id)));
|
|
464
747
|
|
|
465
748
|
// Save annotations
|
|
466
749
|
const annotations = state.annotations ?? [];
|
|
467
750
|
const upsertAnnotation = db.prepare(
|
|
468
|
-
`INSERT INTO annotations (id, type, points, bounds, color, width, text, label, created_at)
|
|
469
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
470
|
-
ON CONFLICT(id) DO UPDATE SET
|
|
751
|
+
`INSERT INTO annotations (board_id, id, type, points, bounds, color, width, text, label, created_at)
|
|
752
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
753
|
+
ON CONFLICT(board_id, id) DO UPDATE SET
|
|
471
754
|
type = excluded.type, points = excluded.points, bounds = excluded.bounds,
|
|
472
755
|
color = excluded.color, width = excluded.width, text = excluded.text,
|
|
473
756
|
label = excluded.label, created_at = excluded.created_at
|
|
@@ -478,6 +761,7 @@ export function saveStateToDB(db: Database, state: PersistedCanvasState): void {
|
|
|
478
761
|
);
|
|
479
762
|
for (const annotation of annotations) {
|
|
480
763
|
upsertAnnotation.run(
|
|
764
|
+
boardId,
|
|
481
765
|
annotation.id,
|
|
482
766
|
annotation.type,
|
|
483
767
|
JSON.stringify(annotation.points),
|
|
@@ -489,51 +773,53 @@ export function saveStateToDB(db: Database, state: PersistedCanvasState): void {
|
|
|
489
773
|
annotation.createdAt,
|
|
490
774
|
);
|
|
491
775
|
}
|
|
492
|
-
deleteMissingRows(db, 'annotations', 'id', new Set(annotations.map((annotation) => annotation.id)));
|
|
776
|
+
deleteMissingRows(db, 'annotations', 'id', boardId, new Set(annotations.map((annotation) => annotation.id)));
|
|
493
777
|
|
|
494
778
|
// Save context pins (node_id is the whole row — nothing to update)
|
|
495
|
-
const insertPin = db.prepare('INSERT OR IGNORE INTO context_pins (node_id) VALUES (?)');
|
|
779
|
+
const insertPin = db.prepare('INSERT OR IGNORE INTO context_pins (board_id, node_id) VALUES (?, ?)');
|
|
496
780
|
for (const pinId of state.contextPins) {
|
|
497
|
-
insertPin.run(pinId);
|
|
781
|
+
insertPin.run(boardId, pinId);
|
|
498
782
|
}
|
|
499
|
-
deleteMissingRows(db, 'context_pins', 'node_id', new Set(state.contextPins));
|
|
783
|
+
deleteMissingRows(db, 'context_pins', 'node_id', boardId, new Set(state.contextPins));
|
|
500
784
|
|
|
501
785
|
db.run(
|
|
502
|
-
`INSERT INTO ax_state (key, value) VALUES (?, ?)
|
|
503
|
-
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
786
|
+
`INSERT INTO ax_state (board_id, key, value) VALUES (?, ?, ?)
|
|
787
|
+
ON CONFLICT(board_id, key) DO UPDATE SET value = excluded.value
|
|
504
788
|
WHERE value IS NOT excluded.value`,
|
|
505
|
-
['state', JSON.stringify(state.ax ?? createEmptyAxState())],
|
|
789
|
+
[boardId, 'state', JSON.stringify(state.ax ?? createEmptyAxState())],
|
|
790
|
+
);
|
|
791
|
+
db.run(
|
|
792
|
+
`INSERT INTO ax_state (board_id, key, value) VALUES (?, ?, ?)
|
|
793
|
+
ON CONFLICT(board_id, key) DO UPDATE SET value = excluded.value
|
|
794
|
+
WHERE value IS NOT excluded.value`,
|
|
795
|
+
[boardId, 'revision', JSON.stringify(state.revisionState ?? { revision: 0, floor: 0, tombstones: [] })],
|
|
506
796
|
);
|
|
507
797
|
});
|
|
508
798
|
|
|
509
799
|
transaction();
|
|
510
800
|
}
|
|
511
801
|
|
|
512
|
-
/**
|
|
513
|
-
export function
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
const row = db.query<{ value: string }, [string]>('SELECT value FROM meta WHERE key = ?').get(key);
|
|
527
|
-
return row ? Number(row.value) : 0;
|
|
528
|
-
};
|
|
802
|
+
/** Loads one board (the open board when `boardId` is omitted); null when there is no such board. */
|
|
803
|
+
export function loadStateFromDB(db: Database, boardId?: string): PersistedCanvasState | null {
|
|
804
|
+
const id = boardId ?? getActiveBoardIdFromDB(db);
|
|
805
|
+
if (!id) return null;
|
|
806
|
+
interface BoardRow {
|
|
807
|
+
viewport_x: number;
|
|
808
|
+
viewport_y: number;
|
|
809
|
+
viewport_scale: number;
|
|
810
|
+
tour: string;
|
|
811
|
+
}
|
|
812
|
+
const board = db
|
|
813
|
+
.query<BoardRow, [string]>('SELECT viewport_x, viewport_y, viewport_scale, tour FROM boards WHERE id = ?')
|
|
814
|
+
.get(id);
|
|
815
|
+
if (!board) return null;
|
|
529
816
|
|
|
530
817
|
const viewport: ViewportState = {
|
|
531
|
-
x:
|
|
532
|
-
y:
|
|
533
|
-
scale:
|
|
818
|
+
x: board.viewport_x,
|
|
819
|
+
y: board.viewport_y,
|
|
820
|
+
scale: board.viewport_scale || 1,
|
|
534
821
|
};
|
|
535
|
-
const
|
|
536
|
-
const theme = themeValue ? normalizeCanvasTheme(themeValue) : undefined;
|
|
822
|
+
const theme = readThemeFromDB(db);
|
|
537
823
|
|
|
538
824
|
// Load nodes
|
|
539
825
|
interface NodeRow {
|
|
@@ -547,18 +833,28 @@ export function loadStateFromDB(db: Database): PersistedCanvasState | null {
|
|
|
547
833
|
collapsed: number;
|
|
548
834
|
pinned: number;
|
|
549
835
|
data: string;
|
|
836
|
+
attribution: string;
|
|
837
|
+
content_revision: number;
|
|
550
838
|
}
|
|
551
|
-
const nodeRows = db.query<NodeRow, []>('SELECT * FROM nodes').all();
|
|
552
|
-
const nodes: CanvasNodeState[] = nodeRows.map((row) =>
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
839
|
+
const nodeRows = db.query<NodeRow, [string]>('SELECT * FROM nodes WHERE board_id = ?').all(id);
|
|
840
|
+
const nodes: CanvasNodeState[] = nodeRows.map((row) => {
|
|
841
|
+
const attribution = JSON.parse(row.attribution || '{}') as Partial<
|
|
842
|
+
Pick<CanvasNodeState, 'createdBy' | 'lastEditedBy'>
|
|
843
|
+
>;
|
|
844
|
+
return {
|
|
845
|
+
id: row.id,
|
|
846
|
+
type: row.type as CanvasNodeState['type'],
|
|
847
|
+
position: { x: row.pos_x, y: row.pos_y },
|
|
848
|
+
size: { width: row.width, height: row.height },
|
|
849
|
+
zIndex: row.z_index,
|
|
850
|
+
collapsed: row.collapsed === 1,
|
|
851
|
+
pinned: row.pinned === 1,
|
|
852
|
+
data: JSON.parse(row.data) as Record<string, unknown>,
|
|
853
|
+
createdBy: attribution.createdBy,
|
|
854
|
+
lastEditedBy: attribution.lastEditedBy,
|
|
855
|
+
contentRevision: row.content_revision,
|
|
856
|
+
};
|
|
857
|
+
});
|
|
562
858
|
|
|
563
859
|
// Load edges
|
|
564
860
|
interface EdgeRow {
|
|
@@ -570,7 +866,7 @@ export function loadStateFromDB(db: Database): PersistedCanvasState | null {
|
|
|
570
866
|
style: string | null;
|
|
571
867
|
animated: number;
|
|
572
868
|
}
|
|
573
|
-
const edgeRows = db.query<EdgeRow, []>('SELECT * FROM edges').all();
|
|
869
|
+
const edgeRows = db.query<EdgeRow, [string]>('SELECT * FROM edges WHERE board_id = ?').all(id);
|
|
574
870
|
const edges: CanvasEdge[] = edgeRows.map((row) => ({
|
|
575
871
|
id: row.id,
|
|
576
872
|
from: row.from_node,
|
|
@@ -593,7 +889,7 @@ export function loadStateFromDB(db: Database): PersistedCanvasState | null {
|
|
|
593
889
|
label: string | null;
|
|
594
890
|
created_at: string;
|
|
595
891
|
}
|
|
596
|
-
const annotationRows = db.query<AnnotationRow, []>('SELECT * FROM annotations').all();
|
|
892
|
+
const annotationRows = db.query<AnnotationRow, [string]>('SELECT * FROM annotations WHERE board_id = ?').all(id);
|
|
597
893
|
const annotations: CanvasAnnotation[] = annotationRows.map((row) => ({
|
|
598
894
|
id: row.id,
|
|
599
895
|
type: row.type as CanvasAnnotation['type'],
|
|
@@ -610,31 +906,196 @@ export function loadStateFromDB(db: Database): PersistedCanvasState | null {
|
|
|
610
906
|
interface PinRow {
|
|
611
907
|
node_id: string;
|
|
612
908
|
}
|
|
613
|
-
const pinRows = db.query<PinRow, []>('SELECT node_id FROM context_pins').all();
|
|
909
|
+
const pinRows = db.query<PinRow, [string]>('SELECT node_id FROM context_pins WHERE board_id = ?').all(id);
|
|
614
910
|
const contextPins = pinRows.map((row) => row.node_id);
|
|
615
911
|
|
|
616
|
-
const axRow = db
|
|
912
|
+
const axRow = db
|
|
913
|
+
.query<{ value: string }, [string, string]>('SELECT value FROM ax_state WHERE board_id = ? AND key = ?')
|
|
914
|
+
.get(id, 'state');
|
|
617
915
|
|
|
916
|
+
const revisionRaw = db
|
|
917
|
+
.query<{ value: string }, [string, string]>('SELECT value FROM ax_state WHERE board_id = ? AND key = ?')
|
|
918
|
+
.get(id, 'revision')?.value;
|
|
618
919
|
return {
|
|
619
920
|
version: 1,
|
|
620
921
|
theme,
|
|
621
|
-
tour: readTour(
|
|
922
|
+
tour: readTour(board.tour),
|
|
622
923
|
viewport,
|
|
623
924
|
nodes,
|
|
624
925
|
edges,
|
|
625
926
|
annotations,
|
|
626
927
|
contextPins,
|
|
627
928
|
ax: parsePersistedAxState(axRow?.value),
|
|
929
|
+
revisionState: revisionRaw ? (JSON.parse(revisionRaw) as PersistedRevisionState) : undefined,
|
|
628
930
|
};
|
|
629
931
|
}
|
|
630
932
|
|
|
933
|
+
// ── Boards ──────────────────────────────────────────────────────
|
|
934
|
+
|
|
935
|
+
export interface CanvasBoard {
|
|
936
|
+
id: string;
|
|
937
|
+
name: string;
|
|
938
|
+
/** A human-chosen shelf on Home ("Planning", "Research"); null is uncategorized. */
|
|
939
|
+
category: string | null;
|
|
940
|
+
createdAt: string;
|
|
941
|
+
lastOpenedAt: string | null;
|
|
942
|
+
nodeCount: number;
|
|
943
|
+
/** Markdown node used as this board's introduction. */
|
|
944
|
+
readmeNodeId: string | null;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
interface BoardListRow {
|
|
948
|
+
id: string;
|
|
949
|
+
name: string;
|
|
950
|
+
category: string | null;
|
|
951
|
+
created_at: string;
|
|
952
|
+
last_opened_at: string | null;
|
|
953
|
+
node_count: number;
|
|
954
|
+
readme_node_id: string | null;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
const BOARD_LIST_SQL = `SELECT b.id, b.name, b.category, b.created_at, b.last_opened_at, b.readme_node_id,
|
|
958
|
+
(SELECT COUNT(*) FROM nodes n WHERE n.board_id = b.id) AS node_count
|
|
959
|
+
FROM boards b`;
|
|
960
|
+
|
|
961
|
+
function rowToBoard(row: BoardListRow): CanvasBoard {
|
|
962
|
+
return {
|
|
963
|
+
id: row.id,
|
|
964
|
+
name: row.name,
|
|
965
|
+
category: row.category,
|
|
966
|
+
createdAt: row.created_at,
|
|
967
|
+
lastOpenedAt: row.last_opened_at,
|
|
968
|
+
nodeCount: row.node_count,
|
|
969
|
+
readmeNodeId: row.readme_node_id,
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/** Most recently opened first, then never-opened boards, newest first. */
|
|
974
|
+
export function listBoardsFromDB(db: Database): CanvasBoard[] {
|
|
975
|
+
return db
|
|
976
|
+
.query<BoardListRow, []>(`${BOARD_LIST_SQL} ORDER BY b.last_opened_at DESC NULLS LAST, b.created_at DESC`)
|
|
977
|
+
.all()
|
|
978
|
+
.map(rowToBoard);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
export function getBoardFromDB(db: Database, id: string): CanvasBoard | null {
|
|
982
|
+
const row = db.query<BoardListRow, [string]>(`${BOARD_LIST_SQL} WHERE b.id = ?`).get(id);
|
|
983
|
+
return row ? rowToBoard(row) : null;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
export function createBoardInDB(db: Database, name: string, category: string | null = null): CanvasBoard {
|
|
987
|
+
category = normalizeBoardCategory(category);
|
|
988
|
+
const id = createBoardId();
|
|
989
|
+
const createdAt = new Date(Date.now()).toISOString();
|
|
990
|
+
db.run('INSERT INTO boards (id, name, category, created_at) VALUES (?, ?, ?, ?)', [id, name, category, createdAt]);
|
|
991
|
+
return { id, name, category, createdAt, lastOpenedAt: null, nodeCount: 0, readmeNodeId: null };
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/** Rename and/or re-shelve a board; `category: null` removes it from its category. */
|
|
995
|
+
export function updateBoardInDB(
|
|
996
|
+
db: Database,
|
|
997
|
+
id: string,
|
|
998
|
+
patch: { name?: string; category?: string | null; readmeNodeId?: string | null },
|
|
999
|
+
): boolean {
|
|
1000
|
+
const category = patch.category === undefined ? undefined : normalizeBoardCategory(patch.category);
|
|
1001
|
+
if (!db.query<{ id: string }, [string]>('SELECT id FROM boards WHERE id = ?').get(id)) return false;
|
|
1002
|
+
if (patch.name !== undefined) db.run('UPDATE boards SET name = ? WHERE id = ?', [patch.name, id]);
|
|
1003
|
+
if (category !== undefined) db.run('UPDATE boards SET category = ? WHERE id = ?', [category, id]);
|
|
1004
|
+
if (patch.readmeNodeId !== undefined) {
|
|
1005
|
+
db.run('UPDATE boards SET readme_node_id = ? WHERE id = ?', [patch.readmeNodeId, id]);
|
|
1006
|
+
}
|
|
1007
|
+
return true;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/** Create and populate an inactive board as one SQLite transaction. */
|
|
1011
|
+
export function createBoardWithStateInDB(
|
|
1012
|
+
db: Database,
|
|
1013
|
+
name: string,
|
|
1014
|
+
category: string | null,
|
|
1015
|
+
state: PersistedCanvasState,
|
|
1016
|
+
readmeNodeId: string | null,
|
|
1017
|
+
): CanvasBoard {
|
|
1018
|
+
return db.transaction(() => {
|
|
1019
|
+
const board = createBoardInDB(db, name, category);
|
|
1020
|
+
saveStateToDB(db, board.id, state);
|
|
1021
|
+
updateBoardInDB(db, board.id, { readmeNodeId });
|
|
1022
|
+
return { ...board, nodeCount: state.nodes.length, readmeNodeId };
|
|
1023
|
+
})();
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/** Deletes a board with its rows and its snapshots. */
|
|
1027
|
+
export function deleteBoardFromDB(db: Database, id: string): boolean {
|
|
1028
|
+
const transaction = db.transaction(() => {
|
|
1029
|
+
for (const table of ['nodes', 'edges', 'annotations', 'context_pins', 'ax_state']) {
|
|
1030
|
+
db.run(`DELETE FROM ${table} WHERE board_id = ?`, [id]);
|
|
1031
|
+
}
|
|
1032
|
+
const snapshotIds = db
|
|
1033
|
+
.query<{ id: string }, [string]>('SELECT id FROM snapshots WHERE board_id = ?')
|
|
1034
|
+
.all(id)
|
|
1035
|
+
.map((row) => row.id);
|
|
1036
|
+
for (const snapshotId of snapshotIds) deleteSnapshotFromDB(db, snapshotId);
|
|
1037
|
+
db.run('DELETE FROM document_imports WHERE board_id = ?', [id]);
|
|
1038
|
+
db.run('DELETE FROM attachments WHERE board_id = ?', [id]);
|
|
1039
|
+
db.run('DELETE FROM attachment_bytes WHERE sha256 NOT IN (SELECT sha256 FROM attachments)');
|
|
1040
|
+
db.run("DELETE FROM meta WHERE key = 'active_board' AND value = ?", [id]);
|
|
1041
|
+
return db.run('DELETE FROM boards WHERE id = ?', [id]).changes > 0;
|
|
1042
|
+
});
|
|
1043
|
+
return transaction();
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
export function readMetaFromDB(db: Database, key: string): string | null {
|
|
1047
|
+
return db.query<{ value: string }, [string]>('SELECT value FROM meta WHERE key = ?').get(key)?.value ?? null;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/** Write a workspace-wide meta value; null removes it. */
|
|
1051
|
+
export function writeMetaToDB(db: Database, key: string, value: string | null): void {
|
|
1052
|
+
if (value === null) {
|
|
1053
|
+
db.run('DELETE FROM meta WHERE key = ?', [key]);
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
db.run('INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value', [
|
|
1057
|
+
key,
|
|
1058
|
+
value,
|
|
1059
|
+
]);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/** The board that was open last, if it still exists. */
|
|
1063
|
+
export function getActiveBoardIdFromDB(db: Database): string | null {
|
|
1064
|
+
const id = db.query<{ value: string }, [string]>('SELECT value FROM meta WHERE key = ?').get('active_board')?.value;
|
|
1065
|
+
if (!id) return null;
|
|
1066
|
+
return db.query<{ id: string }, [string]>('SELECT id FROM boards WHERE id = ?').get(id) ? id : null;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
export function setActiveBoardIdInDB(db: Database, id: string | null): void {
|
|
1070
|
+
if (!id) {
|
|
1071
|
+
db.run("DELETE FROM meta WHERE key = 'active_board'");
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
const latestOpenedAt = db
|
|
1075
|
+
.query<{ value: string }, []>('SELECT MAX(last_opened_at) AS value FROM boards')
|
|
1076
|
+
.get()?.value;
|
|
1077
|
+
const now = Date.now();
|
|
1078
|
+
const openedAt = new Date(Math.max(now, latestOpenedAt ? Date.parse(latestOpenedAt) + 1 : now)).toISOString();
|
|
1079
|
+
db.run(
|
|
1080
|
+
"INSERT INTO meta (key, value) VALUES ('active_board', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value",
|
|
1081
|
+
[id],
|
|
1082
|
+
);
|
|
1083
|
+
db.run('UPDATE boards SET last_opened_at = ? WHERE id = ?', [openedAt, id]);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
631
1086
|
// ── Snapshot Persistence ────────────────────────────────────────
|
|
632
1087
|
|
|
633
|
-
export function saveSnapshotToDB(
|
|
1088
|
+
export function saveSnapshotToDB(
|
|
1089
|
+
db: Database,
|
|
1090
|
+
boardId: string,
|
|
1091
|
+
snapshot: CanvasSnapshot,
|
|
1092
|
+
state: PersistedCanvasState,
|
|
1093
|
+
): void {
|
|
634
1094
|
const transaction = db.transaction(() => {
|
|
635
1095
|
// Insert snapshot metadata
|
|
636
|
-
db.run('INSERT INTO snapshots (id, name, created_at, node_count, edge_count) VALUES (?, ?, ?, ?, ?)', [
|
|
1096
|
+
db.run('INSERT INTO snapshots (id, board_id, name, created_at, node_count, edge_count) VALUES (?, ?, ?, ?, ?, ?)', [
|
|
637
1097
|
snapshot.id,
|
|
1098
|
+
boardId,
|
|
638
1099
|
snapshot.name,
|
|
639
1100
|
snapshot.createdAt,
|
|
640
1101
|
state.nodes.length,
|
|
@@ -670,8 +1131,8 @@ export function saveSnapshotToDB(db: Database, snapshot: CanvasSnapshot, state:
|
|
|
670
1131
|
|
|
671
1132
|
// Insert snapshot nodes
|
|
672
1133
|
const insertNode = db.prepare(
|
|
673
|
-
`INSERT INTO snapshot_nodes (snapshot_id, id, type, pos_x, pos_y, width, height, z_index, collapsed, pinned, data)
|
|
674
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1134
|
+
`INSERT INTO snapshot_nodes (snapshot_id, id, type, pos_x, pos_y, width, height, z_index, collapsed, pinned, data, attribution, content_revision)
|
|
1135
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
675
1136
|
);
|
|
676
1137
|
for (const node of state.nodes) {
|
|
677
1138
|
insertNode.run(
|
|
@@ -686,6 +1147,8 @@ export function saveSnapshotToDB(db: Database, snapshot: CanvasSnapshot, state:
|
|
|
686
1147
|
node.collapsed ? 1 : 0,
|
|
687
1148
|
node.pinned ? 1 : 0,
|
|
688
1149
|
JSON.stringify(node.data),
|
|
1150
|
+
JSON.stringify({ createdBy: node.createdBy, lastEditedBy: node.lastEditedBy }),
|
|
1151
|
+
node.contentRevision ?? 0,
|
|
689
1152
|
);
|
|
690
1153
|
}
|
|
691
1154
|
|
|
@@ -732,16 +1195,24 @@ export function saveSnapshotToDB(db: Database, snapshot: CanvasSnapshot, state:
|
|
|
732
1195
|
for (const pinId of state.contextPins) {
|
|
733
1196
|
insertPin.run(snapshot.id, pinId);
|
|
734
1197
|
}
|
|
1198
|
+
db.run('INSERT INTO snapshot_meta (snapshot_id, key, value) VALUES (?, ?, ?)', [
|
|
1199
|
+
snapshot.id,
|
|
1200
|
+
'revision_state',
|
|
1201
|
+
JSON.stringify(state.revisionState ?? { revision: 0, floor: 0, tombstones: [] }),
|
|
1202
|
+
]);
|
|
735
1203
|
});
|
|
736
1204
|
|
|
737
1205
|
transaction();
|
|
738
1206
|
}
|
|
739
1207
|
|
|
1208
|
+
/** By id or name (most recent match) on `boardId`. */
|
|
740
1209
|
export function loadSnapshotFromDB(
|
|
741
1210
|
db: Database,
|
|
742
1211
|
idOrName: string,
|
|
1212
|
+
boardId: string | null,
|
|
743
1213
|
): { snapshot: CanvasSnapshot; state: PersistedCanvasState } | null {
|
|
744
|
-
|
|
1214
|
+
if (!boardId) return null;
|
|
1215
|
+
|
|
745
1216
|
interface SnapshotRow {
|
|
746
1217
|
id: string;
|
|
747
1218
|
name: string;
|
|
@@ -749,12 +1220,16 @@ export function loadSnapshotFromDB(
|
|
|
749
1220
|
node_count: number;
|
|
750
1221
|
edge_count: number;
|
|
751
1222
|
}
|
|
752
|
-
let snapshotRow = db
|
|
1223
|
+
let snapshotRow = db
|
|
1224
|
+
.query<SnapshotRow, [string, string]>('SELECT * FROM snapshots WHERE board_id = ? AND id = ?')
|
|
1225
|
+
.get(boardId, idOrName);
|
|
753
1226
|
|
|
754
1227
|
if (!snapshotRow) {
|
|
755
1228
|
snapshotRow = db
|
|
756
|
-
.query<SnapshotRow, [string]>(
|
|
757
|
-
|
|
1229
|
+
.query<SnapshotRow, [string, string]>(
|
|
1230
|
+
'SELECT * FROM snapshots WHERE board_id = ? AND name = ? ORDER BY created_at DESC LIMIT 1',
|
|
1231
|
+
)
|
|
1232
|
+
.get(boardId, idOrName);
|
|
758
1233
|
}
|
|
759
1234
|
|
|
760
1235
|
if (!snapshotRow) return null;
|
|
@@ -795,20 +1270,30 @@ export function loadSnapshotFromDB(
|
|
|
795
1270
|
collapsed: number;
|
|
796
1271
|
pinned: number;
|
|
797
1272
|
data: string;
|
|
1273
|
+
attribution: string;
|
|
1274
|
+
content_revision: number;
|
|
798
1275
|
}
|
|
799
1276
|
const nodeRows = db
|
|
800
1277
|
.query<NodeRow, [string]>('SELECT * FROM snapshot_nodes WHERE snapshot_id = ?')
|
|
801
1278
|
.all(snapshotRow.id);
|
|
802
|
-
const nodes: CanvasNodeState[] = nodeRows.map((row) =>
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
1279
|
+
const nodes: CanvasNodeState[] = nodeRows.map((row) => {
|
|
1280
|
+
const attribution = JSON.parse(row.attribution || '{}') as Partial<
|
|
1281
|
+
Pick<CanvasNodeState, 'createdBy' | 'lastEditedBy'>
|
|
1282
|
+
>;
|
|
1283
|
+
return {
|
|
1284
|
+
id: row.id,
|
|
1285
|
+
type: row.type as CanvasNodeState['type'],
|
|
1286
|
+
position: { x: row.pos_x, y: row.pos_y },
|
|
1287
|
+
size: { width: row.width, height: row.height },
|
|
1288
|
+
zIndex: row.z_index,
|
|
1289
|
+
collapsed: row.collapsed === 1,
|
|
1290
|
+
pinned: row.pinned === 1,
|
|
1291
|
+
data: JSON.parse(row.data) as Record<string, unknown>,
|
|
1292
|
+
createdBy: attribution.createdBy,
|
|
1293
|
+
lastEditedBy: attribution.lastEditedBy,
|
|
1294
|
+
contentRevision: row.content_revision,
|
|
1295
|
+
};
|
|
1296
|
+
});
|
|
812
1297
|
|
|
813
1298
|
// Load snapshot edges
|
|
814
1299
|
interface EdgeRow {
|
|
@@ -880,18 +1365,25 @@ export function loadSnapshotFromDB(
|
|
|
880
1365
|
annotations,
|
|
881
1366
|
contextPins,
|
|
882
1367
|
ax: parsePersistedAxState(metaMap.get('ax_state')),
|
|
1368
|
+
revisionState: metaMap.has('revision_state')
|
|
1369
|
+
? (JSON.parse(metaMap.get('revision_state')!) as PersistedRevisionState)
|
|
1370
|
+
: undefined,
|
|
883
1371
|
},
|
|
884
1372
|
};
|
|
885
1373
|
}
|
|
886
1374
|
|
|
887
|
-
export function listSnapshotsFromDB(
|
|
1375
|
+
export function listSnapshotsFromDB(
|
|
1376
|
+
db: Database,
|
|
1377
|
+
boardId: string,
|
|
1378
|
+
options: CanvasSnapshotListOptions = {},
|
|
1379
|
+
): CanvasSnapshot[] {
|
|
888
1380
|
const query = options.query?.trim().toLowerCase();
|
|
889
1381
|
const before = normalizeSnapshotTimestamp(options.before);
|
|
890
1382
|
const after = normalizeSnapshotTimestamp(options.after);
|
|
891
1383
|
const limit = options.all ? undefined : (normalizePositiveInteger(options.limit) ?? 20);
|
|
892
1384
|
|
|
893
|
-
let sql = 'SELECT * FROM snapshots WHERE
|
|
894
|
-
const params: string[] = [];
|
|
1385
|
+
let sql = 'SELECT * FROM snapshots WHERE board_id = ?';
|
|
1386
|
+
const params: string[] = [boardId];
|
|
895
1387
|
|
|
896
1388
|
if (query) {
|
|
897
1389
|
sql += ' AND (LOWER(id) LIKE ? OR LOWER(name) LIKE ?)';
|
|
@@ -931,8 +1423,8 @@ export function listSnapshotsFromDB(db: Database, options: CanvasSnapshotListOpt
|
|
|
931
1423
|
}));
|
|
932
1424
|
}
|
|
933
1425
|
|
|
934
|
-
export function renameSnapshotInDB(db: Database, id: string, name: string): boolean {
|
|
935
|
-
return db.run('UPDATE snapshots SET name = ? WHERE id = ?', [name, id]).changes > 0;
|
|
1426
|
+
export function renameSnapshotInDB(db: Database, boardId: string, id: string, name: string): boolean {
|
|
1427
|
+
return db.run('UPDATE snapshots SET name = ? WHERE board_id = ? AND id = ?', [name, boardId, id]).changes > 0;
|
|
936
1428
|
}
|
|
937
1429
|
|
|
938
1430
|
export function deleteSnapshotFromDB(db: Database, id: string): boolean {
|