@rljson/db 0.0.11 → 0.0.13
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.architecture.md +859 -1
- package/README.contributors.md +534 -20
- package/README.public.md +943 -4
- package/README.trouble.md +49 -0
- package/dist/README.architecture.md +859 -1
- package/dist/README.contributors.md +534 -20
- package/dist/README.public.md +943 -4
- package/dist/README.trouble.md +49 -0
- package/dist/controller/controller.d.ts +2 -2
- package/dist/controller/tree-controller.d.ts +18 -0
- package/dist/db.d.ts +0 -6
- package/dist/db.js +317 -110
- package/dist/db.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/package.json +16 -16
package/README.public.md
CHANGED
|
@@ -1,7 +1,946 @@
|
|
|
1
|
-
# @rljson/
|
|
1
|
+
# @rljson/db
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A high-level TypeScript database abstraction for content-addressed, immutable RLJSON data. Provides intuitive querying with native support for hierarchical structures, version history, and pluggable storage backends.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Features
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- **Content-Addressed:** All data identified by SHA-based hashes
|
|
8
|
+
- **Immutable:** Changes create new versions automatically
|
|
9
|
+
- **Hierarchical:** First-class tree structures with smart expansion
|
|
10
|
+
- **Type-Safe:** Full TypeScript support
|
|
11
|
+
- **Time-Travel:** Query historical versions
|
|
12
|
+
- **Storage-Agnostic:** Memory, file, or network backends
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @rljson/db @rljson/io @rljson/rljson
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**Requirements:** Node.js >= 22.14.0
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { Db } from '@rljson/db';
|
|
26
|
+
import { IoMem } from '@rljson/io';
|
|
27
|
+
import { Route, createComponentsTableCfg } from '@rljson/rljson';
|
|
28
|
+
|
|
29
|
+
// Initialize database with in-memory storage
|
|
30
|
+
const io = new IoMem();
|
|
31
|
+
await io.init();
|
|
32
|
+
const db = new Db(io);
|
|
33
|
+
|
|
34
|
+
// Create a table
|
|
35
|
+
const tableCfg = createComponentsTableCfg('users', [
|
|
36
|
+
{ key: 'name', type: 'string' },
|
|
37
|
+
{ key: 'email', type: 'string' }
|
|
38
|
+
]);
|
|
39
|
+
await db.core.createTableWithInsertHistory(tableCfg);
|
|
40
|
+
|
|
41
|
+
// Import data
|
|
42
|
+
await db.core.import({
|
|
43
|
+
users: {
|
|
44
|
+
_type: 'components',
|
|
45
|
+
_data: [
|
|
46
|
+
{ name: 'Alice', email: 'alice@example.com', _hash: '...' }
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Query data
|
|
52
|
+
const route = Route.fromFlat('users');
|
|
53
|
+
const result = await db.get(route, {});
|
|
54
|
+
console.log(result.rljson.users._data);
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Core Concepts
|
|
58
|
+
|
|
59
|
+
### Content-Addressed Data
|
|
60
|
+
|
|
61
|
+
All data in @rljson/db is identified by its **content hash** (SHA-256 based). This means:
|
|
62
|
+
|
|
63
|
+
- Identical data always has the same hash
|
|
64
|
+
- Changes to data produce a new hash
|
|
65
|
+
- Perfect caching: same hash = same data
|
|
66
|
+
- Deduplication: store each unique value once
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import { hsh } from '@rljson/hash';
|
|
70
|
+
|
|
71
|
+
const data = { name: 'Alice', age: 30 };
|
|
72
|
+
const hash = hsh(data)._hash;
|
|
73
|
+
// 'abc123...' - deterministic hash based on content
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Immutability
|
|
77
|
+
|
|
78
|
+
Data is **never modified in place**. All mutations create new versions:
|
|
79
|
+
|
|
80
|
+
- INSERT creates new data with new hash
|
|
81
|
+
- Old versions remain accessible
|
|
82
|
+
- Time-travel queries via insert history
|
|
83
|
+
- No update conflicts
|
|
84
|
+
|
|
85
|
+
### Routes
|
|
86
|
+
|
|
87
|
+
Routes define paths through related data. They use a flat string syntax:
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
/tableName # Root table
|
|
91
|
+
/tableName@hash # Specific row by hash
|
|
92
|
+
/tableName@timeId # Historic version
|
|
93
|
+
/tableName/childTable # Relationship traversal
|
|
94
|
+
/tableName@hash/childTable@hash2 # Nested navigation
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Example:
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
// Simple route
|
|
101
|
+
Route.fromFlat('users')
|
|
102
|
+
|
|
103
|
+
// With hash reference
|
|
104
|
+
Route.fromFlat('users@abc123')
|
|
105
|
+
|
|
106
|
+
// Nested relationship
|
|
107
|
+
Route.fromFlat('projects/tasks')
|
|
108
|
+
|
|
109
|
+
// Complex navigation
|
|
110
|
+
Route.fromFlat('projects@hash1/tasks@hash2')
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Data Types
|
|
114
|
+
|
|
115
|
+
### 1. Components
|
|
116
|
+
|
|
117
|
+
Flat tables with arbitrary columns. Similar to traditional database tables.
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
type Component = {
|
|
121
|
+
[column: string]: JsonValue;
|
|
122
|
+
_hash: string;
|
|
123
|
+
};
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**Use Cases:**
|
|
127
|
+
|
|
128
|
+
- User records
|
|
129
|
+
- Configuration data
|
|
130
|
+
- Event logs
|
|
131
|
+
- Any flat, record-based data
|
|
132
|
+
|
|
133
|
+
**Example:**
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
const component = {
|
|
137
|
+
name: 'Alice',
|
|
138
|
+
email: 'alice@example.com',
|
|
139
|
+
role: 'admin',
|
|
140
|
+
_hash: '...'
|
|
141
|
+
};
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### 2. Trees
|
|
145
|
+
|
|
146
|
+
Hierarchical structures where each node has:
|
|
147
|
+
|
|
148
|
+
- `id`: Node identifier
|
|
149
|
+
- `children`: Array of child hash references
|
|
150
|
+
- `meta`: Node metadata
|
|
151
|
+
- `isParent`: Boolean flag for parent nodes
|
|
152
|
+
- `_hash`: Content hash
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
type Tree = {
|
|
156
|
+
id: string;
|
|
157
|
+
children: string[]; // Hash references
|
|
158
|
+
meta: Json;
|
|
159
|
+
isParent: boolean;
|
|
160
|
+
_hash: string;
|
|
161
|
+
};
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**Use Cases:**
|
|
165
|
+
|
|
166
|
+
- File systems
|
|
167
|
+
- Organizational hierarchies
|
|
168
|
+
- Menu structures
|
|
169
|
+
- DOM-like structures
|
|
170
|
+
|
|
171
|
+
**Important:** Tree queries behave differently based on context:
|
|
172
|
+
|
|
173
|
+
- **WHERE clause** (`{ _hash: ... }`): Returns single node only (prevents infinite recursion)
|
|
174
|
+
- **Route navigation**: Expands children recursively
|
|
175
|
+
|
|
176
|
+
**Example:**
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
const tree = {
|
|
180
|
+
id: 'root',
|
|
181
|
+
children: ['childHash1', 'childHash2'],
|
|
182
|
+
meta: { name: 'Root Node' },
|
|
183
|
+
isParent: true,
|
|
184
|
+
_hash: '...'
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// Single node query (efficient)
|
|
188
|
+
await db.get(route, { _hash: tree._hash });
|
|
189
|
+
|
|
190
|
+
// Expanded navigation (recursive)
|
|
191
|
+
await db.get(Route.fromFlat(`tree@${tree._hash}`));
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### 3. Cakes
|
|
195
|
+
|
|
196
|
+
Multi-dimensional data cubes with slicing capabilities.
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
type Cake = {
|
|
200
|
+
sliceIdsTable: string;
|
|
201
|
+
sliceIdsRow: string;
|
|
202
|
+
layers: Record<string, string>; // layerTable -> layerRef
|
|
203
|
+
_hash: string;
|
|
204
|
+
};
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**Use Cases:**
|
|
208
|
+
|
|
209
|
+
- Multi-environment configurations
|
|
210
|
+
- A/B testing variants
|
|
211
|
+
- Feature flags
|
|
212
|
+
- Dimensional data analysis
|
|
213
|
+
|
|
214
|
+
**Example:**
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
const cake = {
|
|
218
|
+
sliceIdsTable: 'environments',
|
|
219
|
+
sliceIdsRow: 'production',
|
|
220
|
+
layers: {
|
|
221
|
+
configLayer: 'layerHash1',
|
|
222
|
+
settingsLayer: 'layerHash2'
|
|
223
|
+
},
|
|
224
|
+
_hash: '...'
|
|
225
|
+
};
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### 4. Layers
|
|
229
|
+
|
|
230
|
+
Composable data layers with inheritance and dimension filtering.
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
type Layer = {
|
|
234
|
+
base?: string; // Base layer hash
|
|
235
|
+
sliceIdsTable: string;
|
|
236
|
+
sliceIdsTableRow: string;
|
|
237
|
+
componentsTable: string;
|
|
238
|
+
_hash: string;
|
|
239
|
+
};
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**Use Cases:**
|
|
243
|
+
|
|
244
|
+
- Configuration inheritance
|
|
245
|
+
- Environment-specific settings
|
|
246
|
+
- Progressive overrides
|
|
247
|
+
- Template-based data
|
|
248
|
+
|
|
249
|
+
**Example:**
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
const layer = {
|
|
253
|
+
base: 'baseLayerHash',
|
|
254
|
+
sliceIdsTable: 'environments',
|
|
255
|
+
sliceIdsTableRow: 'staging',
|
|
256
|
+
componentsTable: 'settings',
|
|
257
|
+
_hash: '...'
|
|
258
|
+
};
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### 5. SliceIds
|
|
262
|
+
|
|
263
|
+
Dimension identifiers for cakes and layers. Enable multi-dimensional data organization.
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
type SliceId = string; // e.g., 'production', 'staging', 'featureA'
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Querying Data
|
|
270
|
+
|
|
271
|
+
### Basic Queries
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
// Get all records from a table
|
|
275
|
+
const route = Route.fromFlat('users');
|
|
276
|
+
const result = await db.get(route, {});
|
|
277
|
+
console.log(result.rljson.users._data); // All users
|
|
278
|
+
|
|
279
|
+
// Query by hash
|
|
280
|
+
const result = await db.get(route, { _hash: 'specific-hash' });
|
|
281
|
+
console.log(result.rljson.users._data[0]); // Single user
|
|
282
|
+
|
|
283
|
+
// Query by field values
|
|
284
|
+
const result = await db.get(route, { role: 'admin' });
|
|
285
|
+
console.log(result.rljson.users._data); // All admin users
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Nested Queries
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
// Query with relationships
|
|
292
|
+
const route = Route.fromFlat('projects/tasks');
|
|
293
|
+
const result = await db.get(route, {
|
|
294
|
+
projects: { status: 'active' },
|
|
295
|
+
tasks: { priority: 'high' }
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// Result contains both projects and their related tasks
|
|
299
|
+
console.log(result.rljson.projects);
|
|
300
|
+
console.log(result.rljson.tasks);
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Tree Queries
|
|
304
|
+
|
|
305
|
+
**Critical:** Tree behavior depends on query context.
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
// ✅ WHERE clause - Returns ONLY requested node
|
|
309
|
+
// Use this for querying specific nodes without expansion
|
|
310
|
+
const nodeRoute = Route.fromFlat('fileSystem');
|
|
311
|
+
const nodeResult = await db.get(nodeRoute, { _hash: nodeHash });
|
|
312
|
+
// Returns: { fileSystem: { _data: [singleNode] } }
|
|
313
|
+
// No children expanded - prevents heap crashes on large trees
|
|
314
|
+
|
|
315
|
+
// ✅ Route navigation - Expands children recursively
|
|
316
|
+
// Use this for traversing tree structures
|
|
317
|
+
const treeRoute = Route.fromFlat(`fileSystem@${rootHash}`);
|
|
318
|
+
const treeResult = await db.get(treeRoute);
|
|
319
|
+
// Returns: Complete tree with all children expanded
|
|
320
|
+
// Access via treeResult.tree for hierarchical view
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Result Container
|
|
324
|
+
|
|
325
|
+
All queries return a `Container` object with three views of the data:
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
type Container = {
|
|
329
|
+
rljson: Rljson; // Table data indexed by table name
|
|
330
|
+
tree: Json; // Hierarchical representation
|
|
331
|
+
cell: Cell[]; // Path-value pairs for modifications
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
type Cell = {
|
|
335
|
+
route: Route;
|
|
336
|
+
value: JsonValue;
|
|
337
|
+
row: JsonValue;
|
|
338
|
+
path: Array<Array<string | number>>;
|
|
339
|
+
};
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
**Example:**
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
const { rljson, tree, cell } = await db.get(route, where);
|
|
346
|
+
|
|
347
|
+
// RLJSON view - Table-oriented
|
|
348
|
+
console.log(rljson.users._data);
|
|
349
|
+
|
|
350
|
+
// Tree view - Hierarchical
|
|
351
|
+
console.log(tree.users[0].name);
|
|
352
|
+
|
|
353
|
+
// Cell view - Path-based for mutations
|
|
354
|
+
console.log(cell[0].path); // ['users', 0, 'name']
|
|
355
|
+
console.log(cell[0].value); // 'Alice'
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
## Inserting Data
|
|
359
|
+
|
|
360
|
+
### Using isolate() and inject()
|
|
361
|
+
|
|
362
|
+
The recommended pattern for data modifications:
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
import { isolate, inject } from '@rljson/db';
|
|
366
|
+
|
|
367
|
+
// 1. Query existing data
|
|
368
|
+
const route = Route.fromFlat('users/profile/settings');
|
|
369
|
+
const { tree, cell } = await db.get(route, {});
|
|
370
|
+
|
|
371
|
+
// 2. Isolate specific path
|
|
372
|
+
const path = cell[0].path;
|
|
373
|
+
const isolated = isolate(tree, path);
|
|
374
|
+
// Returns only the data at the specified path
|
|
375
|
+
|
|
376
|
+
// 3. Modify isolated data
|
|
377
|
+
isolated.profile.settings.theme = 'dark';
|
|
378
|
+
isolated.profile.settings.notifications = true;
|
|
379
|
+
|
|
380
|
+
// 4. Inject changes back
|
|
381
|
+
inject(isolated, path, {
|
|
382
|
+
theme: 'dark',
|
|
383
|
+
notifications: true
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// 5. Insert into database
|
|
387
|
+
const results = await db.insert(route, isolated);
|
|
388
|
+
console.log(results); // InsertHistoryRow[]
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### Direct Import
|
|
392
|
+
|
|
393
|
+
For bulk data loading:
|
|
394
|
+
|
|
395
|
+
```typescript
|
|
396
|
+
await db.core.import({
|
|
397
|
+
users: {
|
|
398
|
+
_type: 'components',
|
|
399
|
+
_data: [
|
|
400
|
+
{ name: 'Alice', email: 'alice@example.com', _hash: 'hash1' },
|
|
401
|
+
{ name: 'Bob', email: 'bob@example.com', _hash: 'hash2' }
|
|
402
|
+
]
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### Tree INSERT
|
|
408
|
+
|
|
409
|
+
**Important:** When inserting tree data that has already been isolated, use the fixed behavior:
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
// The treeFromObject function now has skipRootCreation parameter
|
|
413
|
+
// This is handled internally by db.insert() - no action needed
|
|
414
|
+
|
|
415
|
+
const treeData = isolate(existingTree, path);
|
|
416
|
+
// Modify treeData...
|
|
417
|
+
|
|
418
|
+
// INSERT automatically uses skipRootCreation=true internally
|
|
419
|
+
await db.insert(route, treeData);
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
**Why this matters:** The `treeFromObject` function creates an explicit root node. When inserting already-isolated subtrees, this created a double-root structure causing navigation failures. The fix in v0.0.12+ handles this automatically.
|
|
423
|
+
|
|
424
|
+
## Version History
|
|
425
|
+
|
|
426
|
+
Every insert creates a history entry with timestamps and references:
|
|
427
|
+
|
|
428
|
+
```typescript
|
|
429
|
+
// Get insert history for a table
|
|
430
|
+
const history = await db.getInsertHistory('users', {
|
|
431
|
+
sorted: true,
|
|
432
|
+
ascending: false // Most recent first
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
console.log(history.usersInsertHistory._data);
|
|
436
|
+
// [
|
|
437
|
+
// {
|
|
438
|
+
// timeId: 'ABC123:20260126T150000Z',
|
|
439
|
+
// usersRef: 'hash-of-inserted-data',
|
|
440
|
+
// route: '/users',
|
|
441
|
+
// previous: ['previous-timeId'],
|
|
442
|
+
// ...
|
|
443
|
+
// }
|
|
444
|
+
// ]
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
### Time-Travel Queries
|
|
448
|
+
|
|
449
|
+
Query historical versions using timeIds:
|
|
450
|
+
|
|
451
|
+
```typescript
|
|
452
|
+
// Query specific version by timeId
|
|
453
|
+
const route = Route.fromFlat(`users@ABC123:20260126T150000Z`);
|
|
454
|
+
const historicData = await db.get(route);
|
|
455
|
+
console.log(historicData.rljson.users._data); // Data as it was at that time
|
|
456
|
+
|
|
457
|
+
// Get all timeIds for a specific hash
|
|
458
|
+
const timeIds = await db.getTimeIdsForRef('users', 'hash1');
|
|
459
|
+
console.log(timeIds); // ['timeId1', 'timeId2', ...]
|
|
460
|
+
|
|
461
|
+
// Get data hash for a specific timeId
|
|
462
|
+
const ref = await db.getRefOfTimeId('users', 'ABC123:20260126T150000Z');
|
|
463
|
+
console.log(ref); // 'hash1'
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
## Advanced Features
|
|
467
|
+
|
|
468
|
+
### Join System
|
|
469
|
+
|
|
470
|
+
The Join system provides SQL-like operations on query results:
|
|
471
|
+
|
|
472
|
+
```typescript
|
|
473
|
+
import { Join, ColumnSelection, RowFilter } from '@rljson/db';
|
|
474
|
+
|
|
475
|
+
// Create a join from query results
|
|
476
|
+
const { rljson, tree, cell } = await db.get(route, where);
|
|
477
|
+
const join = new Join({ rljson, tree, cell });
|
|
478
|
+
|
|
479
|
+
// Apply column selection
|
|
480
|
+
const selection = new ColumnSelection(
|
|
481
|
+
route,
|
|
482
|
+
[
|
|
483
|
+
{ key: 'name', type: 'include' },
|
|
484
|
+
{ key: 'email', type: 'include' }
|
|
485
|
+
]
|
|
486
|
+
);
|
|
487
|
+
join.select(selection);
|
|
488
|
+
|
|
489
|
+
// Apply row filtering
|
|
490
|
+
const filter = new RowFilter(
|
|
491
|
+
route,
|
|
492
|
+
{
|
|
493
|
+
age: { gt: 25 } // Greater than 25
|
|
494
|
+
}
|
|
495
|
+
);
|
|
496
|
+
join.filter(filter);
|
|
497
|
+
|
|
498
|
+
// Apply sorting
|
|
499
|
+
const sort = new RowSort(
|
|
500
|
+
route,
|
|
501
|
+
[{ key: 'name', direction: 'asc' }]
|
|
502
|
+
);
|
|
503
|
+
join.sort(sort);
|
|
504
|
+
|
|
505
|
+
// Get transformed results
|
|
506
|
+
const resultRows = await join.rows();
|
|
507
|
+
console.log(resultRows);
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### Multi-Edit Operations
|
|
511
|
+
|
|
512
|
+
Transactional editing with rollback support:
|
|
513
|
+
|
|
514
|
+
```typescript
|
|
515
|
+
import { MultiEditManager } from '@rljson/db';
|
|
516
|
+
|
|
517
|
+
const manager = new MultiEditManager('configCake', db);
|
|
518
|
+
await manager.init();
|
|
519
|
+
|
|
520
|
+
// Perform multiple edits as a transaction
|
|
521
|
+
await manager.multiEdit(async (head) => {
|
|
522
|
+
// Edit 1: Update column selection
|
|
523
|
+
await head.edit({
|
|
524
|
+
type: 'columnSelection',
|
|
525
|
+
params: { columns: ['name', 'email'] }
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
// Edit 2: Apply filter
|
|
529
|
+
await head.edit({
|
|
530
|
+
type: 'rowFilter',
|
|
531
|
+
params: { age: { gt: 18 } }
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
return head;
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
// Publish changes (commits transaction)
|
|
538
|
+
const published = await manager.publishHead();
|
|
539
|
+
console.log(published);
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
### Real-Time Notifications
|
|
543
|
+
|
|
544
|
+
Register callbacks for data changes:
|
|
545
|
+
|
|
546
|
+
```typescript
|
|
547
|
+
// Register callback
|
|
548
|
+
db.notify.registerCallback('myCallback', async (insertHistoryRow) => {
|
|
549
|
+
console.log('Data changed:', insertHistoryRow);
|
|
550
|
+
// Perform side effects (cache invalidation, UI updates, etc.)
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
// Perform operations - callback fires automatically
|
|
554
|
+
await db.insert(route, data);
|
|
555
|
+
|
|
556
|
+
// Unregister when done
|
|
557
|
+
db.notify.unregisterCallback('myCallback');
|
|
558
|
+
|
|
559
|
+
// Or unregister all callbacks for a route
|
|
560
|
+
db.notify.unregisterAll(route);
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
### Caching
|
|
564
|
+
|
|
565
|
+
The database automatically caches query results based on query signatures:
|
|
566
|
+
|
|
567
|
+
```typescript
|
|
568
|
+
// Check cache size
|
|
569
|
+
console.log(`Cache size: ${db.cache.size}`);
|
|
570
|
+
|
|
571
|
+
// Clear cache manually
|
|
572
|
+
db.clearCache();
|
|
573
|
+
|
|
574
|
+
// Caching is automatic when:
|
|
575
|
+
// - Route contains hash references (@hash)
|
|
576
|
+
// - Filters are applied
|
|
577
|
+
// - SliceIds are specified
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
## Best Practices
|
|
581
|
+
|
|
582
|
+
### 1. Content-Addressed Design
|
|
583
|
+
|
|
584
|
+
All data is immutable - never modify in place:
|
|
585
|
+
|
|
586
|
+
```typescript
|
|
587
|
+
// ❌ WRONG - Modifying data directly
|
|
588
|
+
const { rljson } = await db.get(route, {});
|
|
589
|
+
rljson.users._data[0].name = 'Changed'; // This won't persist!
|
|
590
|
+
|
|
591
|
+
// ✅ CORRECT - Use isolate/inject pattern
|
|
592
|
+
const { tree, cell } = await db.get(route, {});
|
|
593
|
+
const isolated = isolate(tree, cell[0].path);
|
|
594
|
+
isolated.name = 'Changed';
|
|
595
|
+
await db.insert(route, isolated); // Creates new version
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
### 2. Tree Query Optimization
|
|
599
|
+
|
|
600
|
+
When querying trees by hash, use WHERE clauses for single nodes:
|
|
601
|
+
|
|
602
|
+
```typescript
|
|
603
|
+
// ✅ Efficient - Returns single node only
|
|
604
|
+
await db.get(Route.fromFlat('tree'), { _hash: nodeHash });
|
|
605
|
+
|
|
606
|
+
// ❌ Avoid for large trees - Expands ALL children recursively
|
|
607
|
+
// Only use for navigation when you need the full tree
|
|
608
|
+
await db.get(Route.fromFlat(`tree@${nodeHash}`));
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
This is **critical** for large trees to prevent:
|
|
612
|
+
|
|
613
|
+
- Heap exhaustion
|
|
614
|
+
- Infinite recursion
|
|
615
|
+
- Performance degradation
|
|
616
|
+
|
|
617
|
+
### 3. Batch Operations
|
|
618
|
+
|
|
619
|
+
Import data in batches for better performance:
|
|
620
|
+
|
|
621
|
+
```typescript
|
|
622
|
+
const batchSize = 100;
|
|
623
|
+
for (let i = 0; i < data.length; i += batchSize) {
|
|
624
|
+
const batch = data.slice(i, i + batchSize);
|
|
625
|
+
await db.core.import({
|
|
626
|
+
users: { _type: 'components', _data: batch }
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
### 4. Always Use Insert History
|
|
632
|
+
|
|
633
|
+
Create tables with insert history to enable time-travel:
|
|
634
|
+
|
|
635
|
+
```typescript
|
|
636
|
+
// ✅ CORRECT - Enables version tracking
|
|
637
|
+
await db.core.createTableWithInsertHistory(tableCfg);
|
|
638
|
+
|
|
639
|
+
// ❌ AVOID - No version history
|
|
640
|
+
await db.core.createTable(tableCfg);
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
### 5. Type Safety
|
|
644
|
+
|
|
645
|
+
Use TypeScript types from `@rljson/rljson` for type-safe operations:
|
|
646
|
+
|
|
647
|
+
```typescript
|
|
648
|
+
import type { Component, Tree, Cake, Layer } from '@rljson/rljson';
|
|
649
|
+
|
|
650
|
+
// Type-safe component
|
|
651
|
+
const user: Component = {
|
|
652
|
+
name: 'Alice',
|
|
653
|
+
email: 'alice@example.com',
|
|
654
|
+
_hash: '...'
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
// Type-safe tree
|
|
658
|
+
const tree: Tree = {
|
|
659
|
+
id: 'root',
|
|
660
|
+
children: [],
|
|
661
|
+
meta: { name: 'Root' },
|
|
662
|
+
isParent: false,
|
|
663
|
+
_hash: '...'
|
|
664
|
+
};
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
### 6. Error Handling
|
|
668
|
+
|
|
669
|
+
Always wrap database operations in try-catch:
|
|
670
|
+
|
|
671
|
+
```typescript
|
|
672
|
+
try {
|
|
673
|
+
const result = await db.get(route, where);
|
|
674
|
+
// Process result...
|
|
675
|
+
} catch (error) {
|
|
676
|
+
if (error.message.includes('Maximum recursion depth')) {
|
|
677
|
+
console.error('Infinite recursion detected in route');
|
|
678
|
+
} else if (error.message.includes('not valid')) {
|
|
679
|
+
console.error('Invalid route or data structure');
|
|
680
|
+
} else {
|
|
681
|
+
console.error('Unexpected error:', error);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
## Troubleshooting
|
|
687
|
+
|
|
688
|
+
### Tree INSERT Failures
|
|
689
|
+
|
|
690
|
+
**Symptoms:**
|
|
691
|
+
|
|
692
|
+
- Tree INSERT completes without errors
|
|
693
|
+
- GET queries return empty results or only root node
|
|
694
|
+
- Cell length is 1 instead of expected count
|
|
695
|
+
|
|
696
|
+
**Solution:** This was fixed in v0.0.12+. Upgrade to latest version:
|
|
697
|
+
|
|
698
|
+
```bash
|
|
699
|
+
pnpm update @rljson/db@latest
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
See [README.trouble.md](./README.trouble.md) for more troubleshooting guides.
|
|
703
|
+
|
|
704
|
+
### Infinite Recursion Errors
|
|
705
|
+
|
|
706
|
+
**Problem:** Tree queries causing stack overflow or heap exhaustion.
|
|
707
|
+
|
|
708
|
+
**Solution:** Use WHERE clause for single-node queries:
|
|
709
|
+
|
|
710
|
+
```typescript
|
|
711
|
+
// ✅ This
|
|
712
|
+
await db.get(route, { _hash: nodeHash });
|
|
713
|
+
|
|
714
|
+
// ❌ Not this (for large trees)
|
|
715
|
+
await db.get(Route.fromFlat(`tree@${nodeHash}`));
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
### Cache Not Clearing
|
|
719
|
+
|
|
720
|
+
**Problem:** Stale data persists after modifications.
|
|
721
|
+
|
|
722
|
+
**Solution:** Register notify callbacks to clear cache automatically:
|
|
723
|
+
|
|
724
|
+
```typescript
|
|
725
|
+
db.notify.register(route, async () => {
|
|
726
|
+
db.clearCache();
|
|
727
|
+
});
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
## API Reference
|
|
731
|
+
|
|
732
|
+
### Core Methods
|
|
733
|
+
|
|
734
|
+
#### `db.get(route, where, filter?, sliceIds?, options?)`
|
|
735
|
+
|
|
736
|
+
Query data from the database.
|
|
737
|
+
|
|
738
|
+
**Parameters:**
|
|
739
|
+
|
|
740
|
+
- `route: Route` - The route to query
|
|
741
|
+
- `where: string | Json` - Filter criteria
|
|
742
|
+
- `filter?: ControllerChildProperty[]` - Optional child filters
|
|
743
|
+
- `sliceIds?: SliceId[]` - Optional dimension filters
|
|
744
|
+
- `options?: GetOptions` - Query options (`skipRljson`, `skipTree`, `skipCell`)
|
|
745
|
+
|
|
746
|
+
**Returns:** `Promise<ContainerWithControllers>`
|
|
747
|
+
|
|
748
|
+
#### `db.insert(route, data, origin?, refs?)`
|
|
749
|
+
|
|
750
|
+
Insert new data into the database.
|
|
751
|
+
|
|
752
|
+
**Parameters:**
|
|
753
|
+
|
|
754
|
+
- `route: Route` - Destination route
|
|
755
|
+
- `data: Json` - Data to insert
|
|
756
|
+
- `origin?: Ref` - Optional origin reference
|
|
757
|
+
- `refs?: ControllerRefs` - Optional controller references
|
|
758
|
+
|
|
759
|
+
**Returns:** `Promise<InsertHistoryRow[]>`
|
|
760
|
+
|
|
761
|
+
#### `db.getInsertHistory(table, options?)`
|
|
762
|
+
|
|
763
|
+
Get version history for a table.
|
|
764
|
+
|
|
765
|
+
**Parameters:**
|
|
766
|
+
|
|
767
|
+
- `table: string` - Table name
|
|
768
|
+
- `options?: { sorted?: boolean, ascending?: boolean }` - Sort options
|
|
769
|
+
|
|
770
|
+
**Returns:** `Promise<InsertHistoryTable>`
|
|
771
|
+
|
|
772
|
+
#### `db.core.import(rljson)`
|
|
773
|
+
|
|
774
|
+
Import RLJSON data into the database.
|
|
775
|
+
|
|
776
|
+
**Parameters:**
|
|
777
|
+
|
|
778
|
+
- `rljson: Rljson` - Data to import
|
|
779
|
+
|
|
780
|
+
**Returns:** `Promise<void>`
|
|
781
|
+
|
|
782
|
+
#### `db.core.dump()`
|
|
783
|
+
|
|
784
|
+
Export all database data.
|
|
785
|
+
|
|
786
|
+
**Returns:** `Promise<Rljson>`
|
|
787
|
+
|
|
788
|
+
#### `db.core.createTableWithInsertHistory(cfg)`
|
|
789
|
+
|
|
790
|
+
Create a table with automatic version tracking.
|
|
791
|
+
|
|
792
|
+
**Parameters:**
|
|
793
|
+
|
|
794
|
+
- `cfg: TableCfg` - Table configuration
|
|
795
|
+
|
|
796
|
+
**Returns:** `Promise<void>`
|
|
797
|
+
|
|
798
|
+
### Utility Functions
|
|
799
|
+
|
|
800
|
+
#### `isolate(tree, path, preservedKeys?)`
|
|
801
|
+
|
|
802
|
+
Extract data at a specific path.
|
|
803
|
+
|
|
804
|
+
**Parameters:**
|
|
805
|
+
|
|
806
|
+
- `tree: any` - Source tree
|
|
807
|
+
- `path: (string | number)[]` - Path to isolate
|
|
808
|
+
- `preservedKeys?: string[]` - Keys to preserve (e.g., metadata)
|
|
809
|
+
|
|
810
|
+
**Returns:** Isolated data subtree
|
|
811
|
+
|
|
812
|
+
#### `inject(tree, path, value)`
|
|
813
|
+
|
|
814
|
+
Insert value at a specific path.
|
|
815
|
+
|
|
816
|
+
**Parameters:**
|
|
817
|
+
|
|
818
|
+
- `tree: any` - Target tree
|
|
819
|
+
- `path: (string | number)[]` - Destination path
|
|
820
|
+
- `value: any` - Value to inject
|
|
821
|
+
|
|
822
|
+
**Returns:** Modified tree
|
|
823
|
+
|
|
824
|
+
#### `treeFromObject(obj, skipRootCreation?)`
|
|
825
|
+
|
|
826
|
+
Convert object to tree structure.
|
|
827
|
+
|
|
828
|
+
**Parameters:**
|
|
829
|
+
|
|
830
|
+
- `obj: Json` - Object to convert
|
|
831
|
+
- `skipRootCreation?: boolean` - Skip automatic root creation (default: false)
|
|
832
|
+
|
|
833
|
+
**Returns:** `Tree[]`
|
|
834
|
+
|
|
835
|
+
#### `makeUnique(array)`
|
|
836
|
+
|
|
837
|
+
Remove duplicates by hash.
|
|
838
|
+
|
|
839
|
+
**Parameters:**
|
|
840
|
+
|
|
841
|
+
- `array: any[]` - Array with potential duplicates
|
|
842
|
+
|
|
843
|
+
**Returns:** `any[]` - Unique elements
|
|
844
|
+
|
|
845
|
+
#### `mergeTrees(trees)`
|
|
846
|
+
|
|
847
|
+
Combine multiple tree structures.
|
|
848
|
+
|
|
849
|
+
**Parameters:**
|
|
850
|
+
|
|
851
|
+
- `trees: Tree[][]` - Arrays of trees to merge
|
|
852
|
+
|
|
853
|
+
**Returns:** `Tree[]`
|
|
854
|
+
|
|
855
|
+
## Storage Backends
|
|
856
|
+
|
|
857
|
+
@rljson/db works with any `@rljson/io` implementation:
|
|
858
|
+
|
|
859
|
+
### IoMem (In-Memory)
|
|
860
|
+
|
|
861
|
+
Perfect for development, testing, and temporary data:
|
|
862
|
+
|
|
863
|
+
```typescript
|
|
864
|
+
import { IoMem } from '@rljson/io';
|
|
865
|
+
|
|
866
|
+
const io = new IoMem();
|
|
867
|
+
await io.init();
|
|
868
|
+
const db = new Db(io);
|
|
869
|
+
|
|
870
|
+
// Data stored in memory - cleared on process exit
|
|
871
|
+
```
|
|
872
|
+
|
|
873
|
+
### IoFile (File System)
|
|
874
|
+
|
|
875
|
+
Persistent storage using the file system:
|
|
876
|
+
|
|
877
|
+
```typescript
|
|
878
|
+
import { IoFile } from '@rljson/io';
|
|
879
|
+
|
|
880
|
+
const io = new IoFile('/path/to/data');
|
|
881
|
+
await io.init();
|
|
882
|
+
const db = new Db(io);
|
|
883
|
+
|
|
884
|
+
// Data persisted to disk
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
### IoMulti (Multiple Backends)
|
|
888
|
+
|
|
889
|
+
Redundant storage across multiple backends with priority-based cascade:
|
|
890
|
+
|
|
891
|
+
```typescript
|
|
892
|
+
import { IoMulti, IoMem, IoFile } from '@rljson/io';
|
|
893
|
+
|
|
894
|
+
const io1 = new IoMem();
|
|
895
|
+
const io2 = new IoFile('/path/to/backup');
|
|
896
|
+
const io3 = new IoFile('/path/to/archive');
|
|
897
|
+
|
|
898
|
+
await io1.init();
|
|
899
|
+
await io2.init();
|
|
900
|
+
await io3.init();
|
|
901
|
+
|
|
902
|
+
const multi = new IoMulti([io1, io2, io3]);
|
|
903
|
+
const db = new Db(multi);
|
|
904
|
+
|
|
905
|
+
// Writes to all backends, reads from first available
|
|
906
|
+
// Priority: io1 > io2 > io3
|
|
907
|
+
```
|
|
908
|
+
|
|
909
|
+
## Examples
|
|
910
|
+
|
|
911
|
+
See [src/example.ts](src/example.ts) for a complete working example demonstrating:
|
|
912
|
+
|
|
913
|
+
- Table creation
|
|
914
|
+
- Data import/export
|
|
915
|
+
- Queries and filters
|
|
916
|
+
- Tree operations
|
|
917
|
+
- Version history
|
|
918
|
+
- Multi-edit operations
|
|
919
|
+
|
|
920
|
+
Run the example:
|
|
921
|
+
|
|
922
|
+
```bash
|
|
923
|
+
pnpm run build
|
|
924
|
+
node dist/example.js
|
|
925
|
+
```
|
|
926
|
+
|
|
927
|
+
## Related Packages
|
|
928
|
+
|
|
929
|
+
- `@rljson/rljson` - Core RLJSON data structures and types
|
|
930
|
+
- `@rljson/io` - Storage backend implementations
|
|
931
|
+
- `@rljson/hash` - Content-addressing utilities
|
|
932
|
+
- `@rljson/json` - JSON manipulation helpers
|
|
933
|
+
- `@rljson/validate` - Data validation utilities
|
|
934
|
+
|
|
935
|
+
## License
|
|
936
|
+
|
|
937
|
+
MIT
|
|
938
|
+
|
|
939
|
+
## Documentation
|
|
940
|
+
|
|
941
|
+
- [README.md](./README.md) - Main documentation index
|
|
942
|
+
- [README.public.md](./README.public.md) - This file (public API)
|
|
943
|
+
- [README.contributors.md](./README.contributors.md) - Developer guide
|
|
944
|
+
- [README.architecture.md](./README.architecture.md) - Technical architecture
|
|
945
|
+
- [README.trouble.md](./README.trouble.md) - Troubleshooting guide
|
|
946
|
+
- [README.blog.md](./README.blog.md) - Blog posts and updates
|