@graypirate/tabula 1.0.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/README.md +141 -0
- package/dist/API/create.d.ts +33 -0
- package/dist/API/create.js +91 -0
- package/dist/API/delete.d.ts +31 -0
- package/dist/API/delete.js +38 -0
- package/dist/API/index.d.ts +13 -0
- package/dist/API/index.js +9 -0
- package/dist/API/init.d.ts +3 -0
- package/dist/API/init.js +8 -0
- package/dist/API/read.d.ts +60 -0
- package/dist/API/read.js +95 -0
- package/dist/API/search.d.ts +4 -0
- package/dist/API/search.js +4 -0
- package/dist/API/types.d.ts +23 -0
- package/dist/API/types.js +0 -0
- package/dist/API/validation.d.ts +8 -0
- package/dist/API/validation.js +103 -0
- package/dist/API/write.d.ts +30 -0
- package/dist/API/write.js +90 -0
- package/dist/CLI/arguments.d.ts +45 -0
- package/dist/CLI/arguments.js +263 -0
- package/dist/CLI/dispatch.d.ts +3 -0
- package/dist/CLI/dispatch.js +143 -0
- package/dist/CLI/errors.d.ts +10 -0
- package/dist/CLI/errors.js +20 -0
- package/dist/CLI/index.d.ts +6 -0
- package/dist/CLI/index.js +70 -0
- package/dist/CLI/io.d.ts +6 -0
- package/dist/CLI/io.js +28 -0
- package/dist/CLI/json.d.ts +9 -0
- package/dist/CLI/json.js +42 -0
- package/dist/CLI/properties.d.ts +3 -0
- package/dist/CLI/properties.js +22 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/src/storage/db/blocks.d.ts +68 -0
- package/dist/src/storage/db/blocks.js +185 -0
- package/dist/src/storage/db/edges.d.ts +61 -0
- package/dist/src/storage/db/edges.js +306 -0
- package/dist/src/storage/db/init.d.ts +22 -0
- package/dist/src/storage/db/init.js +108 -0
- package/dist/src/storage/db/nodes.d.ts +34 -0
- package/dist/src/storage/db/nodes.js +91 -0
- package/dist/src/storage/db/objects.d.ts +55 -0
- package/dist/src/storage/db/objects.js +123 -0
- package/dist/src/storage/db/schema.sql +59 -0
- package/dist/src/storage/index.d.ts +47 -0
- package/dist/src/storage/index.js +315 -0
- package/dist/src/storage/types.d.ts +15 -0
- package/dist/src/storage/types.js +1 -0
- package/dist/src/types/block.d.ts +12 -0
- package/dist/src/types/block.js +0 -0
- package/dist/src/types/database.d.ts +6 -0
- package/dist/src/types/database.js +0 -0
- package/dist/src/types/graph.d.ts +11 -0
- package/dist/src/types/graph.js +0 -0
- package/dist/src/types/json.d.ts +7 -0
- package/dist/src/types/json.js +0 -0
- package/dist/src/types/object.d.ts +12 -0
- package/dist/src/types/object.js +0 -0
- package/dist/src/types/workspace.d.ts +6 -0
- package/dist/src/types/workspace.js +0 -0
- package/dist/src/utils/id.d.ts +6 -0
- package/dist/src/utils/id.js +18 -0
- package/dist/src/utils/yaml.d.ts +3 -0
- package/dist/src/utils/yaml.js +26 -0
- package/dist/src/workspace/index.d.ts +1 -0
- package/dist/src/workspace/index.js +1 -0
- package/dist/src/workspace/resolution.d.ts +20 -0
- package/dist/src/workspace/resolution.js +90 -0
- package/package.json +43 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { BlockPrefix, DatabasePrefix, ObjectPrefix } from "../../utils/id";
|
|
2
|
+
import { getStoredNodeType, isStoredNode, } from "./nodes";
|
|
3
|
+
/**
|
|
4
|
+
* Reads the current direct parent for an object or block node.
|
|
5
|
+
* @param db - The database containing the child node
|
|
6
|
+
* @param childID - The object or block ID whose parent should be read
|
|
7
|
+
* @returns The parent reference, or undefined when the child is unattached
|
|
8
|
+
*/
|
|
9
|
+
export function getEntityParent(db, childID) {
|
|
10
|
+
const childType = getStoredNodeType(db, childID);
|
|
11
|
+
if (childType === undefined) {
|
|
12
|
+
throw new Error(`Entity not found: ${childID}`);
|
|
13
|
+
}
|
|
14
|
+
if (childType === "database") {
|
|
15
|
+
throw new Error(`Database cannot be a child entity: ${childID}`);
|
|
16
|
+
}
|
|
17
|
+
const row = db.query(`
|
|
18
|
+
SELECT
|
|
19
|
+
edges.parent_id AS id,
|
|
20
|
+
nodes.type
|
|
21
|
+
FROM edges
|
|
22
|
+
JOIN nodes ON nodes.id = edges.parent_id
|
|
23
|
+
WHERE edges.child_id = $childID
|
|
24
|
+
`).get({ $childID: childID });
|
|
25
|
+
return row === null
|
|
26
|
+
? undefined
|
|
27
|
+
: {
|
|
28
|
+
type: row.type,
|
|
29
|
+
id: row.id,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Adds an object as a root child of the database.
|
|
34
|
+
* @param db - The database containing the object
|
|
35
|
+
* @param databaseID - The database parent ID
|
|
36
|
+
* @param objectID - The object child ID
|
|
37
|
+
*/
|
|
38
|
+
export function appendDatabaseRootObject(db, databaseID, objectID) {
|
|
39
|
+
appendEntityChild(db, databaseID, { type: "object", id: objectID });
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Appends an existing object or block under a database, object, or block parent.
|
|
43
|
+
* If the child already has a parent, it is moved to the new parent.
|
|
44
|
+
* @param db - The database containing the parent and child
|
|
45
|
+
* @param parentID - The database, object, or block parent ID
|
|
46
|
+
* @param child - The object or block child to attach
|
|
47
|
+
*/
|
|
48
|
+
export function appendEntityChild(db, parentID, child) {
|
|
49
|
+
validateParent(db, parentID);
|
|
50
|
+
validateChildBatch(db, parentID, [child]);
|
|
51
|
+
if (hasEntityChild(db, parentID, child.id)) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const append = db.transaction(() => {
|
|
55
|
+
detachEntityParent(db, child.id);
|
|
56
|
+
const row = db.query(`
|
|
57
|
+
SELECT COALESCE(MAX(position), -1) AS maxPosition
|
|
58
|
+
FROM edges
|
|
59
|
+
WHERE parent_id = $parentID
|
|
60
|
+
`).get({ $parentID: parentID });
|
|
61
|
+
db.query(`
|
|
62
|
+
INSERT INTO edges (parent_id, child_id, position)
|
|
63
|
+
VALUES ($parentID, $childID, $position)
|
|
64
|
+
`).run({
|
|
65
|
+
$parentID: parentID,
|
|
66
|
+
$childID: child.id,
|
|
67
|
+
$position: row.maxPosition + 1,
|
|
68
|
+
});
|
|
69
|
+
validateNoEntityCycles(db);
|
|
70
|
+
});
|
|
71
|
+
append();
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Reads root object IDs ordered under the database.
|
|
75
|
+
* @param db - The database to read
|
|
76
|
+
* @param databaseID - The database ID whose root objects should be listed
|
|
77
|
+
* @returns Ordered root object IDs
|
|
78
|
+
*/
|
|
79
|
+
export function getDatabaseRootObjects(db, databaseID) {
|
|
80
|
+
validateDatabaseParent(db, databaseID);
|
|
81
|
+
const rows = db.query(`
|
|
82
|
+
SELECT child_id AS id
|
|
83
|
+
FROM edges
|
|
84
|
+
JOIN nodes ON nodes.id = edges.child_id
|
|
85
|
+
WHERE parent_id = $databaseID
|
|
86
|
+
AND nodes.type = 'object'
|
|
87
|
+
ORDER BY position
|
|
88
|
+
`).all({ $databaseID: databaseID });
|
|
89
|
+
return rows.map((row) => row.id);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Reads direct child entity references for a database, object, or block parent.
|
|
93
|
+
* @param db - The database containing the parent
|
|
94
|
+
* @param parentID - The database or entity parent ID
|
|
95
|
+
* @returns Ordered child entity references
|
|
96
|
+
*/
|
|
97
|
+
export function getDirectEntityChildren(db, parentID) {
|
|
98
|
+
validateParent(db, parentID);
|
|
99
|
+
const rows = db.query(`
|
|
100
|
+
SELECT
|
|
101
|
+
child_id AS id,
|
|
102
|
+
nodes.type,
|
|
103
|
+
position
|
|
104
|
+
FROM edges
|
|
105
|
+
JOIN nodes ON nodes.id = edges.child_id
|
|
106
|
+
WHERE parent_id = $parentID
|
|
107
|
+
ORDER BY position
|
|
108
|
+
`).all({ $parentID: parentID });
|
|
109
|
+
return rows.map((row) => {
|
|
110
|
+
if (row.type === "database") {
|
|
111
|
+
throw new Error(`Database cannot be a child entity: ${row.id}`);
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
type: row.type,
|
|
115
|
+
id: row.id,
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Replaces direct children for one or more parents.
|
|
121
|
+
* @param db - The database containing the parents and children
|
|
122
|
+
* @param desiredChildrenByParent - Complete desired child lists keyed by parent ID
|
|
123
|
+
*/
|
|
124
|
+
export function replaceEntityChildren(db, desiredChildrenByParent) {
|
|
125
|
+
if (desiredChildrenByParent.size === 0) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const replace = db.transaction(() => {
|
|
129
|
+
const incomingChildIDs = new Set();
|
|
130
|
+
for (const [parentID, children] of desiredChildrenByParent) {
|
|
131
|
+
validateParent(db, parentID);
|
|
132
|
+
validateChildBatch(db, parentID, children);
|
|
133
|
+
for (const child of children) {
|
|
134
|
+
if (incomingChildIDs.has(child.id)) {
|
|
135
|
+
throw new Error(`Duplicate child entity: ${child.id}`);
|
|
136
|
+
}
|
|
137
|
+
incomingChildIDs.add(child.id);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
for (const parentID of desiredChildrenByParent.keys()) {
|
|
141
|
+
db.query(`
|
|
142
|
+
DELETE FROM edges
|
|
143
|
+
WHERE parent_id = $parentID
|
|
144
|
+
`).run({ $parentID: parentID });
|
|
145
|
+
}
|
|
146
|
+
const insert = db.query(`
|
|
147
|
+
INSERT INTO edges (parent_id, child_id, position)
|
|
148
|
+
VALUES ($parentID, $childID, $position)
|
|
149
|
+
`);
|
|
150
|
+
for (const [parentID, children] of desiredChildrenByParent) {
|
|
151
|
+
children.forEach((child, position) => {
|
|
152
|
+
detachEntityParent(db, child.id);
|
|
153
|
+
insert.run({
|
|
154
|
+
$parentID: parentID,
|
|
155
|
+
$childID: child.id,
|
|
156
|
+
$position: position,
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
validateNoEntityCycles(db);
|
|
161
|
+
});
|
|
162
|
+
replace();
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Reads direct child IDs without joining subtype rows.
|
|
166
|
+
* @param db - The database containing the parent
|
|
167
|
+
* @param parentID - The parent ID to read
|
|
168
|
+
* @returns Ordered child IDs
|
|
169
|
+
*/
|
|
170
|
+
export function getDirectEntityChildIDs(db, parentID) {
|
|
171
|
+
return getDirectEntityChildren(db, parentID).map((child) => child.id);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Removes the current parent edge for a child so it can be reparented or detached.
|
|
175
|
+
* @param db - The database containing the edge
|
|
176
|
+
* @param childID - The child ID to detach
|
|
177
|
+
*/
|
|
178
|
+
export function detachEntityParent(db, childID) {
|
|
179
|
+
db.query(`
|
|
180
|
+
DELETE FROM edges
|
|
181
|
+
WHERE child_id = $childID
|
|
182
|
+
`).run({ $childID: childID });
|
|
183
|
+
}
|
|
184
|
+
/** Checks whether any parent edge references an entity. */
|
|
185
|
+
export function hasEntityParent(db, id) {
|
|
186
|
+
return db.query(`
|
|
187
|
+
SELECT 1
|
|
188
|
+
FROM edges
|
|
189
|
+
WHERE child_id = $id
|
|
190
|
+
`).get({ $id: id }) !== null;
|
|
191
|
+
}
|
|
192
|
+
/** Checks whether a direct parent/child edge already exists. */
|
|
193
|
+
function hasEntityChild(db, parentID, childID) {
|
|
194
|
+
return db.query(`
|
|
195
|
+
SELECT 1
|
|
196
|
+
FROM edges
|
|
197
|
+
WHERE parent_id = $parentID
|
|
198
|
+
AND child_id = $childID
|
|
199
|
+
`).get({
|
|
200
|
+
$parentID: parentID,
|
|
201
|
+
$childID: childID,
|
|
202
|
+
}) !== null;
|
|
203
|
+
}
|
|
204
|
+
/** Validates that an entity reference uses the correct ID prefix for its type. */
|
|
205
|
+
function validateEntityReference(reference) {
|
|
206
|
+
if (reference.type === "database") {
|
|
207
|
+
throw new Error(`Database cannot be a child entity: ${reference.id}`);
|
|
208
|
+
}
|
|
209
|
+
if (reference.type === "object" && !reference.id.startsWith(`${ObjectPrefix}_`)) {
|
|
210
|
+
throw new Error(`Invalid object id: ${reference.id}`);
|
|
211
|
+
}
|
|
212
|
+
if (reference.type === "block" && !reference.id.startsWith(`${BlockPrefix}_`)) {
|
|
213
|
+
throw new Error(`Invalid block id: ${reference.id}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/** Validates that an entity exists and matches the requested type. */
|
|
217
|
+
function validateExistingEntity(db, reference) {
|
|
218
|
+
validateEntityReference(reference);
|
|
219
|
+
const type = getStoredNodeType(db, reference.id);
|
|
220
|
+
if (type === undefined) {
|
|
221
|
+
throw new Error(`Entity not found: ${reference.id}`);
|
|
222
|
+
}
|
|
223
|
+
if (type !== reference.type) {
|
|
224
|
+
throw new Error(`Entity ${reference.id} is a ${type}, not a ${reference.type}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/** Validates one direct child list for parent/child type rules and duplicates. */
|
|
228
|
+
function validateChildBatch(db, parentID, children) {
|
|
229
|
+
const childIDs = new Set();
|
|
230
|
+
for (const child of children) {
|
|
231
|
+
validateExistingEntity(db, child);
|
|
232
|
+
if (parentID === child.id) {
|
|
233
|
+
throw new Error(`Entity ${child.id} cannot parent itself`);
|
|
234
|
+
}
|
|
235
|
+
if (parentID.startsWith(`${DatabasePrefix}_`) && child.type === "block") {
|
|
236
|
+
throw new Error(`Database children must be objects: ${child.id}`);
|
|
237
|
+
}
|
|
238
|
+
if (childIDs.has(child.id)) {
|
|
239
|
+
throw new Error(`Duplicate child entity: ${child.id}`);
|
|
240
|
+
}
|
|
241
|
+
childIDs.add(child.id);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/** Validates that a database, object, or block parent exists. */
|
|
245
|
+
function validateParent(db, parentID) {
|
|
246
|
+
if (parentID.startsWith(`${DatabasePrefix}_`)) {
|
|
247
|
+
validateDatabaseParent(db, parentID);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (!isStoredNode(db, parentID)) {
|
|
251
|
+
throw new Error(`Parent entity not found: ${parentID}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/** Validates that a database parent exists. */
|
|
255
|
+
function validateDatabaseParent(db, databaseID) {
|
|
256
|
+
if (!databaseID.startsWith(`${DatabasePrefix}_`)) {
|
|
257
|
+
throw new Error(`Invalid database id: ${databaseID}`);
|
|
258
|
+
}
|
|
259
|
+
const row = db.query(`
|
|
260
|
+
SELECT nodes.type
|
|
261
|
+
FROM "database"
|
|
262
|
+
JOIN nodes ON nodes.id = "database".id
|
|
263
|
+
WHERE "database".id = $id
|
|
264
|
+
`).get({ $id: databaseID });
|
|
265
|
+
if (!row) {
|
|
266
|
+
throw new Error(`Database not found: ${databaseID}`);
|
|
267
|
+
}
|
|
268
|
+
if (row.type !== "database") {
|
|
269
|
+
throw new Error(`Entity ${databaseID} is a ${row.type}, not a database`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/** Validates that global containment edges do not contain object/block cycles. */
|
|
273
|
+
function validateNoEntityCycles(db) {
|
|
274
|
+
const rows = db.query(`
|
|
275
|
+
SELECT parent_id AS parentID, child_id AS childID
|
|
276
|
+
FROM edges
|
|
277
|
+
`).all();
|
|
278
|
+
const childrenByParent = new Map();
|
|
279
|
+
for (const row of rows) {
|
|
280
|
+
if (row.parentID.startsWith(`${DatabasePrefix}_`)) {
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
const children = childrenByParent.get(row.parentID) ?? [];
|
|
284
|
+
children.push(row.childID);
|
|
285
|
+
childrenByParent.set(row.parentID, children);
|
|
286
|
+
}
|
|
287
|
+
const visiting = new Set();
|
|
288
|
+
const visited = new Set();
|
|
289
|
+
const visit = (id) => {
|
|
290
|
+
if (visiting.has(id)) {
|
|
291
|
+
throw new Error(`Entity cycle detected at ${id}`);
|
|
292
|
+
}
|
|
293
|
+
if (visited.has(id)) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
visiting.add(id);
|
|
297
|
+
for (const childID of childrenByParent.get(id) ?? []) {
|
|
298
|
+
visit(childID);
|
|
299
|
+
}
|
|
300
|
+
visiting.delete(id);
|
|
301
|
+
visited.add(id);
|
|
302
|
+
};
|
|
303
|
+
for (const parentID of childrenByParent.keys()) {
|
|
304
|
+
visit(parentID);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import type { DBMetadata } from "../../types/database";
|
|
3
|
+
export declare const SchemaVersion = "0.0.3";
|
|
4
|
+
/**
|
|
5
|
+
* Opens a database, initializes its schema, and creates its metadata when needed.
|
|
6
|
+
* @param path - The path of the SQLite database
|
|
7
|
+
* @param name - The optional database name
|
|
8
|
+
* @returns The initialized SQLite database connection
|
|
9
|
+
*/
|
|
10
|
+
export declare function initDatabase(path: string, name?: string): Database;
|
|
11
|
+
/**
|
|
12
|
+
* Opens an existing initialized database without re-running schema setup.
|
|
13
|
+
* @param path - The path of the SQLite database
|
|
14
|
+
* @returns The opened SQLite database connection
|
|
15
|
+
*/
|
|
16
|
+
export declare function openDatabase(path: string): Database;
|
|
17
|
+
/**
|
|
18
|
+
* Reads the metadata associated with a database.
|
|
19
|
+
* @param db - The database to read
|
|
20
|
+
* @returns The database metadata
|
|
21
|
+
*/
|
|
22
|
+
export declare function getDatabaseMetadata(db: Database): DBMetadata;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { createDatabaseID } from "../../utils/id";
|
|
3
|
+
import schema from "./schema.sql" with { type: "text" };
|
|
4
|
+
export const SchemaVersion = "0.0.3";
|
|
5
|
+
/**
|
|
6
|
+
* Opens a database, initializes its schema, and creates its metadata when needed.
|
|
7
|
+
* @param path - The path of the SQLite database
|
|
8
|
+
* @param name - The optional database name
|
|
9
|
+
* @returns The initialized SQLite database connection
|
|
10
|
+
*/
|
|
11
|
+
export function initDatabase(path, name) {
|
|
12
|
+
const db = new Database(path);
|
|
13
|
+
try {
|
|
14
|
+
db.exec("PRAGMA busy_timeout = 5000;");
|
|
15
|
+
db.exec("PRAGMA foreign_keys = ON;");
|
|
16
|
+
const isFresh = isEmptyDatabase(db);
|
|
17
|
+
if (!isFresh) {
|
|
18
|
+
validateDatabaseMetadata(db);
|
|
19
|
+
}
|
|
20
|
+
db.exec("PRAGMA journal_mode = WAL;");
|
|
21
|
+
db.exec(schema);
|
|
22
|
+
if (isFresh) {
|
|
23
|
+
const insertMetadata = db.transaction(() => {
|
|
24
|
+
const databaseID = createDatabaseID();
|
|
25
|
+
db.query(`
|
|
26
|
+
INSERT INTO nodes (id, type)
|
|
27
|
+
VALUES ($id, 'database')
|
|
28
|
+
`).run({ $id: databaseID });
|
|
29
|
+
db.query(`
|
|
30
|
+
INSERT INTO "database" (id, name, schema_version)
|
|
31
|
+
VALUES ($id, $name, $schemaVersion)
|
|
32
|
+
`).run({
|
|
33
|
+
$id: databaseID,
|
|
34
|
+
$name: name ?? null,
|
|
35
|
+
$schemaVersion: SchemaVersion,
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
insertMetadata();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
db.close();
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
return db;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Opens an existing initialized database without re-running schema setup.
|
|
49
|
+
* @param path - The path of the SQLite database
|
|
50
|
+
* @returns The opened SQLite database connection
|
|
51
|
+
*/
|
|
52
|
+
export function openDatabase(path) {
|
|
53
|
+
const db = new Database(path, { create: false, readwrite: true });
|
|
54
|
+
try {
|
|
55
|
+
db.exec("PRAGMA busy_timeout = 5000;");
|
|
56
|
+
db.exec("PRAGMA foreign_keys = ON;");
|
|
57
|
+
validateDatabaseMetadata(db);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
db.close();
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
return db;
|
|
64
|
+
}
|
|
65
|
+
function isEmptyDatabase(db) {
|
|
66
|
+
const row = db.query(`
|
|
67
|
+
SELECT COUNT(*) AS count
|
|
68
|
+
FROM sqlite_master
|
|
69
|
+
WHERE name NOT LIKE 'sqlite_%'
|
|
70
|
+
`).get();
|
|
71
|
+
return row.count === 0;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Reads the metadata associated with a database.
|
|
75
|
+
* @param db - The database to read
|
|
76
|
+
* @returns The database metadata
|
|
77
|
+
*/
|
|
78
|
+
export function getDatabaseMetadata(db) {
|
|
79
|
+
const rows = db.query(`
|
|
80
|
+
SELECT
|
|
81
|
+
id,
|
|
82
|
+
name,
|
|
83
|
+
schema_version AS schemaVersion
|
|
84
|
+
FROM "database"
|
|
85
|
+
LIMIT 2
|
|
86
|
+
`).all();
|
|
87
|
+
if (rows.length !== 1) {
|
|
88
|
+
throw new Error("Database metadata not found");
|
|
89
|
+
}
|
|
90
|
+
const row = rows[0];
|
|
91
|
+
return {
|
|
92
|
+
id: row.id,
|
|
93
|
+
name: row.name ?? undefined,
|
|
94
|
+
schemaVersion: row.schemaVersion,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function validateDatabaseMetadata(db) {
|
|
98
|
+
let metadata;
|
|
99
|
+
try {
|
|
100
|
+
metadata = getDatabaseMetadata(db);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
throw new Error(`Incompatible database schema; expected valid version ${SchemaVersion} metadata`);
|
|
104
|
+
}
|
|
105
|
+
if (metadata.schemaVersion !== SchemaVersion) {
|
|
106
|
+
throw new Error(`Unsupported database schema version ${metadata.schemaVersion}; expected ${SchemaVersion}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import type { StoredEntityReference, StoredEntityType } from "../types";
|
|
3
|
+
/**
|
|
4
|
+
* Inserts one graph node row.
|
|
5
|
+
* @param db - The database containing the node
|
|
6
|
+
* @param reference - The node type and ID to insert
|
|
7
|
+
*/
|
|
8
|
+
export declare function insertStoredNode(db: Database, reference: StoredEntityReference): void;
|
|
9
|
+
/**
|
|
10
|
+
* Inserts one graph node row, or validates that an existing row has the same type.
|
|
11
|
+
* @param db - The database containing the node
|
|
12
|
+
* @param reference - The node type and ID to persist
|
|
13
|
+
*/
|
|
14
|
+
export declare function upsertStoredNode(db: Database, reference: StoredEntityReference): void;
|
|
15
|
+
/**
|
|
16
|
+
* Checks whether a graph node exists.
|
|
17
|
+
* @param db - The database to check
|
|
18
|
+
* @param id - The node ID to check
|
|
19
|
+
* @returns True if the node exists
|
|
20
|
+
*/
|
|
21
|
+
export declare function isStoredNode(db: Database, id: string): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Reads the stored node type for an ID.
|
|
24
|
+
* @param db - The database containing the node
|
|
25
|
+
* @param id - The node ID to read
|
|
26
|
+
* @returns The node type, or undefined when no node exists
|
|
27
|
+
*/
|
|
28
|
+
export declare function getStoredNodeType(db: Database, id: string): StoredEntityType | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Deletes graph nodes by ID.
|
|
31
|
+
* @param db - The database containing the nodes
|
|
32
|
+
* @param ids - The node IDs to delete
|
|
33
|
+
*/
|
|
34
|
+
export declare function deleteStoredNodes(db: Database, ids: string[]): void;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { BlockPrefix, DatabasePrefix, ObjectPrefix } from "../../utils/id";
|
|
2
|
+
/**
|
|
3
|
+
* Inserts one graph node row.
|
|
4
|
+
* @param db - The database containing the node
|
|
5
|
+
* @param reference - The node type and ID to insert
|
|
6
|
+
*/
|
|
7
|
+
export function insertStoredNode(db, reference) {
|
|
8
|
+
validateNodeReference(reference);
|
|
9
|
+
const existingType = getStoredNodeType(db, reference.id);
|
|
10
|
+
if (existingType !== undefined) {
|
|
11
|
+
throw new Error(`Entity already exists: ${reference.id}`);
|
|
12
|
+
}
|
|
13
|
+
db.query(`
|
|
14
|
+
INSERT INTO nodes (id, type)
|
|
15
|
+
VALUES ($id, $type)
|
|
16
|
+
`).run({
|
|
17
|
+
$id: reference.id,
|
|
18
|
+
$type: reference.type,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Inserts one graph node row, or validates that an existing row has the same type.
|
|
23
|
+
* @param db - The database containing the node
|
|
24
|
+
* @param reference - The node type and ID to persist
|
|
25
|
+
*/
|
|
26
|
+
export function upsertStoredNode(db, reference) {
|
|
27
|
+
validateNodeReference(reference);
|
|
28
|
+
const existingType = getStoredNodeType(db, reference.id);
|
|
29
|
+
if (existingType !== undefined) {
|
|
30
|
+
if (existingType !== reference.type) {
|
|
31
|
+
throw new Error(`Entity ${reference.id} is a ${existingType}, not a ${reference.type}`);
|
|
32
|
+
}
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
db.query(`
|
|
36
|
+
INSERT INTO nodes (id, type)
|
|
37
|
+
VALUES ($id, $type)
|
|
38
|
+
`).run({
|
|
39
|
+
$id: reference.id,
|
|
40
|
+
$type: reference.type,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Checks whether a graph node exists.
|
|
45
|
+
* @param db - The database to check
|
|
46
|
+
* @param id - The node ID to check
|
|
47
|
+
* @returns True if the node exists
|
|
48
|
+
*/
|
|
49
|
+
export function isStoredNode(db, id) {
|
|
50
|
+
return getStoredNodeType(db, id) !== undefined;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Reads the stored node type for an ID.
|
|
54
|
+
* @param db - The database containing the node
|
|
55
|
+
* @param id - The node ID to read
|
|
56
|
+
* @returns The node type, or undefined when no node exists
|
|
57
|
+
*/
|
|
58
|
+
export function getStoredNodeType(db, id) {
|
|
59
|
+
const row = db.query(`
|
|
60
|
+
SELECT type
|
|
61
|
+
FROM nodes
|
|
62
|
+
WHERE id = $id
|
|
63
|
+
`).get({ $id: id });
|
|
64
|
+
return row?.type;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Deletes graph nodes by ID.
|
|
68
|
+
* @param db - The database containing the nodes
|
|
69
|
+
* @param ids - The node IDs to delete
|
|
70
|
+
*/
|
|
71
|
+
export function deleteStoredNodes(db, ids) {
|
|
72
|
+
if (ids.length === 0) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
db.query(`
|
|
76
|
+
DELETE FROM nodes
|
|
77
|
+
WHERE id IN (SELECT value FROM json_each($ids))
|
|
78
|
+
`).run({ $ids: JSON.stringify(ids) });
|
|
79
|
+
}
|
|
80
|
+
/** Validates that a node reference uses the correct ID prefix for its type. */
|
|
81
|
+
function validateNodeReference(reference) {
|
|
82
|
+
if (reference.type === "database" && !reference.id.startsWith(`${DatabasePrefix}_`)) {
|
|
83
|
+
throw new Error(`Invalid database id: ${reference.id}`);
|
|
84
|
+
}
|
|
85
|
+
if (reference.type === "object" && !reference.id.startsWith(`${ObjectPrefix}_`)) {
|
|
86
|
+
throw new Error(`Invalid object id: ${reference.id}`);
|
|
87
|
+
}
|
|
88
|
+
if (reference.type === "block" && !reference.id.startsWith(`${BlockPrefix}_`)) {
|
|
89
|
+
throw new Error(`Invalid block id: ${reference.id}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import type { ObjID, ObjMetadata } from "../../types/object";
|
|
3
|
+
import type { StoredObject } from "../types";
|
|
4
|
+
/**
|
|
5
|
+
* Inserts an object row without adding database-root or child containment edges.
|
|
6
|
+
* @param db - The database containing the object
|
|
7
|
+
* @param metadata - The object metadata to insert
|
|
8
|
+
*/
|
|
9
|
+
export declare function insertStoredObject(db: Database, metadata: StoredObject): void;
|
|
10
|
+
/**
|
|
11
|
+
* Inserts or updates an object row without changing containment edges.
|
|
12
|
+
* @param db - The database containing the object
|
|
13
|
+
* @param metadata - The object metadata to persist
|
|
14
|
+
*/
|
|
15
|
+
export declare function upsertStoredObject(db: Database, metadata: StoredObject): void;
|
|
16
|
+
/**
|
|
17
|
+
* Reads object metadata without recursive children.
|
|
18
|
+
* @param db - The database containing the object
|
|
19
|
+
* @param objectID - The object ID to read
|
|
20
|
+
* @returns The object metadata
|
|
21
|
+
*/
|
|
22
|
+
export declare function getObjectMetadata(db: Database, objectID: ObjID): ObjMetadata;
|
|
23
|
+
/**
|
|
24
|
+
* Reads the stored object row without recursive children.
|
|
25
|
+
* @param db - The database containing the object
|
|
26
|
+
* @param objectID - The object ID to read
|
|
27
|
+
* @returns The stored object metadata
|
|
28
|
+
*/
|
|
29
|
+
export declare function getStoredObject(db: Database, objectID: ObjID): StoredObject;
|
|
30
|
+
/**
|
|
31
|
+
* Updates object metadata without changing containment edges.
|
|
32
|
+
* @param db - The database containing the object
|
|
33
|
+
* @param metadata - The object metadata to update
|
|
34
|
+
*/
|
|
35
|
+
export declare function updateObjectMetadata(db: Database, metadata: StoredObject): void;
|
|
36
|
+
/**
|
|
37
|
+
* Updates the stored object row without changing recursive children.
|
|
38
|
+
* @param db - The database containing the object
|
|
39
|
+
* @param object - The object metadata to update
|
|
40
|
+
*/
|
|
41
|
+
export declare function updateStoredObject(db: Database, object: StoredObject): void;
|
|
42
|
+
/**
|
|
43
|
+
* Deletes an object row without deleting its graph node or containment subtree.
|
|
44
|
+
* @param db - The database containing the object
|
|
45
|
+
* @param objectID - The object ID to delete
|
|
46
|
+
* @returns True if the object existed and was deleted
|
|
47
|
+
*/
|
|
48
|
+
export declare function deleteStoredObject(db: Database, objectID: ObjID): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Checks whether an object row exists.
|
|
51
|
+
* @param db - The database to check
|
|
52
|
+
* @param objectID - The object ID to check
|
|
53
|
+
* @returns True if the object exists
|
|
54
|
+
*/
|
|
55
|
+
export declare function isStoredObject(db: Database, objectID: ObjID): boolean;
|