@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/README.contributors.md +15 -225
- package/dist/README.contributors.md +15 -225
- package/dist/cars-example.d.ts +37 -0
- package/dist/core.d.ts +17 -6
- package/dist/db.d.ts +125 -0
- package/dist/db.js +813 -37
- package/dist/notify.d.ts +39 -0
- package/dist/src/example.ts +9 -5
- package/package.json +29 -36
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 {
|
|
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
|
-
|
|
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
|
|
16
|
-
* @param type The type of the table.
|
|
365
|
+
* @param tableCfg TableCfg of table to create
|
|
17
366
|
*/
|
|
18
|
-
async createTable(
|
|
19
|
-
return this._io.
|
|
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(
|
|
34
|
-
return this._io.dumpTable({
|
|
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
|
|
43
|
-
|
|
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.
|
|
412
|
+
return await this._io.dump();
|
|
53
413
|
}
|
|
54
414
|
// ...........................................................................
|
|
55
415
|
async hasTable(table) {
|
|
56
|
-
|
|
57
|
-
|
|
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.
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
};
|