@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,123 @@
|
|
|
1
|
+
import { ObjectPrefix } from "../../utils/id";
|
|
2
|
+
/**
|
|
3
|
+
* Inserts an object row without adding database-root or child containment edges.
|
|
4
|
+
* @param db - The database containing the object
|
|
5
|
+
* @param metadata - The object metadata to insert
|
|
6
|
+
*/
|
|
7
|
+
export function insertStoredObject(db, metadata) {
|
|
8
|
+
validateObjectMetadata(metadata);
|
|
9
|
+
db.query(`
|
|
10
|
+
INSERT INTO objects (id, name, properties)
|
|
11
|
+
VALUES ($id, $name, $properties)
|
|
12
|
+
`).run(mapObjectParameters(metadata));
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Inserts or updates an object row without changing containment edges.
|
|
16
|
+
* @param db - The database containing the object
|
|
17
|
+
* @param metadata - The object metadata to persist
|
|
18
|
+
*/
|
|
19
|
+
export function upsertStoredObject(db, metadata) {
|
|
20
|
+
validateObjectMetadata(metadata);
|
|
21
|
+
if (isStoredObject(db, metadata.id)) {
|
|
22
|
+
db.query(`
|
|
23
|
+
UPDATE objects
|
|
24
|
+
SET name = $name,
|
|
25
|
+
properties = $properties
|
|
26
|
+
WHERE id = $id
|
|
27
|
+
`).run(mapObjectParameters(metadata));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
insertStoredObject(db, metadata);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Reads object metadata without recursive children.
|
|
34
|
+
* @param db - The database containing the object
|
|
35
|
+
* @param objectID - The object ID to read
|
|
36
|
+
* @returns The object metadata
|
|
37
|
+
*/
|
|
38
|
+
export function getObjectMetadata(db, objectID) {
|
|
39
|
+
const row = db.query(`
|
|
40
|
+
SELECT id, name, properties
|
|
41
|
+
FROM objects
|
|
42
|
+
WHERE id = $id
|
|
43
|
+
`).get({ $id: objectID });
|
|
44
|
+
if (!row) {
|
|
45
|
+
throw new Error(`Object not found: ${objectID}`);
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
id: row.id,
|
|
49
|
+
type: "object",
|
|
50
|
+
name: row.name,
|
|
51
|
+
properties: JSON.parse(row.properties),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Reads the stored object row without recursive children.
|
|
56
|
+
* @param db - The database containing the object
|
|
57
|
+
* @param objectID - The object ID to read
|
|
58
|
+
* @returns The stored object metadata
|
|
59
|
+
*/
|
|
60
|
+
export function getStoredObject(db, objectID) {
|
|
61
|
+
return getObjectMetadata(db, objectID);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Updates object metadata without changing containment edges.
|
|
65
|
+
* @param db - The database containing the object
|
|
66
|
+
* @param metadata - The object metadata to update
|
|
67
|
+
*/
|
|
68
|
+
export function updateObjectMetadata(db, metadata) {
|
|
69
|
+
validateObjectMetadata(metadata);
|
|
70
|
+
if (!isStoredObject(db, metadata.id)) {
|
|
71
|
+
throw new Error(`Object not found: ${metadata.id}`);
|
|
72
|
+
}
|
|
73
|
+
db.query(`
|
|
74
|
+
UPDATE objects
|
|
75
|
+
SET name = $name,
|
|
76
|
+
properties = $properties
|
|
77
|
+
WHERE id = $id
|
|
78
|
+
`).run(mapObjectParameters(metadata));
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Updates the stored object row without changing recursive children.
|
|
82
|
+
* @param db - The database containing the object
|
|
83
|
+
* @param object - The object metadata to update
|
|
84
|
+
*/
|
|
85
|
+
export function updateStoredObject(db, object) {
|
|
86
|
+
updateObjectMetadata(db, object);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Deletes an object row without deleting its graph node or containment subtree.
|
|
90
|
+
* @param db - The database containing the object
|
|
91
|
+
* @param objectID - The object ID to delete
|
|
92
|
+
* @returns True if the object existed and was deleted
|
|
93
|
+
*/
|
|
94
|
+
export function deleteStoredObject(db, objectID) {
|
|
95
|
+
const result = db.query(`
|
|
96
|
+
DELETE FROM objects
|
|
97
|
+
WHERE id = $id
|
|
98
|
+
`).run({ $id: objectID });
|
|
99
|
+
return result.changes > 0;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Checks whether an object row exists.
|
|
103
|
+
* @param db - The database to check
|
|
104
|
+
* @param objectID - The object ID to check
|
|
105
|
+
* @returns True if the object exists
|
|
106
|
+
*/
|
|
107
|
+
export function isStoredObject(db, objectID) {
|
|
108
|
+
return db.query("SELECT 1 FROM objects WHERE id = $id").get({ $id: objectID }) !== null;
|
|
109
|
+
}
|
|
110
|
+
/** Validates an object ID. */
|
|
111
|
+
function validateObjectMetadata(metadata) {
|
|
112
|
+
if (!metadata.id.startsWith(`${ObjectPrefix}_`)) {
|
|
113
|
+
throw new Error(`Invalid object id: ${metadata.id}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/** Maps object metadata to SQLite named parameters. */
|
|
117
|
+
function mapObjectParameters(metadata) {
|
|
118
|
+
return {
|
|
119
|
+
$id: metadata.id,
|
|
120
|
+
$name: metadata.name,
|
|
121
|
+
$properties: JSON.stringify(metadata.properties ?? {}),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS "database" (
|
|
2
|
+
id TEXT PRIMARY KEY
|
|
3
|
+
REFERENCES nodes(id)
|
|
4
|
+
ON DELETE RESTRICT
|
|
5
|
+
DEFERRABLE INITIALLY DEFERRED,
|
|
6
|
+
name TEXT,
|
|
7
|
+
schema_version TEXT NOT NULL
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
CREATE TRIGGER IF NOT EXISTS database_singleton_insert
|
|
11
|
+
BEFORE INSERT ON "database"
|
|
12
|
+
WHEN (SELECT COUNT(*) FROM "database") >= 1
|
|
13
|
+
BEGIN
|
|
14
|
+
SELECT RAISE(ABORT, 'database metadata already exists');
|
|
15
|
+
END;
|
|
16
|
+
|
|
17
|
+
CREATE TABLE IF NOT EXISTS nodes (
|
|
18
|
+
id TEXT PRIMARY KEY,
|
|
19
|
+
type TEXT NOT NULL CHECK (type IN ('database', 'object', 'block'))
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
CREATE TABLE IF NOT EXISTS objects (
|
|
23
|
+
id TEXT PRIMARY KEY
|
|
24
|
+
REFERENCES nodes(id)
|
|
25
|
+
ON DELETE CASCADE
|
|
26
|
+
DEFERRABLE INITIALLY DEFERRED,
|
|
27
|
+
name TEXT NOT NULL,
|
|
28
|
+
properties TEXT NOT NULL DEFAULT '{}'
|
|
29
|
+
CHECK (json_valid(properties))
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
CREATE TABLE IF NOT EXISTS blocks (
|
|
33
|
+
id TEXT PRIMARY KEY
|
|
34
|
+
REFERENCES nodes(id)
|
|
35
|
+
ON DELETE CASCADE
|
|
36
|
+
DEFERRABLE INITIALLY DEFERRED,
|
|
37
|
+
content TEXT NOT NULL,
|
|
38
|
+
properties TEXT NOT NULL DEFAULT '{}'
|
|
39
|
+
CHECK (json_valid(properties))
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
CREATE TABLE IF NOT EXISTS edges (
|
|
43
|
+
parent_id TEXT NOT NULL
|
|
44
|
+
REFERENCES nodes(id)
|
|
45
|
+
ON DELETE CASCADE
|
|
46
|
+
DEFERRABLE INITIALLY DEFERRED,
|
|
47
|
+
child_id TEXT NOT NULL
|
|
48
|
+
REFERENCES nodes(id)
|
|
49
|
+
ON DELETE CASCADE
|
|
50
|
+
DEFERRABLE INITIALLY DEFERRED,
|
|
51
|
+
position INTEGER NOT NULL CHECK (position >= 0),
|
|
52
|
+
PRIMARY KEY (parent_id, child_id),
|
|
53
|
+
UNIQUE (child_id),
|
|
54
|
+
UNIQUE (parent_id, position),
|
|
55
|
+
CHECK (parent_id <> child_id)
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
CREATE INDEX IF NOT EXISTS edges_parent_id_idx
|
|
59
|
+
ON edges(parent_id);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import type { Block, BlockID, BlockMetadata } from "../types/block";
|
|
3
|
+
import type { DBMetadata } from "../types/database";
|
|
4
|
+
import type { Entity, EntityReference } from "../types/graph";
|
|
5
|
+
import type { Obj, ObjID, ObjMetadata } from "../types/object";
|
|
6
|
+
import type { StoredEntity, StoredEntityID, StoredEntityReference } from "./types";
|
|
7
|
+
export type { StoredBlock, StoredEntity, StoredEntityID, StoredEntityReference, StoredEntityType, StoredObject, } from "./types";
|
|
8
|
+
export type SearchType = "object" | "block";
|
|
9
|
+
export interface SearchResult {
|
|
10
|
+
type: SearchType;
|
|
11
|
+
id: string;
|
|
12
|
+
label: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function initializeStorage(path: string, name?: string): Database;
|
|
15
|
+
export declare function openStorage(path: string): Database;
|
|
16
|
+
export declare function readDatabaseMetadata(db: Database): DBMetadata;
|
|
17
|
+
/**
|
|
18
|
+
* Create an entity with parent placement.
|
|
19
|
+
*
|
|
20
|
+
* Objects: parentID defaults to database root if unspecified.
|
|
21
|
+
* Blocks require an explicit object or block parent.
|
|
22
|
+
* @param db
|
|
23
|
+
* @param entity
|
|
24
|
+
* @param parentID
|
|
25
|
+
*/
|
|
26
|
+
export declare function createEntity(db: Database, entity: StoredEntity, parentID?: StoredEntityID): void;
|
|
27
|
+
export declare function objectExists(db: Database, objectID: ObjID): boolean;
|
|
28
|
+
export declare function blockExists(db: Database, blockID: BlockID): boolean;
|
|
29
|
+
export declare function entityExists(db: Database, id: string): boolean;
|
|
30
|
+
export declare function readObjectTree(db: Database, objectID: ObjID): Obj;
|
|
31
|
+
export declare function readBlockTree(db: Database, blockID: BlockID): Block;
|
|
32
|
+
export declare function readEntityTree(db: Database, entityID: string, visited?: Set<string>): Entity;
|
|
33
|
+
export declare function readDatabaseRootObjects(db: Database): ObjID[];
|
|
34
|
+
export declare function readObjectMetadata(db: Database, objectID: ObjID): ObjMetadata;
|
|
35
|
+
export declare function readBlockMetadata(db: Database, blockID: BlockID): BlockMetadata;
|
|
36
|
+
export declare function readDirectEntityChildIDs(db: Database, parentID: string): StoredEntityID[];
|
|
37
|
+
export declare function readEntityParent(db: Database, entityID: string): StoredEntityReference | undefined;
|
|
38
|
+
export declare function readEntityParentID(db: Database, entityID: string): StoredEntityID | null;
|
|
39
|
+
export declare function persistEntityTree(db: Database, root: Entity): void;
|
|
40
|
+
export declare function writeEntityTree(db: Database, root: Entity, parentID?: StoredEntityID): Entity;
|
|
41
|
+
export declare function moveObjectToDatabaseRoot(db: Database, objectID: ObjID): void;
|
|
42
|
+
export declare function attachEntityChild(db: Database, parentID: StoredEntityID, child: EntityReference): void;
|
|
43
|
+
export declare function replaceDirectEntityChildren(db: Database, desiredChildrenByParent: Map<string, EntityReference[]>): void;
|
|
44
|
+
export declare function deleteObjectTree(db: Database, objectID: ObjID): boolean;
|
|
45
|
+
export declare function deleteBlockTree(db: Database, blockID: BlockID): boolean;
|
|
46
|
+
export declare function deleteEntityTree(db: Database, entityID: string): boolean;
|
|
47
|
+
export declare function searchEntities(db: Database, query: string, type?: SearchType): SearchResult[];
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { getBlockMetadata, getStoredBlock, insertStoredBlock, isStoredBlock, upsertStoredBlock, } from "./db/blocks";
|
|
2
|
+
import { appendEntityChild, appendDatabaseRootObject, getDatabaseRootObjects, getDirectEntityChildren, getDirectEntityChildIDs, getEntityParent, replaceEntityChildren, } from "./db/edges";
|
|
3
|
+
import { getDatabaseMetadata, initDatabase, openDatabase as openStoredDatabase, } from "./db/init";
|
|
4
|
+
import { deleteStoredNodes, getStoredNodeType, insertStoredNode, isStoredNode, upsertStoredNode, } from "./db/nodes";
|
|
5
|
+
import { getObjectMetadata, getStoredObject, insertStoredObject, isStoredObject, upsertStoredObject, } from "./db/objects";
|
|
6
|
+
// DB functionality
|
|
7
|
+
export function initializeStorage(path, name) {
|
|
8
|
+
return initDatabase(path, name);
|
|
9
|
+
}
|
|
10
|
+
export function openStorage(path) {
|
|
11
|
+
return openStoredDatabase(path);
|
|
12
|
+
}
|
|
13
|
+
export function readDatabaseMetadata(db) {
|
|
14
|
+
return getDatabaseMetadata(db);
|
|
15
|
+
}
|
|
16
|
+
// Creation and existence
|
|
17
|
+
/**
|
|
18
|
+
* Create an entity with parent placement.
|
|
19
|
+
*
|
|
20
|
+
* Objects: parentID defaults to database root if unspecified.
|
|
21
|
+
* Blocks require an explicit object or block parent.
|
|
22
|
+
* @param db
|
|
23
|
+
* @param entity
|
|
24
|
+
* @param parentID
|
|
25
|
+
*/
|
|
26
|
+
export function createEntity(db, entity, parentID) {
|
|
27
|
+
const create = db.transaction(() => {
|
|
28
|
+
insertStoredNode(db, {
|
|
29
|
+
type: entity.type,
|
|
30
|
+
id: entity.id,
|
|
31
|
+
});
|
|
32
|
+
if (entity.type === "object") {
|
|
33
|
+
insertStoredObject(db, entity);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
insertStoredBlock(db, entity);
|
|
37
|
+
}
|
|
38
|
+
appendEntityChild(db, resolveRootParentID(db, entity.type, parentID), {
|
|
39
|
+
type: entity.type,
|
|
40
|
+
id: entity.id,
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
create();
|
|
44
|
+
}
|
|
45
|
+
export function objectExists(db, objectID) {
|
|
46
|
+
return isStoredObject(db, objectID);
|
|
47
|
+
}
|
|
48
|
+
export function blockExists(db, blockID) {
|
|
49
|
+
return isStoredBlock(db, blockID);
|
|
50
|
+
}
|
|
51
|
+
export function entityExists(db, id) {
|
|
52
|
+
return isStoredNode(db, id);
|
|
53
|
+
}
|
|
54
|
+
// Read
|
|
55
|
+
export function readObjectTree(db, objectID) {
|
|
56
|
+
const type = getStoredNodeType(db, objectID);
|
|
57
|
+
if (type === undefined || type !== "object") {
|
|
58
|
+
throw new Error(`Object not found: ${objectID}`);
|
|
59
|
+
}
|
|
60
|
+
const entity = readEntityTree(db, objectID);
|
|
61
|
+
if (entity.type !== "object") {
|
|
62
|
+
throw new Error(`Object not found: ${objectID}`);
|
|
63
|
+
}
|
|
64
|
+
return entity;
|
|
65
|
+
}
|
|
66
|
+
export function readBlockTree(db, blockID) {
|
|
67
|
+
const type = getStoredNodeType(db, blockID);
|
|
68
|
+
if (type === undefined || type !== "block") {
|
|
69
|
+
throw new Error(`Block not found: ${blockID}`);
|
|
70
|
+
}
|
|
71
|
+
const entity = readEntityTree(db, blockID);
|
|
72
|
+
if (entity.type !== "block") {
|
|
73
|
+
throw new Error(`Block not found: ${blockID}`);
|
|
74
|
+
}
|
|
75
|
+
return entity;
|
|
76
|
+
}
|
|
77
|
+
export function readEntityTree(db, entityID, visited = new Set()) {
|
|
78
|
+
if (visited.has(entityID)) {
|
|
79
|
+
throw new Error(`Entity cycle detected at ${entityID}`);
|
|
80
|
+
}
|
|
81
|
+
visited.add(entityID);
|
|
82
|
+
const type = getStoredNodeType(db, entityID);
|
|
83
|
+
if (type === undefined) {
|
|
84
|
+
throw new Error(`Entity not found: ${entityID}`);
|
|
85
|
+
}
|
|
86
|
+
if (type === "database") {
|
|
87
|
+
throw new Error(`Database cannot be read as a public entity: ${entityID}`);
|
|
88
|
+
}
|
|
89
|
+
const children = getDirectEntityChildren(db, entityID).map((child) => readEntityTree(db, child.id, new Set(visited)));
|
|
90
|
+
if (type === "object") {
|
|
91
|
+
return {
|
|
92
|
+
...getStoredObject(db, entityID),
|
|
93
|
+
children,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
...getStoredBlock(db, entityID),
|
|
98
|
+
children,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
export function readDatabaseRootObjects(db) {
|
|
102
|
+
return getDatabaseRootObjects(db, getDatabaseMetadata(db).id);
|
|
103
|
+
}
|
|
104
|
+
export function readObjectMetadata(db, objectID) {
|
|
105
|
+
return getObjectMetadata(db, objectID);
|
|
106
|
+
}
|
|
107
|
+
export function readBlockMetadata(db, blockID) {
|
|
108
|
+
return getBlockMetadata(db, blockID);
|
|
109
|
+
}
|
|
110
|
+
export function readDirectEntityChildIDs(db, parentID) {
|
|
111
|
+
return getDirectEntityChildIDs(db, parentID);
|
|
112
|
+
}
|
|
113
|
+
export function readEntityParent(db, entityID) {
|
|
114
|
+
return getEntityParent(db, entityID);
|
|
115
|
+
}
|
|
116
|
+
export function readEntityParentID(db, entityID) {
|
|
117
|
+
return readEntityParent(db, entityID)?.id ?? null;
|
|
118
|
+
}
|
|
119
|
+
// Insert and write
|
|
120
|
+
export function persistEntityTree(db, root) {
|
|
121
|
+
const visit = (entity) => {
|
|
122
|
+
upsertStoredNode(db, {
|
|
123
|
+
type: entity.type,
|
|
124
|
+
id: entity.id,
|
|
125
|
+
});
|
|
126
|
+
if (entity.type === "object") {
|
|
127
|
+
upsertStoredObject(db, {
|
|
128
|
+
id: entity.id,
|
|
129
|
+
type: "object",
|
|
130
|
+
name: entity.name,
|
|
131
|
+
properties: entity.properties ?? {},
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
upsertStoredBlock(db, {
|
|
136
|
+
id: entity.id,
|
|
137
|
+
type: "block",
|
|
138
|
+
content: entity.content,
|
|
139
|
+
properties: entity.properties ?? {},
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
entity.children.forEach(visit);
|
|
143
|
+
};
|
|
144
|
+
visit(root);
|
|
145
|
+
}
|
|
146
|
+
export function writeEntityTree(db, root, parentID) {
|
|
147
|
+
const write = db.transaction(() => {
|
|
148
|
+
persistEntityTree(db, root);
|
|
149
|
+
appendEntityChild(db, resolveRootParentID(db, root.type, parentID), {
|
|
150
|
+
type: root.type,
|
|
151
|
+
id: root.id,
|
|
152
|
+
});
|
|
153
|
+
const desiredChildrenByParent = buildChildMap(root);
|
|
154
|
+
const submittedIDs = collectSubmittedEntityIDs(root);
|
|
155
|
+
const omittedChildIDs = collectOmittedChildIDs(db, desiredChildrenByParent, submittedIDs);
|
|
156
|
+
replaceEntityChildren(db, desiredChildrenByParent);
|
|
157
|
+
deleteOmittedEntitySubtrees(db, omittedChildIDs);
|
|
158
|
+
});
|
|
159
|
+
write();
|
|
160
|
+
return readEntityTree(db, root.id);
|
|
161
|
+
}
|
|
162
|
+
export function moveObjectToDatabaseRoot(db, objectID) {
|
|
163
|
+
appendDatabaseRootObject(db, getDatabaseMetadata(db).id, objectID);
|
|
164
|
+
}
|
|
165
|
+
export function attachEntityChild(db, parentID, child) {
|
|
166
|
+
appendEntityChild(db, parentID, child);
|
|
167
|
+
}
|
|
168
|
+
export function replaceDirectEntityChildren(db, desiredChildrenByParent) {
|
|
169
|
+
replaceEntityChildren(db, desiredChildrenByParent);
|
|
170
|
+
}
|
|
171
|
+
// Deletion
|
|
172
|
+
export function deleteObjectTree(db, objectID) {
|
|
173
|
+
const type = getStoredNodeType(db, objectID);
|
|
174
|
+
if (type === undefined) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
if (type !== "object") {
|
|
178
|
+
throw new Error(`Object not found: ${objectID}`);
|
|
179
|
+
}
|
|
180
|
+
return deleteEntityTree(db, objectID);
|
|
181
|
+
}
|
|
182
|
+
export function deleteBlockTree(db, blockID) {
|
|
183
|
+
const type = getStoredNodeType(db, blockID);
|
|
184
|
+
if (type === undefined) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
if (type !== "block") {
|
|
188
|
+
throw new Error(`Block not found: ${blockID}`);
|
|
189
|
+
}
|
|
190
|
+
return deleteEntityTree(db, blockID);
|
|
191
|
+
}
|
|
192
|
+
export function deleteEntityTree(db, entityID) {
|
|
193
|
+
const type = getStoredNodeType(db, entityID);
|
|
194
|
+
if (type === undefined) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
if (type === "database") {
|
|
198
|
+
throw new Error(`Database cannot be deleted as a public entity: ${entityID}`);
|
|
199
|
+
}
|
|
200
|
+
const ids = collectEntitySubtreeIDs(db, entityID);
|
|
201
|
+
deleteStoredNodes(db, ids);
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
// Search functionality
|
|
205
|
+
export function searchEntities(db, query, type) {
|
|
206
|
+
const types = type === undefined
|
|
207
|
+
? ["object", "block"]
|
|
208
|
+
: [type];
|
|
209
|
+
const rows = [];
|
|
210
|
+
const parameters = { $query: query };
|
|
211
|
+
if (types.includes("object")) {
|
|
212
|
+
rows.push(...db.query(`
|
|
213
|
+
SELECT 'object' AS type, id, name AS label
|
|
214
|
+
FROM objects
|
|
215
|
+
WHERE instr(lower(name), lower($query)) > 0
|
|
216
|
+
OR instr(lower(properties), lower($query)) > 0
|
|
217
|
+
`).all(parameters));
|
|
218
|
+
}
|
|
219
|
+
if (types.includes("block")) {
|
|
220
|
+
rows.push(...db.query(`
|
|
221
|
+
SELECT 'block' AS type, id, content AS label
|
|
222
|
+
FROM blocks
|
|
223
|
+
WHERE instr(lower(content), lower($query)) > 0
|
|
224
|
+
OR instr(lower(properties), lower($query)) > 0
|
|
225
|
+
`).all(parameters));
|
|
226
|
+
}
|
|
227
|
+
return rows
|
|
228
|
+
.map((row) => ({
|
|
229
|
+
...row,
|
|
230
|
+
label: row.type === "block" ? blockLabel(row.label) : row.label,
|
|
231
|
+
}))
|
|
232
|
+
.sort((left, right) => left.type.localeCompare(right.type)
|
|
233
|
+
|| left.label.localeCompare(right.label)
|
|
234
|
+
|| left.id.localeCompare(right.id));
|
|
235
|
+
}
|
|
236
|
+
/** Converts block content into a compact single-line search label. */
|
|
237
|
+
function blockLabel(content) {
|
|
238
|
+
const normalized = content.replace(/\s+/g, " ").trim();
|
|
239
|
+
return normalized.length <= 80 ? normalized : `${normalized.slice(0, 77)}...`;
|
|
240
|
+
}
|
|
241
|
+
/** Builds complete direct-child replacement lists for every entity in a tree. */
|
|
242
|
+
function buildChildMap(root) {
|
|
243
|
+
const map = new Map();
|
|
244
|
+
const visit = (entity) => {
|
|
245
|
+
map.set(entity.id, entity.children.map((child) => ({
|
|
246
|
+
type: child.type,
|
|
247
|
+
id: child.id,
|
|
248
|
+
})));
|
|
249
|
+
entity.children.forEach(visit);
|
|
250
|
+
};
|
|
251
|
+
visit(root);
|
|
252
|
+
return map;
|
|
253
|
+
}
|
|
254
|
+
/** Resolves public root placement. */
|
|
255
|
+
function resolveRootParentID(db, type, parentID) {
|
|
256
|
+
if (parentID !== undefined) {
|
|
257
|
+
return parentID;
|
|
258
|
+
}
|
|
259
|
+
if (type === "object") {
|
|
260
|
+
return getDatabaseMetadata(db).id;
|
|
261
|
+
}
|
|
262
|
+
throw new Error("Block parent is required");
|
|
263
|
+
}
|
|
264
|
+
/** Collects every entity ID explicitly present in a submitted tree. */
|
|
265
|
+
function collectSubmittedEntityIDs(root) {
|
|
266
|
+
const ids = new Set();
|
|
267
|
+
const visit = (entity) => {
|
|
268
|
+
ids.add(entity.id);
|
|
269
|
+
entity.children.forEach(visit);
|
|
270
|
+
};
|
|
271
|
+
visit(root);
|
|
272
|
+
return ids;
|
|
273
|
+
}
|
|
274
|
+
/** Finds existing direct children that a replacement write omitted. */
|
|
275
|
+
function collectOmittedChildIDs(db, desiredChildrenByParent, submittedIDs) {
|
|
276
|
+
const omitted = new Set();
|
|
277
|
+
for (const [parentID, desiredChildren] of desiredChildrenByParent) {
|
|
278
|
+
const desiredChildIDs = new Set(desiredChildren.map((child) => child.id));
|
|
279
|
+
for (const child of getDirectEntityChildren(db, parentID)) {
|
|
280
|
+
if (!desiredChildIDs.has(child.id) && !submittedIDs.has(child.id)) {
|
|
281
|
+
omitted.add(child.id);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return [...omitted];
|
|
286
|
+
}
|
|
287
|
+
/** Deletes omitted children after moved descendants have been reparented. */
|
|
288
|
+
function deleteOmittedEntitySubtrees(db, rootIDs) {
|
|
289
|
+
const deleted = new Set();
|
|
290
|
+
for (const rootID of rootIDs) {
|
|
291
|
+
if (deleted.has(rootID) || getStoredNodeType(db, rootID) === undefined) {
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
const ids = collectEntitySubtreeIDs(db, rootID);
|
|
295
|
+
deleteStoredNodes(db, ids);
|
|
296
|
+
ids.forEach((id) => deleted.add(id));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
/** Collects one entity and all of its recursive containment descendants. */
|
|
300
|
+
function collectEntitySubtreeIDs(db, rootID) {
|
|
301
|
+
const ids = [];
|
|
302
|
+
const visited = new Set();
|
|
303
|
+
const visit = (id) => {
|
|
304
|
+
if (visited.has(id)) {
|
|
305
|
+
throw new Error(`Entity cycle detected at ${id}`);
|
|
306
|
+
}
|
|
307
|
+
visited.add(id);
|
|
308
|
+
ids.push(id);
|
|
309
|
+
for (const child of getDirectEntityChildren(db, id)) {
|
|
310
|
+
visit(child.id);
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
visit(rootID);
|
|
314
|
+
return ids;
|
|
315
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { BlockMetadata } from "../types/block";
|
|
2
|
+
import type { DatabaseID } from "../types/database";
|
|
3
|
+
import type { EntityID, EntityType } from "../types/graph";
|
|
4
|
+
import type { ObjMetadata } from "../types/object";
|
|
5
|
+
export type StoredEntityType = "database" | EntityType;
|
|
6
|
+
export type StoredEntityID = DatabaseID | EntityID;
|
|
7
|
+
export interface StoredEntityReference {
|
|
8
|
+
readonly type: StoredEntityType;
|
|
9
|
+
readonly id: StoredEntityID;
|
|
10
|
+
}
|
|
11
|
+
export type StoredObject = ObjMetadata;
|
|
12
|
+
export interface StoredBlock extends BlockMetadata {
|
|
13
|
+
content: string;
|
|
14
|
+
}
|
|
15
|
+
export type StoredEntity = StoredObject | StoredBlock;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// This file defines the internal stored rows for concrete entity tables.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Entity } from "./graph";
|
|
2
|
+
import type { JSONRecord } from "./json";
|
|
3
|
+
export type BlockID = string;
|
|
4
|
+
export interface BlockMetadata {
|
|
5
|
+
readonly id: BlockID;
|
|
6
|
+
readonly type: "block";
|
|
7
|
+
properties?: JSONRecord;
|
|
8
|
+
}
|
|
9
|
+
export interface Block extends BlockMetadata {
|
|
10
|
+
content: string;
|
|
11
|
+
children: Entity[];
|
|
12
|
+
}
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Block, BlockID } from "./block";
|
|
2
|
+
import type { Obj, ObjID } from "./object";
|
|
3
|
+
import type { WorkspaceID } from "./workspace";
|
|
4
|
+
export type EntityType = "object" | "block";
|
|
5
|
+
export type EntityID = ObjID | BlockID;
|
|
6
|
+
export type Entity = Obj | Block;
|
|
7
|
+
export interface EntityReference {
|
|
8
|
+
readonly type: EntityType;
|
|
9
|
+
readonly id: EntityID;
|
|
10
|
+
}
|
|
11
|
+
export type EntityParentID = WorkspaceID | EntityID | null;
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Entity } from "./graph";
|
|
2
|
+
import type { JSONRecord } from "./json";
|
|
3
|
+
export type ObjID = string;
|
|
4
|
+
export interface ObjMetadata {
|
|
5
|
+
readonly id: ObjID;
|
|
6
|
+
readonly type: "object";
|
|
7
|
+
name: string;
|
|
8
|
+
properties?: JSONRecord;
|
|
9
|
+
}
|
|
10
|
+
export interface Obj extends ObjMetadata {
|
|
11
|
+
children: Entity[];
|
|
12
|
+
}
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const ObjectPrefix: string;
|
|
2
|
+
export declare const BlockPrefix: string;
|
|
3
|
+
export declare const DatabasePrefix: string;
|
|
4
|
+
export declare function createObjID(): string;
|
|
5
|
+
export declare function createBlockID(): string;
|
|
6
|
+
export declare function createDatabaseID(): string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
export const ObjectPrefix = "o";
|
|
3
|
+
export const BlockPrefix = "b";
|
|
4
|
+
export const DatabasePrefix = "d";
|
|
5
|
+
// HELPER: Creates a unique ID with the given prefix
|
|
6
|
+
function createID(prefix) {
|
|
7
|
+
const random = randomUUID().replaceAll("-", "");
|
|
8
|
+
return `${prefix}_${random}`;
|
|
9
|
+
}
|
|
10
|
+
export function createObjID() {
|
|
11
|
+
return createID(ObjectPrefix);
|
|
12
|
+
}
|
|
13
|
+
export function createBlockID() {
|
|
14
|
+
return createID(BlockPrefix);
|
|
15
|
+
}
|
|
16
|
+
export function createDatabaseID() {
|
|
17
|
+
return createID(DatabasePrefix);
|
|
18
|
+
}
|