@rljson/db 0.0.3 → 0.0.5

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/dist/db.js CHANGED
@@ -1,22 +1,379 @@
1
- var __defProp = Object.defineProperty;
2
- var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
- var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
1
  import { IoMem } from "@rljson/io";
5
- import { validate } from "@rljson/validate";
2
+ import { timeId, createEditProtocolTableCfg, Validate, BaseValidator, Route, validateEdit, isTimeId } from "@rljson/rljson";
3
+ import { rmhsh, hsh } from "@rljson/hash";
4
+ import { equals } from "@rljson/json";
6
5
  // @license
7
- const _Core = class _Core {
6
+ class BaseController {
7
+ constructor(_core, _tableKey) {
8
+ this._core = _core;
9
+ this._tableKey = _tableKey;
10
+ }
11
+ // ...........................................................................
12
+ /**
13
+ * Retrieves the current state of the table.
14
+ * @returns A promise that resolves to the current state of the table.
15
+ */
16
+ async table() {
17
+ const rljson = await this._core.dumpTable(this._tableKey);
18
+ return rljson[this._tableKey];
19
+ }
20
+ // ...........................................................................
21
+ /**
22
+ * Fetches a specific entry from the table by its reference or by a partial match.
23
+ * @param where A string representing the reference of the entry to fetch, or an object representing a partial match.
24
+ * @returns A promise that resolves to an array of entries matching the criteria, or null if no entries are found.
25
+ */
26
+ async get(where, filter) {
27
+ if (typeof where === "string") {
28
+ return this._getByHash(where, filter);
29
+ } else if (typeof where === "object" && where !== null) {
30
+ return this._getByWhere(where, filter);
31
+ } else {
32
+ return Promise.resolve({});
33
+ }
34
+ }
35
+ // ...........................................................................
36
+ /**
37
+ * Fetches a specific entry from the table by its reference.
38
+ * @param hash A string representing the reference of the entry to fetch.
39
+ * @returns A promise that resolves to the entry matching the reference, or null if no entry is found.
40
+ */
41
+ async _getByHash(hash, filter) {
42
+ let result = {};
43
+ if (!filter || equals(filter, {})) {
44
+ result = await this._core.readRow(this._tableKey, hash);
45
+ } else {
46
+ result = await this._core.readRows(this._tableKey, {
47
+ _hash: hash,
48
+ ...filter
49
+ });
50
+ }
51
+ return result;
52
+ }
53
+ // ...........................................................................
54
+ /**
55
+ * Fetches entries from the table that match the specified criteria.
56
+ * @param where An object representing the criteria to match.
57
+ * @returns A promise that resolves to an array of entries matching the criteria, or null if no entries are found.
58
+ */
59
+ async _getByWhere(where, filter) {
60
+ const rows = await this._core.readRows(this._tableKey, {
61
+ ...where,
62
+ ...filter
63
+ });
64
+ return rows;
65
+ }
66
+ }
67
+ // @license
68
+ class CakeController extends BaseController {
69
+ constructor(_core, _tableKey, _refs) {
70
+ super(_core, _tableKey);
71
+ this._core = _core;
72
+ this._tableKey = _tableKey;
73
+ this._refs = _refs;
74
+ }
75
+ _baseLayers = {};
76
+ async init() {
77
+ if (this._tableKey.endsWith("Cake") === false) {
78
+ throw new Error(
79
+ `Table ${this._tableKey} is not supported by CakeController.`
80
+ );
81
+ }
82
+ const rljson = await this._core.dumpTable(this._tableKey);
83
+ const table = rljson[this._tableKey];
84
+ if (table._type !== "cakes") {
85
+ throw new Error(`Table ${this._tableKey} is not of type cakes.`);
86
+ }
87
+ if (this._refs && this._refs.base) {
88
+ const {
89
+ [this._tableKey]: { _data: baseCakes }
90
+ } = await this._core.readRow(this._tableKey, this._refs.base);
91
+ if (baseCakes.length === 0) {
92
+ throw new Error(`Base cake ${this._refs.base} does not exist.`);
93
+ }
94
+ const baseCake = baseCakes[0];
95
+ this._baseLayers = rmhsh(baseCake.layers);
96
+ if (!this._refs.sliceIdsTable || !this._refs.sliceIdsRow) {
97
+ this._refs = {
98
+ sliceIdsTable: baseCake.sliceIdsTable,
99
+ sliceIdsRow: baseCake.sliceIdsRow
100
+ };
101
+ }
102
+ } else {
103
+ const cake = table._data[0];
104
+ this._refs = {
105
+ sliceIdsTable: cake.sliceIdsTable,
106
+ sliceIdsRow: cake.sliceIdsRow
107
+ };
108
+ }
109
+ }
110
+ async run(command, value, origin, refs) {
111
+ if (!command.startsWith("add")) {
112
+ throw new Error(`Command ${command} is not supported by CakeController.`);
113
+ }
114
+ const cake = {
115
+ layers: { ...this._baseLayers, ...value },
116
+ ...refs || this._refs
117
+ };
118
+ const rlJson = { [this._tableKey]: { _data: [cake] } };
119
+ await this._core.import(rlJson);
120
+ const result = {
121
+ //Ref to component
122
+ [this._tableKey + "Ref"]: hsh(cake)._hash,
123
+ //Data from edit
124
+ route: "",
125
+ origin,
126
+ //Unique id/timestamp
127
+ timeId: timeId()
128
+ };
129
+ return result;
130
+ }
131
+ async get(where, filter) {
132
+ if (typeof where === "string") {
133
+ return this._getByHash(where, filter);
134
+ } else if (typeof where === "object" && where !== null) {
135
+ const keys = Object.keys(where);
136
+ if (keys.length === 1 && keys[0].endsWith("Ref")) {
137
+ const tableKey = keys[0].replace("Ref", "");
138
+ return this._getByRef(tableKey, where[keys[0]], filter);
139
+ } else {
140
+ return this._getByWhere(where, filter);
141
+ }
142
+ } else {
143
+ return Promise.resolve({});
144
+ }
145
+ }
146
+ async _getByRef(tableKey, ref, filter) {
147
+ let table = {};
148
+ if (!!filter && Object.keys(filter).length > 0) {
149
+ table = await this._core.readRows(
150
+ this._tableKey,
151
+ filter
152
+ );
153
+ } else {
154
+ table = await this._core.dumpTable(this._tableKey);
155
+ }
156
+ const cakes = [];
157
+ for (const row of table[this._tableKey]._data) {
158
+ const cake = row;
159
+ const layers = cake.layers;
160
+ for (const layerTable of Object.keys(layers)) {
161
+ if (layerTable === tableKey && layers[layerTable] === ref) {
162
+ cakes.push(cake);
163
+ }
164
+ }
165
+ }
166
+ return {
167
+ [this._tableKey]: { _data: cakes, _type: "cakes" }
168
+ };
169
+ }
170
+ }
171
+ // @license
172
+ class ComponentController extends BaseController {
173
+ constructor(_core, _tableKey, _refs) {
174
+ super(_core, _tableKey);
175
+ this._core = _core;
176
+ this._tableKey = _tableKey;
177
+ this._refs = _refs;
178
+ }
179
+ async init() {
180
+ if (!!this._refs && !this._refs.base) {
181
+ throw new Error(`Refs are not required on ComponentController.`);
182
+ }
183
+ const rljson = await this._core.dumpTable(this._tableKey);
184
+ const table = rljson[this._tableKey];
185
+ if (table._type !== "components") {
186
+ throw new Error(`Table ${this._tableKey} is not of type components.`);
187
+ }
188
+ }
189
+ async run(command, value, origin, refs) {
190
+ if (!command.startsWith("add")) {
191
+ throw new Error(
192
+ `Command ${command} is not supported by ComponentController.`
193
+ );
194
+ }
195
+ if (!!refs) {
196
+ throw new Error(`Refs are not supported on ComponentController.`);
197
+ }
198
+ const component = value;
199
+ const rlJson = { [this._tableKey]: { _data: [component] } };
200
+ await this._core.import(rlJson);
201
+ return {
202
+ //Ref to component
203
+ [this._tableKey + "Ref"]: hsh(component)._hash,
204
+ //Data from edit
205
+ route: "",
206
+ origin,
207
+ //Unique id/timestamp
208
+ timeId: timeId()
209
+ };
210
+ }
211
+ }
212
+ // @license
213
+ class LayerController extends BaseController {
214
+ constructor(_core, _tableKey, _refs) {
215
+ super(_core, _tableKey);
216
+ this._core = _core;
217
+ this._tableKey = _tableKey;
218
+ this._refs = _refs;
219
+ }
220
+ async init() {
221
+ if (this._tableKey.endsWith("Layer") === false) {
222
+ throw new Error(
223
+ `Table ${this._tableKey} is not supported by LayerController.`
224
+ );
225
+ }
226
+ const rljson = await this._core.dumpTable(this._tableKey);
227
+ const table = rljson[this._tableKey];
228
+ if (table._type !== "layers") {
229
+ throw new Error(`Table ${this._tableKey} is not of type layers.`);
230
+ }
231
+ if (this._refs && this._refs.base) {
232
+ const {
233
+ [this._tableKey]: { _data: baseLayers }
234
+ } = await this._core.readRow(this._tableKey, this._refs.base);
235
+ if (baseLayers.length === 0) {
236
+ throw new Error(`Base layer ${this._refs.base} does not exist.`);
237
+ }
238
+ const baseLayer = baseLayers[0];
239
+ if (
240
+ /* v8 ignore start */
241
+ !this._refs.sliceIdsTable || !this._refs.sliceIdsRow || !this._refs.componentsTable
242
+ ) {
243
+ this._refs = {
244
+ sliceIdsTable: baseLayer.sliceIdsTable,
245
+ sliceIdsTableRow: baseLayer.sliceIdsTableRow,
246
+ componentsTable: baseLayer.componentsTable
247
+ };
248
+ }
249
+ } else {
250
+ const layer = table._data[0];
251
+ this._refs = {
252
+ sliceIdsTable: layer.sliceIdsTable,
253
+ sliceIdsTableRow: layer.sliceIdsTableRow,
254
+ componentsTable: layer.componentsTable
255
+ };
256
+ }
257
+ }
258
+ async run(command, value, origin, refs) {
259
+ if (!command.startsWith("add") && !command.startsWith("remove")) {
260
+ throw new Error(
261
+ `Command ${command} is not supported by LayerController.`
262
+ );
263
+ }
264
+ const layer = command.startsWith("add") === true ? {
265
+ add: value,
266
+ ...refs || this._refs
267
+ } : {
268
+ add: {},
269
+ remove: value,
270
+ ...refs || this._refs
271
+ };
272
+ const rlJson = { [this._tableKey]: { _data: [layer] } };
273
+ await this._core.import(rlJson);
274
+ const result = {
275
+ //Ref to component
276
+ [this._tableKey + "Ref"]: hsh(layer)._hash,
277
+ //Data from edit
278
+ route: "",
279
+ origin,
280
+ //Unique id/timestamp
281
+ timeId: timeId()
282
+ };
283
+ return result;
284
+ }
285
+ async get(where, filter) {
286
+ if (typeof where === "string") {
287
+ return this._getByHash(where, filter);
288
+ } else if (typeof where === "object" && where !== null) {
289
+ const keys = Object.keys(where);
290
+ if (keys.length === 1 && keys[0].endsWith("Ref")) {
291
+ const tableKey = keys[0].replace("Ref", "");
292
+ return this._getByRef(tableKey, where[keys[0]], filter);
293
+ } else {
294
+ return this._getByWhere(where, filter);
295
+ }
296
+ } else {
297
+ return Promise.resolve({});
298
+ }
299
+ }
300
+ async _getByRef(tableKey, ref, filter) {
301
+ let table = {};
302
+ if (!!filter && Object.keys(filter).length > 0) {
303
+ table = await this._core.readRows(this._tableKey, {
304
+ ...filter
305
+ });
306
+ } else {
307
+ table = await this._core.dumpTable(this._tableKey);
308
+ }
309
+ const layers = [];
310
+ for (const row of table[this._tableKey]._data) {
311
+ const layer = row;
312
+ if (layer.componentsTable !== tableKey) {
313
+ continue;
314
+ }
315
+ for (const layerRef of Object.values(layer.add)) {
316
+ if (layerRef === ref) {
317
+ layers.push(layer);
318
+ }
319
+ }
320
+ }
321
+ return {
322
+ [this._tableKey]: { _data: layers, _type: "layers" }
323
+ };
324
+ }
325
+ }
326
+ // @license
327
+ const createController = async (type, core, tableKey, refs) => {
328
+ let ctrl;
329
+ switch (type) {
330
+ case "layers":
331
+ ctrl = new LayerController(core, tableKey, refs);
332
+ break;
333
+ case "components":
334
+ ctrl = new ComponentController(core, tableKey, refs);
335
+ break;
336
+ case "cakes":
337
+ ctrl = new CakeController(core, tableKey, refs);
338
+ break;
339
+ default:
340
+ throw new Error(`Controller for type ${type} is not implemented yet.`);
341
+ }
342
+ await ctrl.init();
343
+ return ctrl;
344
+ };
345
+ // @license
346
+ class Core {
8
347
  // ...........................................................................
9
348
  constructor(_io) {
10
349
  this._io = _io;
11
350
  }
351
+ static example = async () => {
352
+ return new Core(await IoMem.example());
353
+ };
12
354
  // ...........................................................................
355
+ /**
356
+ * Creates a table and an edit protocol for the table
357
+ * @param tableCfg TableCfg of table to create
358
+ */
359
+ async createEditable(tableCfg) {
360
+ await this.createTable(tableCfg);
361
+ await this.createEditProtocol(tableCfg);
362
+ }
13
363
  /**
14
364
  * Creates a table
15
- * @param name The name of the table.
16
- * @param type The type of the table.
365
+ * @param tableCfg TableCfg of table to create
17
366
  */
18
- async createTable(name, type) {
19
- return this._io.createTable({ name, type });
367
+ async createTable(tableCfg) {
368
+ return this._io.createOrExtendTable({ tableCfg });
369
+ }
370
+ /**
371
+ * Creates an edit protocol table for a given table
372
+ * @param tableCfg TableCfg of table
373
+ */
374
+ async createEditProtocol(tableCfg) {
375
+ const cfg = createEditProtocolTableCfg(tableCfg);
376
+ await this.createTable(cfg);
20
377
  }
21
378
  // ...........................................................................
22
379
  /**
@@ -30,17 +387,20 @@ const _Core = class _Core {
30
387
  * @returns a dump of a table.
31
388
  * @throws when table name does not exist
32
389
  */
33
- dumpTable(name) {
34
- return this._io.dumpTable({ name });
390
+ dumpTable(table) {
391
+ return this._io.dumpTable({ table });
35
392
  }
36
393
  // ...........................................................................
37
394
  /**
38
395
  * Imports data into the memory.
396
+ * @param data - The rljson data to import.
39
397
  * @throws {Error} If the data is invalid.
40
398
  */
41
399
  async import(data) {
42
- const result = validate(data);
43
- if (result.hasErrors) {
400
+ const validate = new Validate();
401
+ validate.addValidator(new BaseValidator());
402
+ const result = await validate.run(data);
403
+ if ((result.hasErrors || result.base && result.base.hasErrors) && !result.base.refsNotFound) {
44
404
  throw new Error(
45
405
  "The imported rljson data is not valid:\n" + JSON.stringify(result, null, 2)
46
406
  );
@@ -49,51 +409,467 @@ const _Core = class _Core {
49
409
  }
50
410
  // ...........................................................................
51
411
  async tables() {
52
- return this._io.tables();
412
+ return await this._io.dump();
53
413
  }
54
414
  // ...........................................................................
55
415
  async hasTable(table) {
56
- const tables = await this._io.tables();
57
- return tables.includes(table);
416
+ return await this._io.tableExists(table);
417
+ }
418
+ // ...........................................................................
419
+ async contentType(table) {
420
+ const t = await this._io.dumpTable({ table });
421
+ const contentType = t[table]?._type;
422
+ return contentType;
58
423
  }
59
424
  // ...........................................................................
60
425
  /** Reads a specific row from a database table */
61
426
  readRow(table, rowHash) {
62
- return this._io.readRow({ table, rowHash });
427
+ return this._io.readRows({ table, where: { _hash: rowHash } });
63
428
  }
64
429
  // ...........................................................................
65
430
  readRows(table, where) {
66
431
  return this._io.readRows({ table, where });
67
432
  }
68
- };
69
- __publicField(_Core, "example", async () => {
70
- return new _Core(IoMem.example());
71
- });
72
- let Core = _Core;
433
+ }
434
+ // @license
435
+ class Notify {
436
+ _callbacks = /* @__PURE__ */ new Map();
437
+ // ...........................................................................
438
+ constructor() {
439
+ }
440
+ // ...........................................................................
441
+ /**
442
+ * Registers a callback for a specific route.
443
+ * @param route The route to register the callback for.
444
+ * @param callback The callback function to be invoked when a notification is sent.
445
+ */
446
+ register(route, callback) {
447
+ this._callbacks.set(route.flat, [
448
+ ...this._callbacks.get(route.flat) || [],
449
+ callback
450
+ ]);
451
+ }
452
+ // ...........................................................................
453
+ /**
454
+ * Unregisters a callback for a specific route.
455
+ * @param route The route to unregister the callback from.
456
+ * @param callback The callback function to be removed.
457
+ */
458
+ unregister(route, callback) {
459
+ const callbacks = this._callbacks.get(route.flat);
460
+ if (callbacks) {
461
+ this._callbacks.set(
462
+ route.flat,
463
+ callbacks.filter((cb) => cb !== callback)
464
+ );
465
+ }
466
+ }
467
+ // ...........................................................................
468
+ /**
469
+ * Notifies all registered callbacks for a specific route with the provided edit protocol row.
470
+ * @param route The route to notify callbacks for.
471
+ * @param editProtocolRow The edit protocol row to pass to the callbacks.
472
+ */
473
+ notify(route, editProtocolRow) {
474
+ const callbacks = this._callbacks.get(route.flat);
475
+ if (callbacks) {
476
+ for (const cb of callbacks) {
477
+ cb(editProtocolRow);
478
+ }
479
+ }
480
+ }
481
+ // ...........................................................................
482
+ /**
483
+ * Returns the current map of registered callbacks.
484
+ * @returns A map where keys are route strings and values are arrays of callback functions.
485
+ */
486
+ get callbacks() {
487
+ return this._callbacks;
488
+ }
489
+ // ...........................................................................
490
+ /**
491
+ * Retrieves the list of callbacks registered for a specific route.
492
+ * @param route The route to get callbacks for.
493
+ * @returns An array of callback functions registered for the specified route.
494
+ */
495
+ getCallBacksForRoute(route) {
496
+ return this._callbacks.get(route.flat) || [];
497
+ }
498
+ }
73
499
  // @license
74
- const _Db = class _Db {
500
+ class Db {
75
501
  /**
76
502
  * Constructor
77
503
  * @param _io - The Io instance used to read and write data
78
504
  */
79
505
  constructor(_io) {
80
- /**
81
- * Core functionalities like importing data, setting and getting tables
82
- */
83
- __publicField(this, "core");
84
506
  this._io = _io;
85
507
  this.core = new Core(this._io);
508
+ this.notify = new Notify();
86
509
  }
87
- };
88
- /**
89
- * Example
90
- * @returns A new Db instance for test purposes
91
- */
92
- __publicField(_Db, "example", async () => {
93
- const io = new IoMem();
94
- return new _Db(io);
95
- });
96
- let Db = _Db;
510
+ /**
511
+ * Core functionalities like importing data, setting and getting tables
512
+ */
513
+ core;
514
+ /**
515
+ * Notification system to register callbacks on data changes
516
+ */
517
+ notify;
518
+ // ...........................................................................
519
+ /**
520
+ * Get data from a route with optional filtering
521
+ * @param route - The route to get data from
522
+ * @param where - Optional filter to apply to the data
523
+ * @returns An array of Rljson objects matching the route and filter
524
+ * @throws {Error} If the route is not valid or if any controller cannot be created
525
+ */
526
+ async get(route, where) {
527
+ if (!route.isValid) throw new Error(`Route ${route.flat} is not valid.`);
528
+ const controllers = await this._indexedControllers(route);
529
+ return this._get(route, where, controllers);
530
+ }
531
+ // ...........................................................................
532
+ /**
533
+ * Recursively fetches data from the given route using the provided controllers
534
+ * @param route - The route to fetch data from
535
+ * @param where - The filter to apply to the root table
536
+ * @param controllers - A record of controllers keyed by table name
537
+ * @returns An array of Rljson objects matching the route and filter
538
+ */
539
+ async _get(route, where, controllers) {
540
+ let filter = {};
541
+ if (Route.segmentHasRef(route.root)) {
542
+ let revision = "";
543
+ if (Route.segmentHasDefaultRef(route.root)) {
544
+ revision = Route.segmentRef(route.root);
545
+ } else {
546
+ revision = await this.getRefOfTimeId(
547
+ route.root.tableKey,
548
+ Route.segmentRef(route.root)
549
+ );
550
+ }
551
+ filter = { _hash: revision };
552
+ }
553
+ const rootRljson = await controllers[route.root.tableKey].get(
554
+ where,
555
+ filter
556
+ );
557
+ const rootRefs = rootRljson[route.root.tableKey]._data.map(
558
+ (r) => r._hash
559
+ );
560
+ if (route.segments.length === 1) {
561
+ return [rootRljson];
562
+ }
563
+ const results = [];
564
+ for (const ref of rootRefs) {
565
+ const superiorRoute = new Route(route.segments.slice(0, -1));
566
+ const res = await this._get(
567
+ superiorRoute,
568
+ {
569
+ [route.root.tableKey + "Ref"]: ref
570
+ },
571
+ controllers
572
+ );
573
+ results.push(...res);
574
+ }
575
+ return results.map((r) => ({ ...r, ...rootRljson }));
576
+ }
577
+ // ...........................................................................
578
+ /**
579
+ * Runs an edit by executing the appropriate controller(s) based on the edit's route
580
+ * @param edit - The edit to run
581
+ * @returns The result of the edit as an EditProtocolRow
582
+ * @throws {Error} If the edit is not valid or if any controller cannot be created
583
+ */
584
+ async run(edit, options) {
585
+ const initialRoute = Route.fromFlat(edit.route);
586
+ const runs = await this._resolveRuns(edit);
587
+ const errors = validateEdit(edit);
588
+ if (!!errors.hasErrors) {
589
+ throw new Error(`Edit is not valid:
590
+ ${JSON.stringify(errors, null, 2)}`);
591
+ }
592
+ return this._run(edit, initialRoute, runs, options);
593
+ }
594
+ // ...........................................................................
595
+ /**
596
+ * Recursively runs controllers based on the route of the edit
597
+ * @param edit - The edit to run
598
+ * @param route - The route of the edit
599
+ * @param runFns - A record of controller run functions, keyed by table name
600
+ * @returns The result of the edit
601
+ * @throws {Error} If the route is not valid or if any controller cannot be created
602
+ */
603
+ async _run(edit, route, runFns, options) {
604
+ let result;
605
+ let tableKey;
606
+ const segment = route.segment(0);
607
+ tableKey = segment.tableKey;
608
+ let previous = [];
609
+ if (Route.segmentHasRef(segment)) {
610
+ const routeRef = Route.segmentRef(segment);
611
+ if (Route.segmentHasProtocolRef(segment)) {
612
+ previous = [...previous, routeRef];
613
+ }
614
+ if (Route.segmentHasDefaultRef(segment)) {
615
+ const timeIds = await this.getTimeIdsForRef(
616
+ tableKey,
617
+ Route.segmentRef(segment)
618
+ );
619
+ previous = [...previous, ...timeIds];
620
+ }
621
+ }
622
+ if (!route.isRoot) {
623
+ const childRoute = route.deeper(1);
624
+ const childKeys = this._childKeys(route, edit.value);
625
+ const childRefs = {};
626
+ for (const k of childKeys) {
627
+ const childValue = edit.value[k];
628
+ const childEdit = { ...edit, value: childValue };
629
+ const childResult = await this._run(childEdit, childRoute, runFns);
630
+ const childRefKey = childRoute.top.tableKey + "Ref";
631
+ const childRef = childResult[childRefKey];
632
+ childRefs[k] = childRef;
633
+ }
634
+ const runFn = runFns[tableKey];
635
+ result = {
636
+ ...await runFn(
637
+ edit.command,
638
+ {
639
+ ...edit.value,
640
+ ...childRefs
641
+ },
642
+ edit.origin
643
+ ),
644
+ previous
645
+ };
646
+ } else {
647
+ tableKey = route.root.tableKey;
648
+ const runFn = runFns[tableKey];
649
+ result = {
650
+ ...await runFn(edit.command, edit.value, edit.origin),
651
+ previous
652
+ };
653
+ }
654
+ result.route = edit.route;
655
+ await this._writeProtocol(tableKey, result);
656
+ if (!options?.skipNotification)
657
+ this.notify.notify(Route.fromFlat(edit.route), result);
658
+ return result;
659
+ }
660
+ // ...........................................................................
661
+ /**
662
+ * Registers a callback to be called when an edit is made on the given route
663
+ * @param route - The route to register the callback on
664
+ * @param callback - The callback to be called when an edit is made
665
+ */
666
+ registerObserver(route, callback) {
667
+ this.notify.register(route, callback);
668
+ }
669
+ // ...........................................................................
670
+ /**
671
+ * Unregisters a callback from the given route
672
+ * @param route - The route to unregister the callback from
673
+ * @param callback - The callback to be unregistered
674
+ */
675
+ unregisterObserver(route, callback) {
676
+ this.notify.unregister(route, callback);
677
+ }
678
+ // ...........................................................................
679
+ /**
680
+ * Resolves an edit by returning the run functions of all controllers involved in the edit's route
681
+ * @param edit - The edit to resolve
682
+ * @returns A record of controller run functions, keyed by table name
683
+ * @throws {Error} If the route is not valid or if any controller cannot be created
684
+ */
685
+ async _resolveRuns(edit) {
686
+ const controllers = await this._indexedControllers(
687
+ Route.fromFlat(edit.route)
688
+ );
689
+ const runFns = {};
690
+ for (const tableKey of Object.keys(controllers)) {
691
+ runFns[tableKey] = controllers[tableKey].run.bind(controllers[tableKey]);
692
+ }
693
+ return runFns;
694
+ }
695
+ // ...........................................................................
696
+ /**
697
+ * Returns the keys of child refs in a value based on a route
698
+ * @param route - The route to check
699
+ * @param value - The value to check
700
+ * @returns An array of keys of child refs in the value
701
+ */
702
+ _childKeys(route, value) {
703
+ const keys = Object.keys(value);
704
+ const childKeys = [];
705
+ for (const k of keys) {
706
+ if (typeof value[k] !== "object") continue;
707
+ if (k.endsWith("Ref") && route.next?.tableKey + "Ref" !== k) continue;
708
+ childKeys.push(k);
709
+ }
710
+ return childKeys;
711
+ }
712
+ // ...........................................................................
713
+ /**
714
+ * Get a controller for a specific table
715
+ * @param tableKey - The key of the table to get the controller for
716
+ * @param refs - Optional references required by some controllers
717
+ * @returns A controller for the specified table
718
+ * @throws {Error} If the table does not exist or if the table type is not supported
719
+ */
720
+ async getController(tableKey, refs) {
721
+ if (typeof tableKey !== "string" || tableKey.length === 0) {
722
+ throw new Error("TableKey must be a non-empty string.");
723
+ }
724
+ const tableExists = this.core.hasTable(tableKey);
725
+ if (!tableExists) {
726
+ throw new Error(`Table ${tableKey} does not exist.`);
727
+ }
728
+ const contentType = await this.core.contentType(tableKey);
729
+ if (!contentType) {
730
+ throw new Error(`Table ${tableKey} does not have a valid content type.`);
731
+ }
732
+ return createController(contentType, this.core, tableKey, refs);
733
+ }
734
+ // ...........................................................................
735
+ async _indexedControllers(route) {
736
+ if (!route.isValid) throw new Error(`Route ${route.flat} is not valid.`);
737
+ const controllers = {};
738
+ for (let i = 0; i < route.segments.length; i++) {
739
+ const segment = route.segments[i];
740
+ const tableKey = segment.tableKey;
741
+ const segmentRef = Route.segmentRef(segment);
742
+ const base = segmentRef ? isTimeId(segmentRef) ? await this.getRefOfTimeId(tableKey, segmentRef) : segmentRef : null;
743
+ controllers[tableKey] ??= await this.getController(
744
+ tableKey,
745
+ base ? { base } : void 0
746
+ );
747
+ }
748
+ return controllers;
749
+ }
750
+ // ...........................................................................
751
+ /**
752
+ * Adds an edit protocol row to the edits table of a table
753
+ * @param table - The table the edit was made on
754
+ * @param editProtocolRow - The edit protocol row to add
755
+ * @throws {Error} If the edits table does not exist
756
+ */
757
+ async _writeProtocol(table, editProtocolRow) {
758
+ const protocolTable = table + "Edits";
759
+ const hasTable = await this.core.hasTable(protocolTable);
760
+ if (!hasTable) {
761
+ throw new Error(`Table ${table} does not exist`);
762
+ }
763
+ await this.core.import({
764
+ [protocolTable]: {
765
+ _data: [editProtocolRow],
766
+ _type: "edits"
767
+ }
768
+ });
769
+ }
770
+ // ...........................................................................
771
+ /**
772
+ * Get the edit protocol of a table
773
+ * @param table - The table to get the edit protocol for
774
+ * @throws {Error} If the edits table does not exist
775
+ */
776
+ async getProtocol(table, options) {
777
+ const protocolTable = table + "Edits";
778
+ const hasTable = await this.core.hasTable(protocolTable);
779
+ if (!hasTable) {
780
+ throw new Error(`Table ${table} does not exist`);
781
+ }
782
+ if (options === void 0) {
783
+ options = { sorted: false, ascending: true };
784
+ }
785
+ if (options.sorted) {
786
+ const dumpedTable = await this.core.dumpTable(protocolTable);
787
+ const tableData = dumpedTable[protocolTable]._data;
788
+ tableData.sort((a, b) => {
789
+ const aTime = a.timeId.split(":")[1];
790
+ const bTime = b.timeId.split(":")[1];
791
+ if (options.ascending) {
792
+ return aTime.localeCompare(bTime);
793
+ } else {
794
+ return bTime.localeCompare(aTime);
795
+ }
796
+ });
797
+ return { [protocolTable]: { _data: tableData, _type: "edits" } };
798
+ }
799
+ return this.core.dumpTable(protocolTable);
800
+ }
801
+ // ...........................................................................
802
+ /**
803
+ * Get a specific edit protocol row from a table
804
+ * @param table - The table to get the edit protocol row from
805
+ * @param ref - The reference of the edit protocol row to get
806
+ * @returns The edit protocol row or null if it does not exist
807
+ * @throws {Error} If the edits table does not exist
808
+ */
809
+ async getProtocolRowsByRef(table, ref) {
810
+ const protocolTable = table + "Edits";
811
+ const {
812
+ [protocolTable]: { _data: protocol }
813
+ } = await this.core.readRows(protocolTable, { [table + "Ref"]: ref });
814
+ return protocol || null;
815
+ }
816
+ // ...........................................................................
817
+ /**
818
+ * Get a specific edit protocol row from a table by its timeId
819
+ * @param table - The table to get the edit protocol row from
820
+ * @param timeId - The timeId of the edit protocol row to get
821
+ * @returns The edit protocol row or null if it does not exist
822
+ * @throws {Error} If the edits table does not exist
823
+ */
824
+ async getProtocolRowByTimeId(table, timeId2) {
825
+ const protocolTable = table + "Edits";
826
+ const { [protocolTable]: result } = await this.core.readRows(
827
+ protocolTable,
828
+ { timeId: timeId2 }
829
+ );
830
+ return result._data?.[0] || null;
831
+ }
832
+ // ...........................................................................
833
+ /**
834
+ * Get all timeIds for a specific ref in a table
835
+ * @param table - The table to get the timeIds from
836
+ * @param ref - The reference to get the timeIds for
837
+ * @returns An array of timeIds
838
+ * @throws {Error} If the edits table does not exist
839
+ */
840
+ async getTimeIdsForRef(table, ref) {
841
+ const protocolTable = table + "Edits";
842
+ const { [protocolTable]: result } = await this.core.readRows(
843
+ protocolTable,
844
+ { [table + "Ref"]: ref }
845
+ );
846
+ return result._data?.map((r) => r.timeId) || [];
847
+ }
848
+ // ...........................................................................
849
+ /**
850
+ * Get the ref for a specific timeId in a table
851
+ * @param table - The table to get the ref from
852
+ * @param timeId - The timeId to get the ref for
853
+ * @returns The ref or null if it does not exist
854
+ * @throws {Error} If the edits table does not exist
855
+ */
856
+ async getRefOfTimeId(table, timeId2) {
857
+ const protocolTable = table + "Edits";
858
+ const { [protocolTable]: result } = await this.core.readRows(
859
+ protocolTable,
860
+ { timeId: timeId2 }
861
+ );
862
+ return result._data?.[0]?.[table + "Ref"] || null;
863
+ }
864
+ /**
865
+ * Example
866
+ * @returns A new Db instance for test purposes
867
+ */
868
+ static example = async () => {
869
+ const io = new IoMem();
870
+ return new Db(io);
871
+ };
872
+ }
97
873
  export {
98
874
  Db as RljsonDb
99
875
  };