@rljson/db 0.0.5 → 0.0.6
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/cars-example.d.ts +5 -1
- package/dist/core.d.ts +6 -5
- package/dist/db.d.ts +78 -55
- package/dist/db.js +2091 -324
- package/dist/notify.d.ts +4 -4
- package/package.json +18 -16
package/dist/db.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { equals, merge } from "@rljson/json";
|
|
2
|
+
import { timeId, createInsertHistoryTableCfg, Validate, BaseValidator, Route, validateInsert, isTimeId } from "@rljson/rljson";
|
|
3
|
+
import { rmhsh, hsh, Hash, hip } from "@rljson/hash";
|
|
1
4
|
import { IoMem } from "@rljson/io";
|
|
2
|
-
import {
|
|
3
|
-
import { rmhsh, hsh } from "@rljson/hash";
|
|
4
|
-
import { equals } from "@rljson/json";
|
|
5
|
-
// @license
|
|
5
|
+
import { compileExpression } from "filtrex";
|
|
6
6
|
class BaseController {
|
|
7
7
|
constructor(_core, _tableKey) {
|
|
8
8
|
this._core = _core;
|
|
@@ -27,6 +27,9 @@ class BaseController {
|
|
|
27
27
|
if (typeof where === "string") {
|
|
28
28
|
return this._getByHash(where, filter);
|
|
29
29
|
} else if (typeof where === "object" && where !== null) {
|
|
30
|
+
if (Object.keys(where).length === 1 && "_hash" in where && typeof where["_hash"] === "string") {
|
|
31
|
+
return this._getByHash(where["_hash"], filter);
|
|
32
|
+
}
|
|
30
33
|
return this._getByWhere(where, filter);
|
|
31
34
|
} else {
|
|
32
35
|
return Promise.resolve({});
|
|
@@ -64,7 +67,6 @@ class BaseController {
|
|
|
64
67
|
return rows;
|
|
65
68
|
}
|
|
66
69
|
}
|
|
67
|
-
// @license
|
|
68
70
|
class CakeController extends BaseController {
|
|
69
71
|
constructor(_core, _tableKey, _refs) {
|
|
70
72
|
super(_core, _tableKey);
|
|
@@ -72,6 +74,7 @@ class CakeController extends BaseController {
|
|
|
72
74
|
this._tableKey = _tableKey;
|
|
73
75
|
this._refs = _refs;
|
|
74
76
|
}
|
|
77
|
+
_table = null;
|
|
75
78
|
_baseLayers = {};
|
|
76
79
|
async init() {
|
|
77
80
|
if (this._tableKey.endsWith("Cake") === false) {
|
|
@@ -80,8 +83,8 @@ class CakeController extends BaseController {
|
|
|
80
83
|
);
|
|
81
84
|
}
|
|
82
85
|
const rljson = await this._core.dumpTable(this._tableKey);
|
|
83
|
-
|
|
84
|
-
if (
|
|
86
|
+
this._table = rljson[this._tableKey];
|
|
87
|
+
if (this._table._type !== "cakes") {
|
|
85
88
|
throw new Error(`Table ${this._tableKey} is not of type cakes.`);
|
|
86
89
|
}
|
|
87
90
|
if (this._refs && this._refs.base) {
|
|
@@ -93,21 +96,33 @@ class CakeController extends BaseController {
|
|
|
93
96
|
}
|
|
94
97
|
const baseCake = baseCakes[0];
|
|
95
98
|
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
99
|
} else {
|
|
103
|
-
const cake =
|
|
100
|
+
const cake = this._table._data[0];
|
|
104
101
|
this._refs = {
|
|
105
102
|
sliceIdsTable: cake.sliceIdsTable,
|
|
106
103
|
sliceIdsRow: cake.sliceIdsRow
|
|
107
104
|
};
|
|
108
105
|
}
|
|
109
106
|
}
|
|
110
|
-
async
|
|
107
|
+
async getChildRefs(where, filter) {
|
|
108
|
+
if (!this._table) {
|
|
109
|
+
throw new Error(`Controller not initialized.`);
|
|
110
|
+
}
|
|
111
|
+
const childRefs = [];
|
|
112
|
+
const { [this._tableKey]: table } = await this.get(where, filter);
|
|
113
|
+
const cakes = table._data;
|
|
114
|
+
for (const cake of cakes) {
|
|
115
|
+
for (const layerTable of Object.keys(cake.layers)) {
|
|
116
|
+
if (layerTable.startsWith("_")) continue;
|
|
117
|
+
childRefs.push({
|
|
118
|
+
tableKey: layerTable,
|
|
119
|
+
ref: cake.layers[layerTable]
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return childRefs;
|
|
124
|
+
}
|
|
125
|
+
async insert(command, value, origin, refs) {
|
|
111
126
|
if (!command.startsWith("add")) {
|
|
112
127
|
throw new Error(`Command ${command} is not supported by CakeController.`);
|
|
113
128
|
}
|
|
@@ -132,43 +147,21 @@ class CakeController extends BaseController {
|
|
|
132
147
|
if (typeof where === "string") {
|
|
133
148
|
return this._getByHash(where, filter);
|
|
134
149
|
} else if (typeof where === "object" && where !== null) {
|
|
135
|
-
|
|
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
|
-
}
|
|
150
|
+
return this._getByWhere(where, filter);
|
|
142
151
|
} else {
|
|
143
152
|
return Promise.resolve({});
|
|
144
153
|
}
|
|
145
154
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
}
|
|
155
|
+
filterRow(row, key, value) {
|
|
156
|
+
const cake = row;
|
|
157
|
+
for (const [layerKey, layerRef] of Object.entries(cake.layers)) {
|
|
158
|
+
if (layerKey === key && layerRef === value) {
|
|
159
|
+
return true;
|
|
164
160
|
}
|
|
165
161
|
}
|
|
166
|
-
return
|
|
167
|
-
[this._tableKey]: { _data: cakes, _type: "cakes" }
|
|
168
|
-
};
|
|
162
|
+
return false;
|
|
169
163
|
}
|
|
170
164
|
}
|
|
171
|
-
// @license
|
|
172
165
|
class ComponentController extends BaseController {
|
|
173
166
|
constructor(_core, _tableKey, _refs) {
|
|
174
167
|
super(_core, _tableKey);
|
|
@@ -176,6 +169,9 @@ class ComponentController extends BaseController {
|
|
|
176
169
|
this._tableKey = _tableKey;
|
|
177
170
|
this._refs = _refs;
|
|
178
171
|
}
|
|
172
|
+
_tableCfg = null;
|
|
173
|
+
_resolvedColumns = null;
|
|
174
|
+
_refTableKeyToColumnKeyMap = null;
|
|
179
175
|
async init() {
|
|
180
176
|
if (!!this._refs && !this._refs.base) {
|
|
181
177
|
throw new Error(`Refs are not required on ComponentController.`);
|
|
@@ -185,8 +181,13 @@ class ComponentController extends BaseController {
|
|
|
185
181
|
if (table._type !== "components") {
|
|
186
182
|
throw new Error(`Table ${this._tableKey} is not of type components.`);
|
|
187
183
|
}
|
|
184
|
+
this._tableCfg = await this._core.tableCfg(this._tableKey);
|
|
185
|
+
this._resolvedColumns = await this._resolveReferenceColumns({
|
|
186
|
+
base: this._tableCfg.columns
|
|
187
|
+
});
|
|
188
|
+
this._refTableKeyToColumnKeyMap = this._createRefTableKeyToColumnKeyMap();
|
|
188
189
|
}
|
|
189
|
-
async
|
|
190
|
+
async insert(command, value, origin, refs) {
|
|
190
191
|
if (!command.startsWith("add")) {
|
|
191
192
|
throw new Error(
|
|
192
193
|
`Command ${command} is not supported by ComponentController.`
|
|
@@ -208,8 +209,285 @@ class ComponentController extends BaseController {
|
|
|
208
209
|
timeId: timeId()
|
|
209
210
|
};
|
|
210
211
|
}
|
|
212
|
+
// ...........................................................................
|
|
213
|
+
/**
|
|
214
|
+
* Retrieves references to child entries in related tables based on a condition.
|
|
215
|
+
* @param where - The condition to filter the data.
|
|
216
|
+
* @param filter - Optional filter to apply to the data.
|
|
217
|
+
* @returns
|
|
218
|
+
*/
|
|
219
|
+
async getChildRefs(where, filter) {
|
|
220
|
+
const { [this._tableKey]: table } = await this.get(where, filter);
|
|
221
|
+
const { columns } = await this._core.tableCfg(this._tableKey);
|
|
222
|
+
const childRefs = /* @__PURE__ */ new Map();
|
|
223
|
+
for (const colCfg of columns) {
|
|
224
|
+
if (!colCfg.ref || colCfg.ref === void 0) continue;
|
|
225
|
+
const propertyKey = colCfg.key;
|
|
226
|
+
const childRefTableKey = colCfg.ref.tableKey;
|
|
227
|
+
for (const row of table._data) {
|
|
228
|
+
const refValue = row[propertyKey];
|
|
229
|
+
if (typeof refValue === "string") {
|
|
230
|
+
childRefs.set(`${childRefTableKey}|${propertyKey}|${refValue}`, {
|
|
231
|
+
tableKey: childRefTableKey,
|
|
232
|
+
columnKey: propertyKey,
|
|
233
|
+
ref: refValue
|
|
234
|
+
});
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (Array.isArray(refValue)) {
|
|
238
|
+
for (const refItem of refValue) {
|
|
239
|
+
if (typeof refItem === "string") {
|
|
240
|
+
childRefs.set(`${childRefTableKey}|${propertyKey}|${refItem}`, {
|
|
241
|
+
tableKey: childRefTableKey,
|
|
242
|
+
columnKey: propertyKey,
|
|
243
|
+
ref: refItem
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return Array.from(childRefs.values());
|
|
252
|
+
}
|
|
253
|
+
// ...........................................................................
|
|
254
|
+
/**
|
|
255
|
+
* Fetches a specific entry from the table by a partial match. Resolves references as needed.
|
|
256
|
+
* @param where - An object representing a partial match.
|
|
257
|
+
* @returns A promise that resolves to an array of entries matching the criteria, or null if no entries are found.
|
|
258
|
+
*/
|
|
259
|
+
async _getByWhere(where, filter) {
|
|
260
|
+
const consolidatedWheres = [];
|
|
261
|
+
const consolidatedRows = /* @__PURE__ */ new Map();
|
|
262
|
+
const hasReferenceColumns = this._hasReferenceColumns(where);
|
|
263
|
+
if (hasReferenceColumns) {
|
|
264
|
+
const resolvedReferences = await this._resolveReferences(this._getWhereReferences(where));
|
|
265
|
+
const refWhereClauses = this._referencesToWhereClauses(resolvedReferences);
|
|
266
|
+
for (const refWhere of refWhereClauses) {
|
|
267
|
+
consolidatedWheres.push({
|
|
268
|
+
...this._getWhereBase(where),
|
|
269
|
+
...refWhere
|
|
270
|
+
});
|
|
271
|
+
const {
|
|
272
|
+
[this._tableKey]: { _data: tableData }
|
|
273
|
+
} = await this._core.dumpTable(this._tableKey);
|
|
274
|
+
const column = Object.keys(refWhere)[0];
|
|
275
|
+
const refValue = refWhere[column];
|
|
276
|
+
for (const row of tableData) {
|
|
277
|
+
if (this.filterRow(row, column, refValue)) {
|
|
278
|
+
consolidatedRows.set(row._hash, row);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
const {
|
|
284
|
+
[this._tableKey]: { _data: rows }
|
|
285
|
+
} = await this._core.readRows(this._tableKey, {
|
|
286
|
+
...where,
|
|
287
|
+
...filter
|
|
288
|
+
});
|
|
289
|
+
for (const row of rows) {
|
|
290
|
+
consolidatedRows.set(row._hash, row);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
[this._tableKey]: {
|
|
295
|
+
_data: Array.from(consolidatedRows.values()),
|
|
296
|
+
_type: "components"
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
_referencesToWhereClauses(references) {
|
|
301
|
+
const whereClauses = [];
|
|
302
|
+
for (const [tableKey, refs] of Object.entries(references)) {
|
|
303
|
+
const wherePropertyKeys = this._refTableKeyToColumnKeyMap?.[tableKey];
|
|
304
|
+
for (const propKey of wherePropertyKeys) {
|
|
305
|
+
for (const ref of refs) {
|
|
306
|
+
whereClauses.push({ [propKey]: ref });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return whereClauses;
|
|
311
|
+
}
|
|
312
|
+
_createRefTableKeyToColumnKeyMap() {
|
|
313
|
+
const map = {};
|
|
314
|
+
const columns = this._tableCfg?.columns;
|
|
315
|
+
for (const colCfg of columns) {
|
|
316
|
+
if (colCfg.ref) {
|
|
317
|
+
const tableKey = colCfg.ref.tableKey;
|
|
318
|
+
if (!map[tableKey]) {
|
|
319
|
+
map[tableKey] = [];
|
|
320
|
+
}
|
|
321
|
+
map[tableKey].push(colCfg.key);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return map;
|
|
325
|
+
}
|
|
326
|
+
// ...........................................................................
|
|
327
|
+
/**
|
|
328
|
+
* Extracts reference columns from the where clause.
|
|
329
|
+
* @param where - The condition to filter the data.
|
|
330
|
+
* @returns An object representing only the reference columns in the where clause.
|
|
331
|
+
*/
|
|
332
|
+
_getWhereReferences(where) {
|
|
333
|
+
const whereRefs = {};
|
|
334
|
+
for (const colCfg of this._referenceColumns) {
|
|
335
|
+
if (colCfg.key in where) {
|
|
336
|
+
whereRefs[colCfg.key] = where[colCfg.key];
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return whereRefs;
|
|
340
|
+
}
|
|
341
|
+
// ...........................................................................
|
|
342
|
+
/**
|
|
343
|
+
* Removes reference columns from the where clause.
|
|
344
|
+
* @param where - The condition to filter the data.
|
|
345
|
+
* @returns An object representing the where clause without reference columns.
|
|
346
|
+
*/
|
|
347
|
+
_getWhereBase(where) {
|
|
348
|
+
const whereWithoutRefs = { ...where };
|
|
349
|
+
for (const colCfg of this._referenceColumns) {
|
|
350
|
+
if (colCfg.key in whereWithoutRefs) {
|
|
351
|
+
delete whereWithoutRefs[colCfg.key];
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return whereWithoutRefs;
|
|
355
|
+
}
|
|
356
|
+
// ...........................................................................
|
|
357
|
+
/**
|
|
358
|
+
* Retrieves all reference columns from the resolved columns.
|
|
359
|
+
* @returns An array of ColumnCfg representing the reference columns.
|
|
360
|
+
*/
|
|
361
|
+
get _referenceColumns() {
|
|
362
|
+
if (!this._resolvedColumns) {
|
|
363
|
+
throw new Error(
|
|
364
|
+
`Resolved columns are not available for table ${this._tableKey}. You must call init() first.`
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
const references = [];
|
|
368
|
+
for (const refCols of Object.values(this._resolvedColumns.references)) {
|
|
369
|
+
references.push(...refCols);
|
|
370
|
+
}
|
|
371
|
+
return references;
|
|
372
|
+
}
|
|
373
|
+
// ...........................................................................
|
|
374
|
+
/**
|
|
375
|
+
* Checks if the where clause contains any reference columns.
|
|
376
|
+
* @param where - The condition to filter the data.
|
|
377
|
+
* @returns A promise that resolves to true if reference columns are present, false otherwise.
|
|
378
|
+
*/
|
|
379
|
+
_hasReferenceColumns(where) {
|
|
380
|
+
if (!this._resolvedColumns) {
|
|
381
|
+
throw new Error(
|
|
382
|
+
`Resolved columns are not available for table ${this._tableKey}. You must call init() first.`
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
for (const colCfg of this._referenceColumns) {
|
|
386
|
+
if (colCfg.key in where) {
|
|
387
|
+
return true;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
// ...........................................................................
|
|
393
|
+
/**
|
|
394
|
+
* Resolves reference columns in the where clause.
|
|
395
|
+
* @param columns - The columns to resolve.
|
|
396
|
+
* @returns A promise that resolves to an object containing base and reference columns.
|
|
397
|
+
*/
|
|
398
|
+
async _resolveReferenceColumns(columns) {
|
|
399
|
+
const base = [];
|
|
400
|
+
const references = {};
|
|
401
|
+
for (const col of columns.base) {
|
|
402
|
+
if (!!col.ref) {
|
|
403
|
+
const refTableKey = col.ref.tableKey;
|
|
404
|
+
const { columns: refColumns } = await this._core.tableCfg(refTableKey);
|
|
405
|
+
if (!references[refTableKey]) {
|
|
406
|
+
references[refTableKey] = [];
|
|
407
|
+
}
|
|
408
|
+
const refsHaveRefs = refColumns.some((c) => !!c.ref);
|
|
409
|
+
if (refsHaveRefs) {
|
|
410
|
+
const resolvedRefColumns = await this._resolveReferenceColumns({
|
|
411
|
+
base: refColumns
|
|
412
|
+
});
|
|
413
|
+
references[refTableKey].push(...resolvedRefColumns.base);
|
|
414
|
+
}
|
|
415
|
+
references[refTableKey].push(...refColumns);
|
|
416
|
+
} else {
|
|
417
|
+
base.push(col);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return { base, references };
|
|
421
|
+
}
|
|
422
|
+
// ...........................................................................
|
|
423
|
+
/**
|
|
424
|
+
* Resolves references based on the where clause.
|
|
425
|
+
* @param where - The condition to filter the data.
|
|
426
|
+
* @returns - A promise that resolves to an object containing resolved references.
|
|
427
|
+
*/
|
|
428
|
+
async _resolveReferences(where) {
|
|
429
|
+
if (!this._resolvedColumns) {
|
|
430
|
+
throw new Error(
|
|
431
|
+
`Resolved columns are not available for table ${this._tableKey}. You must call init() first.`
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
const resolvedReferences = {};
|
|
435
|
+
const references = this._resolvedColumns.references;
|
|
436
|
+
for (const [tableKey, refColumns] of Object.entries(references)) {
|
|
437
|
+
const whereForTable = {};
|
|
438
|
+
for (const colCfg of refColumns) {
|
|
439
|
+
if (colCfg.key in where) {
|
|
440
|
+
whereForTable[colCfg.key] = where[colCfg.key];
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
if (Object.keys(whereForTable).length === 0) {
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
const refRows = await this._readRowsWithReferences(
|
|
447
|
+
tableKey,
|
|
448
|
+
whereForTable
|
|
449
|
+
);
|
|
450
|
+
const refs = refRows[tableKey]._data.map(
|
|
451
|
+
(r) => r._hash
|
|
452
|
+
);
|
|
453
|
+
resolvedReferences[tableKey] = refs;
|
|
454
|
+
}
|
|
455
|
+
return resolvedReferences;
|
|
456
|
+
}
|
|
457
|
+
async _readRowsWithReferences(table, where) {
|
|
458
|
+
const splitted = [];
|
|
459
|
+
for (const [key, value] of Object.entries(where)) {
|
|
460
|
+
if (Array.isArray(value)) {
|
|
461
|
+
for (const v of value) {
|
|
462
|
+
splitted.push({ ...where, ...{ [key]: v } });
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (splitted.length > 0) {
|
|
467
|
+
const results = [];
|
|
468
|
+
for (const s of splitted) {
|
|
469
|
+
results.push(await this._core.readRows(table, s));
|
|
470
|
+
}
|
|
471
|
+
return merge(...results);
|
|
472
|
+
} else {
|
|
473
|
+
return this._core.readRows(table, where);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
filterRow(row, key, value) {
|
|
477
|
+
for (const [propertyKey, propertyValue] of Object.entries(row)) {
|
|
478
|
+
if (propertyKey === key && equals(propertyValue, value)) {
|
|
479
|
+
return true;
|
|
480
|
+
} else if (Array.isArray(propertyValue)) {
|
|
481
|
+
for (const item of propertyValue) {
|
|
482
|
+
if (equals(item, value)) {
|
|
483
|
+
return true;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
211
490
|
}
|
|
212
|
-
// @license
|
|
213
491
|
class LayerController extends BaseController {
|
|
214
492
|
constructor(_core, _tableKey, _refs) {
|
|
215
493
|
super(_core, _tableKey);
|
|
@@ -236,10 +514,7 @@ class LayerController extends BaseController {
|
|
|
236
514
|
throw new Error(`Base layer ${this._refs.base} does not exist.`);
|
|
237
515
|
}
|
|
238
516
|
const baseLayer = baseLayers[0];
|
|
239
|
-
if (
|
|
240
|
-
/* v8 ignore start */
|
|
241
|
-
!this._refs.sliceIdsTable || !this._refs.sliceIdsRow || !this._refs.componentsTable
|
|
242
|
-
) {
|
|
517
|
+
if (!this._refs.sliceIdsTable || !this._refs.sliceIdsRow || !this._refs.componentsTable) {
|
|
243
518
|
this._refs = {
|
|
244
519
|
sliceIdsTable: baseLayer.sliceIdsTable,
|
|
245
520
|
sliceIdsTableRow: baseLayer.sliceIdsTableRow,
|
|
@@ -255,7 +530,7 @@ class LayerController extends BaseController {
|
|
|
255
530
|
};
|
|
256
531
|
}
|
|
257
532
|
}
|
|
258
|
-
async
|
|
533
|
+
async insert(command, value, origin, refs) {
|
|
259
534
|
if (!command.startsWith("add") && !command.startsWith("remove")) {
|
|
260
535
|
throw new Error(
|
|
261
536
|
`Command ${command} is not supported by LayerController.`
|
|
@@ -285,45 +560,36 @@ class LayerController extends BaseController {
|
|
|
285
560
|
async get(where, filter) {
|
|
286
561
|
if (typeof where === "string") {
|
|
287
562
|
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
563
|
} else {
|
|
297
|
-
return
|
|
564
|
+
return this._getByWhere(where, filter);
|
|
298
565
|
}
|
|
299
566
|
}
|
|
300
|
-
async
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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) {
|
|
567
|
+
async getChildRefs(where, filter) {
|
|
568
|
+
const { [this._tableKey]: table } = await this.get(where, filter);
|
|
569
|
+
const childRefs = [];
|
|
570
|
+
for (const row of table._data) {
|
|
311
571
|
const layer = row;
|
|
312
|
-
|
|
313
|
-
continue;
|
|
572
|
+
for (const [sliceId, compRef] of Object.entries(layer.add)) {
|
|
573
|
+
if (sliceId.startsWith("_")) continue;
|
|
574
|
+
childRefs.push({
|
|
575
|
+
tableKey: layer.componentsTable,
|
|
576
|
+
ref: compRef
|
|
577
|
+
});
|
|
314
578
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
579
|
+
}
|
|
580
|
+
return childRefs;
|
|
581
|
+
}
|
|
582
|
+
filterRow(row, _, value) {
|
|
583
|
+
const layer = row;
|
|
584
|
+
const compRef = value;
|
|
585
|
+
for (const componentRef of Object.values(layer.add)) {
|
|
586
|
+
if (componentRef === compRef) {
|
|
587
|
+
return true;
|
|
319
588
|
}
|
|
320
589
|
}
|
|
321
|
-
return
|
|
322
|
-
[this._tableKey]: { _data: layers, _type: "layers" }
|
|
323
|
-
};
|
|
590
|
+
return false;
|
|
324
591
|
}
|
|
325
592
|
}
|
|
326
|
-
// @license
|
|
327
593
|
const createController = async (type, core, tableKey, refs) => {
|
|
328
594
|
let ctrl;
|
|
329
595
|
switch (type) {
|
|
@@ -342,7 +608,6 @@ const createController = async (type, core, tableKey, refs) => {
|
|
|
342
608
|
await ctrl.init();
|
|
343
609
|
return ctrl;
|
|
344
610
|
};
|
|
345
|
-
// @license
|
|
346
611
|
class Core {
|
|
347
612
|
// ...........................................................................
|
|
348
613
|
constructor(_io) {
|
|
@@ -353,12 +618,12 @@ class Core {
|
|
|
353
618
|
};
|
|
354
619
|
// ...........................................................................
|
|
355
620
|
/**
|
|
356
|
-
* Creates a table and an
|
|
621
|
+
* Creates a table and an insertHistory for the table
|
|
357
622
|
* @param tableCfg TableCfg of table to create
|
|
358
623
|
*/
|
|
359
|
-
async
|
|
624
|
+
async createTableWithInsertHistory(tableCfg) {
|
|
360
625
|
await this.createTable(tableCfg);
|
|
361
|
-
await this.
|
|
626
|
+
await this.createInsertHistory(tableCfg);
|
|
362
627
|
}
|
|
363
628
|
/**
|
|
364
629
|
* Creates a table
|
|
@@ -368,11 +633,11 @@ class Core {
|
|
|
368
633
|
return this._io.createOrExtendTable({ tableCfg });
|
|
369
634
|
}
|
|
370
635
|
/**
|
|
371
|
-
* Creates an
|
|
636
|
+
* Creates an insertHistory table for a given table
|
|
372
637
|
* @param tableCfg TableCfg of table
|
|
373
638
|
*/
|
|
374
|
-
async
|
|
375
|
-
const cfg =
|
|
639
|
+
async createInsertHistory(tableCfg) {
|
|
640
|
+
const cfg = createInsertHistoryTableCfg(tableCfg);
|
|
376
641
|
await this.createTable(cfg);
|
|
377
642
|
}
|
|
378
643
|
// ...........................................................................
|
|
@@ -422,6 +687,21 @@ class Core {
|
|
|
422
687
|
return contentType;
|
|
423
688
|
}
|
|
424
689
|
// ...........................................................................
|
|
690
|
+
async tableCfg(table) {
|
|
691
|
+
const { [table]: dump } = await this._io.dumpTable({ table });
|
|
692
|
+
const tableCfgRef = dump._tableCfg;
|
|
693
|
+
const tableCfgs = await this._io.rawTableCfgs();
|
|
694
|
+
let tableCfg;
|
|
695
|
+
if (!tableCfgRef) {
|
|
696
|
+
tableCfg = tableCfgs.find((tc) => tc.key === table);
|
|
697
|
+
} else {
|
|
698
|
+
tableCfg = tableCfgs.find(
|
|
699
|
+
(tc) => tc.key === table && tc._hash === tableCfgRef
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
return tableCfg;
|
|
703
|
+
}
|
|
704
|
+
// ...........................................................................
|
|
425
705
|
/** Reads a specific row from a database table */
|
|
426
706
|
readRow(table, rowHash) {
|
|
427
707
|
return this._io.readRows({ table, where: { _hash: rowHash } });
|
|
@@ -431,176 +711,1621 @@ class Core {
|
|
|
431
711
|
return this._io.readRows({ table, where });
|
|
432
712
|
}
|
|
433
713
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
714
|
+
class ColumnSelection {
|
|
715
|
+
constructor(columns) {
|
|
716
|
+
this._throwOnWrongAlias(columns);
|
|
717
|
+
this.routes = columns.map((column) => column.route);
|
|
718
|
+
this.routeHashes = this.routes.map(ColumnSelection.calcHash);
|
|
719
|
+
this.aliases = columns.map((column) => column.alias);
|
|
720
|
+
this.columns = this._initColumns(columns);
|
|
721
|
+
ColumnSelection.check(this.aliases, this.routes);
|
|
439
722
|
}
|
|
440
723
|
// ...........................................................................
|
|
441
724
|
/**
|
|
442
|
-
*
|
|
443
|
-
* @param
|
|
444
|
-
* @
|
|
725
|
+
* Returns unique routes from a list of routes
|
|
726
|
+
* @param routes - The list of routes
|
|
727
|
+
* @returns Unique routes
|
|
445
728
|
*/
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
]);
|
|
729
|
+
static uniqueRoutes(routes) {
|
|
730
|
+
return Array.from(new Set(routes.map((r) => r.flat))).map(
|
|
731
|
+
(flat) => Route.fromFlat(flat)
|
|
732
|
+
);
|
|
451
733
|
}
|
|
452
734
|
// ...........................................................................
|
|
453
735
|
/**
|
|
454
|
-
*
|
|
455
|
-
* @param
|
|
456
|
-
* @
|
|
736
|
+
* Returns a ColumnSelection from a list of route segments
|
|
737
|
+
* @param routeSegmentsList - A list of route segments
|
|
738
|
+
* @returns A ColumnSelection object
|
|
457
739
|
*/
|
|
458
|
-
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
740
|
+
static fromRoutes(routes) {
|
|
741
|
+
const definition = [];
|
|
742
|
+
const aliasCountMap = {};
|
|
743
|
+
for (const route of this.uniqueRoutes(routes)) {
|
|
744
|
+
const alias = route.root.tableKey;
|
|
745
|
+
let uniqueAlias = alias;
|
|
746
|
+
const aliasCount = aliasCountMap[alias] ?? 0;
|
|
747
|
+
if (aliasCount > 0) {
|
|
748
|
+
uniqueAlias = `${alias}${aliasCount}`;
|
|
749
|
+
}
|
|
750
|
+
aliasCountMap[alias] = aliasCount + 1;
|
|
751
|
+
definition.push({
|
|
752
|
+
key: uniqueAlias,
|
|
753
|
+
type: "jsonValue",
|
|
754
|
+
alias: uniqueAlias,
|
|
755
|
+
route: route.flat.slice(1),
|
|
756
|
+
titleLong: "",
|
|
757
|
+
titleShort: ""
|
|
758
|
+
});
|
|
465
759
|
}
|
|
760
|
+
return new ColumnSelection(definition);
|
|
466
761
|
}
|
|
467
762
|
// ...........................................................................
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
const callbacks = this._callbacks.get(route.flat);
|
|
475
|
-
if (callbacks) {
|
|
476
|
-
for (const cb of callbacks) {
|
|
477
|
-
cb(editProtocolRow);
|
|
478
|
-
}
|
|
479
|
-
}
|
|
763
|
+
columns;
|
|
764
|
+
routes;
|
|
765
|
+
aliases;
|
|
766
|
+
routeHashes;
|
|
767
|
+
metadata(key) {
|
|
768
|
+
return this.columns.map((column) => column[key]);
|
|
480
769
|
}
|
|
481
770
|
// ...........................................................................
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
771
|
+
static merge(columnSelections) {
|
|
772
|
+
const routes = columnSelections.map((selection) => selection.routes).flat();
|
|
773
|
+
const routesWithoutDuplicates = Array.from(new Set(routes));
|
|
774
|
+
return ColumnSelection.fromRoutes(
|
|
775
|
+
routesWithoutDuplicates.map((route) => Route.fromFlat(route))
|
|
776
|
+
);
|
|
488
777
|
}
|
|
489
778
|
// ...........................................................................
|
|
490
|
-
|
|
491
|
-
|
|
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) || [];
|
|
779
|
+
static calcHash(str) {
|
|
780
|
+
return Hash.default.calcHash(str);
|
|
497
781
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
class Db {
|
|
501
|
-
/**
|
|
502
|
-
* Constructor
|
|
503
|
-
* @param _io - The Io instance used to read and write data
|
|
504
|
-
*/
|
|
505
|
-
constructor(_io) {
|
|
506
|
-
this._io = _io;
|
|
507
|
-
this.core = new Core(this._io);
|
|
508
|
-
this.notify = new Notify();
|
|
782
|
+
route(aliasRouteOrHash) {
|
|
783
|
+
return this.column(aliasRouteOrHash).route;
|
|
509
784
|
}
|
|
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
785
|
// ...........................................................................
|
|
519
|
-
|
|
520
|
-
|
|
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);
|
|
786
|
+
alias(aliasRouteOrHash) {
|
|
787
|
+
return this.column(aliasRouteOrHash).alias;
|
|
530
788
|
}
|
|
531
789
|
// ...........................................................................
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
);
|
|
790
|
+
columnIndex(hashAliasOrRoute, throwIfNotExisting = true) {
|
|
791
|
+
if (typeof hashAliasOrRoute === "number") {
|
|
792
|
+
return hashAliasOrRoute;
|
|
793
|
+
}
|
|
794
|
+
const str = Array.isArray(hashAliasOrRoute) ? hashAliasOrRoute.join("/") : hashAliasOrRoute;
|
|
795
|
+
const hashIndex = this.routeHashes.indexOf(str);
|
|
796
|
+
if (hashIndex >= 0) {
|
|
797
|
+
return hashIndex;
|
|
798
|
+
}
|
|
799
|
+
const aliasIndex = this.aliases.indexOf(str);
|
|
800
|
+
if (aliasIndex >= 0) {
|
|
801
|
+
return aliasIndex;
|
|
802
|
+
}
|
|
803
|
+
const routeIndex = this.routes.indexOf(str);
|
|
804
|
+
if (routeIndex < 0) {
|
|
805
|
+
if (throwIfNotExisting) {
|
|
806
|
+
throw new Error(`Unknown column alias or route: ${str}`);
|
|
550
807
|
}
|
|
551
|
-
|
|
808
|
+
return -1;
|
|
809
|
+
}
|
|
810
|
+
return routeIndex;
|
|
811
|
+
}
|
|
812
|
+
/***
|
|
813
|
+
* Returns the column config for a specific alias, route or hash.
|
|
814
|
+
*/
|
|
815
|
+
column(aliasRouteOrHash) {
|
|
816
|
+
const index = this.columnIndex(aliasRouteOrHash);
|
|
817
|
+
return this.columns[index];
|
|
818
|
+
}
|
|
819
|
+
// ...........................................................................
|
|
820
|
+
get count() {
|
|
821
|
+
return this.aliases.length;
|
|
822
|
+
}
|
|
823
|
+
// ...........................................................................
|
|
824
|
+
addedColumns(columnSelection) {
|
|
825
|
+
const a = this.routes.filter(
|
|
826
|
+
(route) => !columnSelection.routes.includes(route)
|
|
827
|
+
);
|
|
828
|
+
return a;
|
|
829
|
+
}
|
|
830
|
+
// ...........................................................................
|
|
831
|
+
static check(aliases, routes) {
|
|
832
|
+
const camelCaseRegex = /^[a-z][a-zA-Z0-9]*$/;
|
|
833
|
+
const invalidKeys = aliases.filter((key) => !camelCaseRegex.test(key));
|
|
834
|
+
if (invalidKeys.length > 0) {
|
|
835
|
+
throw new Error(
|
|
836
|
+
`Invalid alias "${invalidKeys[0]}". Aliases must be lower camel case.`
|
|
837
|
+
);
|
|
552
838
|
}
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
|
|
839
|
+
const validValueRegex = /^[a-zA-Z0-9/]*$/;
|
|
840
|
+
const invalidValues = routes.filter(
|
|
841
|
+
(value) => !validValueRegex.test(value)
|
|
556
842
|
);
|
|
557
|
-
|
|
558
|
-
|
|
843
|
+
if (invalidValues.length > 0) {
|
|
844
|
+
throw new Error(
|
|
845
|
+
`Invalid route "${invalidValues}". Routes must only contain letters, numbers and slashes.`
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
const pathParts = routes.map((value) => value.split("/")).flat();
|
|
849
|
+
const invalidPathParts = pathParts.filter(
|
|
850
|
+
(part) => !camelCaseRegex.test(part)
|
|
559
851
|
);
|
|
560
|
-
if (
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
852
|
+
if (invalidPathParts.length > 0) {
|
|
853
|
+
throw new Error(
|
|
854
|
+
`Invalid route segment "${invalidPathParts[0]}". Route segments must be lower camel case.`
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
const routeCountMap = {};
|
|
858
|
+
routes.forEach((value) => {
|
|
859
|
+
routeCountMap[value] = (routeCountMap[value] ?? 0) + 1;
|
|
860
|
+
});
|
|
861
|
+
const duplicateRoutes = Object.entries(routeCountMap).filter(([, count]) => count > 1).map(([route]) => route);
|
|
862
|
+
if (duplicateRoutes.length > 0) {
|
|
863
|
+
throw new Error(
|
|
864
|
+
`Duplicate route ${duplicateRoutes[0]}. A column must only occur once.`
|
|
572
865
|
);
|
|
573
|
-
results.push(...res);
|
|
574
866
|
}
|
|
575
|
-
return results.map((r) => ({ ...r, ...rootRljson }));
|
|
576
867
|
}
|
|
577
|
-
//
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
const errors = validateEdit(edit);
|
|
588
|
-
if (!!errors.hasErrors) {
|
|
589
|
-
throw new Error(`Edit is not valid:
|
|
590
|
-
${JSON.stringify(errors, null, 2)}`);
|
|
868
|
+
// ######################
|
|
869
|
+
// Private
|
|
870
|
+
// ######################
|
|
871
|
+
_throwOnWrongAlias(columns) {
|
|
872
|
+
const aliases = /* @__PURE__ */ new Set();
|
|
873
|
+
for (const column of columns) {
|
|
874
|
+
if (aliases.has(column.alias)) {
|
|
875
|
+
throw new Error(`Duplicate alias: ${column.alias}`);
|
|
876
|
+
}
|
|
877
|
+
aliases.add(column.alias);
|
|
591
878
|
}
|
|
592
|
-
|
|
879
|
+
}
|
|
880
|
+
_initColumns(columns) {
|
|
881
|
+
let i = 0;
|
|
882
|
+
return columns.map(
|
|
883
|
+
(column) => hip({
|
|
884
|
+
...column,
|
|
885
|
+
routeHash: this.routeHashes[i],
|
|
886
|
+
index: i++,
|
|
887
|
+
_hash: ""
|
|
888
|
+
})
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
// ######################
|
|
892
|
+
// Example
|
|
893
|
+
// ######################
|
|
894
|
+
static example() {
|
|
895
|
+
return new ColumnSelection([
|
|
896
|
+
{
|
|
897
|
+
key: "stringCol",
|
|
898
|
+
alias: "stringCol",
|
|
899
|
+
route: "basicTypes/stringsRef/value",
|
|
900
|
+
type: "string",
|
|
901
|
+
titleLong: "String values",
|
|
902
|
+
titleShort: "Strings"
|
|
903
|
+
},
|
|
904
|
+
{
|
|
905
|
+
key: "intCol",
|
|
906
|
+
alias: "intCol",
|
|
907
|
+
route: "basicTypes/numbersRef/intsRef/value",
|
|
908
|
+
type: "number",
|
|
909
|
+
titleLong: "Int values",
|
|
910
|
+
titleShort: "Ints"
|
|
911
|
+
},
|
|
912
|
+
{
|
|
913
|
+
key: "floatCol",
|
|
914
|
+
alias: "floatCol",
|
|
915
|
+
route: "basicTypes/numbersRef/floatsRef/value",
|
|
916
|
+
type: "number",
|
|
917
|
+
titleLong: "Float values",
|
|
918
|
+
titleShort: "Floats"
|
|
919
|
+
},
|
|
920
|
+
{
|
|
921
|
+
key: "booleanCol",
|
|
922
|
+
alias: "booleanCol",
|
|
923
|
+
route: "basicTypes/booleansRef/value",
|
|
924
|
+
type: "boolean",
|
|
925
|
+
titleLong: "Boolean values",
|
|
926
|
+
titleShort: "Booleans"
|
|
927
|
+
},
|
|
928
|
+
{
|
|
929
|
+
key: "jsonObjectCol",
|
|
930
|
+
alias: "jsonObjectCol",
|
|
931
|
+
route: "complexTypes/jsonObjectsRef/value",
|
|
932
|
+
type: "json",
|
|
933
|
+
titleLong: "Json objects",
|
|
934
|
+
titleShort: "JO"
|
|
935
|
+
},
|
|
936
|
+
{
|
|
937
|
+
key: "jsonArrayCol",
|
|
938
|
+
alias: "jsonArrayCol",
|
|
939
|
+
route: "complexTypes/jsonArraysRef/value",
|
|
940
|
+
type: "jsonArray",
|
|
941
|
+
titleLong: "Array values",
|
|
942
|
+
titleShort: "JA"
|
|
943
|
+
},
|
|
944
|
+
{
|
|
945
|
+
key: "jsonValueCol",
|
|
946
|
+
alias: "jsonValueCol",
|
|
947
|
+
route: "complexTypes/jsonValuesRef/value",
|
|
948
|
+
type: "jsonValue",
|
|
949
|
+
titleLong: "Json values",
|
|
950
|
+
titleShort: "JV"
|
|
951
|
+
}
|
|
952
|
+
]);
|
|
953
|
+
}
|
|
954
|
+
static exampleBroken() {
|
|
955
|
+
return [
|
|
956
|
+
{
|
|
957
|
+
key: "stringCol",
|
|
958
|
+
alias: "stringCol",
|
|
959
|
+
route: "basicTypes/stringsRef/value",
|
|
960
|
+
type: "string",
|
|
961
|
+
titleLong: "String values",
|
|
962
|
+
titleShort: "Strings"
|
|
963
|
+
},
|
|
964
|
+
{
|
|
965
|
+
key: "stringCol2",
|
|
966
|
+
alias: "stringCol",
|
|
967
|
+
// ⚠️ Duplicate alias
|
|
968
|
+
route: "basicTypes/stringsRef/value",
|
|
969
|
+
type: "string",
|
|
970
|
+
titleLong: "String values",
|
|
971
|
+
titleShort: "Strings"
|
|
972
|
+
}
|
|
973
|
+
];
|
|
974
|
+
}
|
|
975
|
+
static exampleCarsColumnSelection() {
|
|
976
|
+
return new ColumnSelection([
|
|
977
|
+
{
|
|
978
|
+
route: "carCake/carGeneralLayer/carGeneral/brand",
|
|
979
|
+
key: "brand",
|
|
980
|
+
alias: "brand",
|
|
981
|
+
titleShort: "Brand",
|
|
982
|
+
titleLong: "Car Brand",
|
|
983
|
+
type: "string"
|
|
984
|
+
},
|
|
985
|
+
{
|
|
986
|
+
route: "carCake/carGeneralLayer/carGeneral/type",
|
|
987
|
+
key: "type",
|
|
988
|
+
alias: "type",
|
|
989
|
+
titleShort: "Type",
|
|
990
|
+
titleLong: "Car Type",
|
|
991
|
+
type: "string"
|
|
992
|
+
},
|
|
993
|
+
{
|
|
994
|
+
route: "carCake/carGeneralLayer/carGeneral/isElectric",
|
|
995
|
+
key: "isElectric",
|
|
996
|
+
alias: "isElectric",
|
|
997
|
+
titleShort: "Is Electric",
|
|
998
|
+
titleLong: "This Car is Electric",
|
|
999
|
+
type: "boolean"
|
|
1000
|
+
},
|
|
1001
|
+
{
|
|
1002
|
+
route: "carCake/carTechnicalLayer/carTechnical/transmission",
|
|
1003
|
+
key: "transmission",
|
|
1004
|
+
alias: "transmission",
|
|
1005
|
+
titleShort: "Transmission",
|
|
1006
|
+
titleLong: "Type of Transmission",
|
|
1007
|
+
type: "string"
|
|
1008
|
+
},
|
|
1009
|
+
{
|
|
1010
|
+
route: "carCake/carColorLayer/carColor/sides",
|
|
1011
|
+
key: "sides",
|
|
1012
|
+
alias: "sides",
|
|
1013
|
+
titleShort: "Sides",
|
|
1014
|
+
titleLong: "Car Sides Color",
|
|
1015
|
+
type: "string"
|
|
1016
|
+
}
|
|
1017
|
+
]);
|
|
593
1018
|
}
|
|
594
1019
|
// ...........................................................................
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
1020
|
+
static empty() {
|
|
1021
|
+
return new ColumnSelection([]);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
const trueValues = ["t", "j", "y"];
|
|
1025
|
+
const falseValues = ["n", "f"];
|
|
1026
|
+
const parseBooleanSearch = (search) => {
|
|
1027
|
+
if (typeof search == "undefined" || search == null) {
|
|
1028
|
+
return null;
|
|
1029
|
+
}
|
|
1030
|
+
if (typeof search == "boolean") {
|
|
1031
|
+
return search;
|
|
1032
|
+
}
|
|
1033
|
+
if (typeof search == "number") {
|
|
1034
|
+
return search != 0;
|
|
1035
|
+
}
|
|
1036
|
+
if (typeof search == "string") {
|
|
1037
|
+
const val = search.toLowerCase();
|
|
1038
|
+
for (const trueValue of trueValues) {
|
|
1039
|
+
if (val.startsWith(trueValue)) {
|
|
1040
|
+
return true;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
for (const falseValue of falseValues) {
|
|
1044
|
+
if (val.startsWith(falseValue)) {
|
|
1045
|
+
return false;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
const containsOnlyNumbers = /^\d+$/.test(search);
|
|
1049
|
+
if (containsOnlyNumbers) {
|
|
1050
|
+
return parseInt(search) != 0;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
return false;
|
|
1054
|
+
};
|
|
1055
|
+
const exampleBooleanFilter = () => hip({
|
|
1056
|
+
column: "basicTypes/booleansRef/value",
|
|
1057
|
+
operator: "equals",
|
|
1058
|
+
type: "boolean",
|
|
1059
|
+
search: true,
|
|
1060
|
+
_hash: ""
|
|
1061
|
+
});
|
|
1062
|
+
class BooleanFilterProcessor {
|
|
1063
|
+
constructor(operator, search) {
|
|
1064
|
+
this.operator = operator;
|
|
1065
|
+
this.search = search === null ? null : typeof search === "boolean" ? search : parseBooleanSearch(search);
|
|
1066
|
+
}
|
|
1067
|
+
search;
|
|
1068
|
+
// ...........................................................................
|
|
1069
|
+
static fromModel(model) {
|
|
1070
|
+
return new BooleanFilterProcessor(model.operator, model.search);
|
|
1071
|
+
}
|
|
1072
|
+
// ...........................................................................
|
|
1073
|
+
equals(other) {
|
|
1074
|
+
return other instanceof BooleanFilterProcessor && this.operator === other.operator && this.search === other.search;
|
|
1075
|
+
}
|
|
1076
|
+
// ...........................................................................
|
|
1077
|
+
static allOperators = ["equals", "notEquals"];
|
|
1078
|
+
// ...........................................................................
|
|
1079
|
+
matches(cellValue) {
|
|
1080
|
+
if (this.search === null || this.search === void 0) {
|
|
1081
|
+
return true;
|
|
1082
|
+
}
|
|
1083
|
+
if (cellValue === null || cellValue === void 0) {
|
|
1084
|
+
return false;
|
|
1085
|
+
}
|
|
1086
|
+
if (typeof cellValue == "number") {
|
|
1087
|
+
cellValue = cellValue != 0;
|
|
1088
|
+
}
|
|
1089
|
+
if (typeof cellValue == "string") {
|
|
1090
|
+
const val = cellValue.toLowerCase();
|
|
1091
|
+
cellValue = val === "true" || val === "yes" || "1";
|
|
1092
|
+
}
|
|
1093
|
+
switch (this.operator) {
|
|
1094
|
+
case "equals":
|
|
1095
|
+
return cellValue === this.search;
|
|
1096
|
+
case "notEquals":
|
|
1097
|
+
return cellValue !== this.search;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
// ...........................................................................
|
|
1101
|
+
static get example() {
|
|
1102
|
+
const model = exampleBooleanFilter();
|
|
1103
|
+
const filterProcessor = BooleanFilterProcessor.fromModel(model);
|
|
1104
|
+
return filterProcessor;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
const exampleNumberFilter = () => hip({
|
|
1108
|
+
column: "basicTypes/numbersRef/intsRef/value",
|
|
1109
|
+
operator: "greaterThanOrEquals",
|
|
1110
|
+
type: "number",
|
|
1111
|
+
search: 1e3,
|
|
1112
|
+
_hash: ""
|
|
1113
|
+
});
|
|
1114
|
+
class NumberFilterProcessor {
|
|
1115
|
+
constructor(operator, search) {
|
|
1116
|
+
this.operator = operator;
|
|
1117
|
+
if (operator === "filtrex") {
|
|
1118
|
+
this.search = search;
|
|
1119
|
+
this._initFiltrex();
|
|
1120
|
+
} else {
|
|
1121
|
+
this.search = typeof search == "string" ? parseFloat(search) : search;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
// ...........................................................................
|
|
1125
|
+
static fromModel(model) {
|
|
1126
|
+
return new NumberFilterProcessor(model.operator, model.search);
|
|
1127
|
+
}
|
|
1128
|
+
equals(other) {
|
|
1129
|
+
return other instanceof NumberFilterProcessor && this.operator === other.operator && this.search === other.search;
|
|
1130
|
+
}
|
|
1131
|
+
// ...........................................................................
|
|
1132
|
+
static allOperators = [
|
|
1133
|
+
"equals",
|
|
1134
|
+
"notEquals",
|
|
1135
|
+
"greaterThan",
|
|
1136
|
+
"greaterThanOrEquals",
|
|
1137
|
+
"lessThan",
|
|
1138
|
+
"lessThanOrEquals",
|
|
1139
|
+
"filtrex"
|
|
1140
|
+
];
|
|
1141
|
+
// ...........................................................................
|
|
1142
|
+
matches(cellValue) {
|
|
1143
|
+
if (!this.search && this.search !== 0) {
|
|
1144
|
+
return true;
|
|
1145
|
+
}
|
|
1146
|
+
if (cellValue === null || cellValue === void 0) {
|
|
1147
|
+
return false;
|
|
1148
|
+
}
|
|
1149
|
+
switch (this.operator) {
|
|
1150
|
+
case "equals":
|
|
1151
|
+
return cellValue === this.search;
|
|
1152
|
+
case "notEquals":
|
|
1153
|
+
return cellValue !== this.search;
|
|
1154
|
+
case "greaterThan":
|
|
1155
|
+
return cellValue > this.search;
|
|
1156
|
+
case "lessThan":
|
|
1157
|
+
return cellValue < this.search;
|
|
1158
|
+
case "greaterThanOrEquals":
|
|
1159
|
+
return cellValue >= this.search;
|
|
1160
|
+
case "lessThanOrEquals":
|
|
1161
|
+
return cellValue <= this.search;
|
|
1162
|
+
case "filtrex":
|
|
1163
|
+
return this?._evalExpression(cellValue);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
search = "";
|
|
1167
|
+
static get example() {
|
|
1168
|
+
return NumberFilterProcessor.fromModel(exampleNumberFilter());
|
|
1169
|
+
}
|
|
1170
|
+
// ######################
|
|
1171
|
+
// Private
|
|
1172
|
+
// ######################
|
|
1173
|
+
_expression = null;
|
|
1174
|
+
// ..........................................................................
|
|
1175
|
+
_initFiltrex() {
|
|
1176
|
+
if (this.search === "") {
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
if (typeof this.search === "number") {
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
const isNumber = /^\d+$/.test(this.search);
|
|
1183
|
+
if (isNumber) {
|
|
1184
|
+
this.search = parseInt(this.search);
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
try {
|
|
1188
|
+
this._expression = compileExpression(this.search);
|
|
1189
|
+
} catch (_) {
|
|
1190
|
+
this._expression = "";
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
// ...........................................................................
|
|
1194
|
+
_evalExpression(cellValue) {
|
|
1195
|
+
return this._expression ? (
|
|
1196
|
+
// result can also contain errors which could be sent to the outside here
|
|
1197
|
+
this._expression({ v: cellValue }) == true
|
|
1198
|
+
) : cellValue == this.search;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
const exampleStringFilter = () => hip({
|
|
1202
|
+
type: "string",
|
|
1203
|
+
column: "basicTypes/stringsRef/value",
|
|
1204
|
+
operator: "startsWith",
|
|
1205
|
+
search: "t",
|
|
1206
|
+
matchCase: false,
|
|
1207
|
+
_hash: ""
|
|
1208
|
+
});
|
|
1209
|
+
class StringFilterProcessor {
|
|
1210
|
+
constructor(operator, search, matchCase = false) {
|
|
1211
|
+
this.operator = operator;
|
|
1212
|
+
this.matchCase = matchCase;
|
|
1213
|
+
this.search = (matchCase ? search : search.toLowerCase()).replaceAll(
|
|
1214
|
+
" ",
|
|
1215
|
+
""
|
|
1216
|
+
);
|
|
1217
|
+
try {
|
|
1218
|
+
this.regExp = operator === "regExp" ? new RegExp(search) : null;
|
|
1219
|
+
} catch {
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
// ...........................................................................
|
|
1223
|
+
static fromModel(model) {
|
|
1224
|
+
return new StringFilterProcessor(
|
|
1225
|
+
model.operator,
|
|
1226
|
+
model.search,
|
|
1227
|
+
model.matchCase
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
// ...........................................................................
|
|
1231
|
+
search;
|
|
1232
|
+
regExp = null;
|
|
1233
|
+
// ...........................................................................
|
|
1234
|
+
equals(other) {
|
|
1235
|
+
return other instanceof StringFilterProcessor && this.operator === other.operator && this.search === other.search && this.matchCase === other.matchCase;
|
|
1236
|
+
}
|
|
1237
|
+
// ...........................................................................
|
|
1238
|
+
static allOperators = [
|
|
1239
|
+
"contains",
|
|
1240
|
+
"equals",
|
|
1241
|
+
"notEquals",
|
|
1242
|
+
"startsWith",
|
|
1243
|
+
"notContains",
|
|
1244
|
+
"endsWith",
|
|
1245
|
+
"regExp"
|
|
1246
|
+
];
|
|
1247
|
+
// ...........................................................................
|
|
1248
|
+
matches(cellValue) {
|
|
1249
|
+
if (!this.search) {
|
|
1250
|
+
return true;
|
|
1251
|
+
}
|
|
1252
|
+
if (cellValue === null || cellValue === void 0) {
|
|
1253
|
+
return false;
|
|
1254
|
+
}
|
|
1255
|
+
if (typeof cellValue !== "string") {
|
|
1256
|
+
cellValue = `${cellValue}`;
|
|
1257
|
+
}
|
|
1258
|
+
if (!this.matchCase) {
|
|
1259
|
+
cellValue = cellValue.toLowerCase();
|
|
1260
|
+
}
|
|
1261
|
+
cellValue = cellValue.replaceAll(" ", "");
|
|
1262
|
+
switch (this.operator) {
|
|
1263
|
+
case "equals":
|
|
1264
|
+
return cellValue === this.search;
|
|
1265
|
+
case "notEquals":
|
|
1266
|
+
return cellValue !== this.search;
|
|
1267
|
+
case "startsWith":
|
|
1268
|
+
return cellValue.startsWith(this.search);
|
|
1269
|
+
case "contains":
|
|
1270
|
+
return cellValue.includes(this.search);
|
|
1271
|
+
case "endsWith":
|
|
1272
|
+
return cellValue.endsWith(this.search);
|
|
1273
|
+
/* v8 ignore next -- @preserve */
|
|
1274
|
+
case "regExp":
|
|
1275
|
+
return this.regExp?.test(cellValue) ?? false;
|
|
1276
|
+
case "notContains":
|
|
1277
|
+
return !cellValue.includes(this.search);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
// ...........................................................................
|
|
1281
|
+
static get example() {
|
|
1282
|
+
const result = StringFilterProcessor.fromModel(exampleStringFilter());
|
|
1283
|
+
return result;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
class ColumnFilterProcessor {
|
|
1287
|
+
/* v8 ignore next -- @preserve */
|
|
1288
|
+
matches(_cellValue) {
|
|
1289
|
+
return true;
|
|
1290
|
+
}
|
|
1291
|
+
equals(_other) {
|
|
1292
|
+
return this === _other;
|
|
1293
|
+
}
|
|
1294
|
+
// ...........................................................................
|
|
1295
|
+
static fromModel(model) {
|
|
1296
|
+
switch (model.type) {
|
|
1297
|
+
case "string":
|
|
1298
|
+
return StringFilterProcessor.fromModel(model);
|
|
1299
|
+
case "number":
|
|
1300
|
+
return NumberFilterProcessor.fromModel(model);
|
|
1301
|
+
case "boolean":
|
|
1302
|
+
return BooleanFilterProcessor.fromModel(model);
|
|
1303
|
+
/* v8 ignore next -- @preserve */
|
|
1304
|
+
default:
|
|
1305
|
+
return StringFilterProcessor.fromModel(model);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
// ...........................................................................
|
|
1309
|
+
/* v8 ignore stop */
|
|
1310
|
+
static operatorsForType(type) {
|
|
1311
|
+
switch (type) {
|
|
1312
|
+
case "string":
|
|
1313
|
+
return StringFilterProcessor.allOperators;
|
|
1314
|
+
case "number":
|
|
1315
|
+
return NumberFilterProcessor.allOperators;
|
|
1316
|
+
case "boolean":
|
|
1317
|
+
return BooleanFilterProcessor.allOperators;
|
|
1318
|
+
default:
|
|
1319
|
+
return StringFilterProcessor.allOperators;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
static translationsForType(type, language) {
|
|
1323
|
+
const operators = ColumnFilterProcessor.operatorsForType(type);
|
|
1324
|
+
const translations = [];
|
|
1325
|
+
for (const operator of operators) {
|
|
1326
|
+
translations.push(
|
|
1327
|
+
ColumnFilterProcessor.translateOperator(operator, language)
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1330
|
+
return translations;
|
|
1331
|
+
}
|
|
1332
|
+
static translateOperator(operator, language) {
|
|
1333
|
+
const translations = {
|
|
1334
|
+
equals: { en: "Equals", de: "Gleich" },
|
|
1335
|
+
notEquals: { en: "Not equals", de: "Ungleich" },
|
|
1336
|
+
greaterThan: { en: "Greater than", de: "Größer" },
|
|
1337
|
+
greaterThanOrEquals: {
|
|
1338
|
+
en: "Greater than or Equals",
|
|
1339
|
+
de: "Größer oder gleich"
|
|
1340
|
+
},
|
|
1341
|
+
lessThan: { en: "Less than", de: "Kleiner als" },
|
|
1342
|
+
lessThanOrEquals: {
|
|
1343
|
+
en: "Less than or equals",
|
|
1344
|
+
de: "Kleiner gleich"
|
|
1345
|
+
},
|
|
1346
|
+
startsWith: { en: "Starts with", de: "Beginnt mit" },
|
|
1347
|
+
contains: { en: "Contains", de: "Enthält" },
|
|
1348
|
+
notContains: { en: "Not contains", de: "Enthält nicht" },
|
|
1349
|
+
endsWith: { en: "Ends with", de: "Endet mit" },
|
|
1350
|
+
regExp: { en: "Regular expression", de: "Regulärer Ausdruck" },
|
|
1351
|
+
filtrex: { en: "Expression", de: "Ausdruck" }
|
|
1352
|
+
};
|
|
1353
|
+
return translations[operator][language];
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
class RowFilterProcessor {
|
|
1357
|
+
// ...........................................................................
|
|
1358
|
+
constructor(columnFilters, operator = "and") {
|
|
1359
|
+
this.operator = operator;
|
|
1360
|
+
this._columnFilters = this._initColumnFilters(columnFilters);
|
|
1361
|
+
}
|
|
1362
|
+
// ...........................................................................
|
|
1363
|
+
static fromModel(model) {
|
|
1364
|
+
const operator = model.operator;
|
|
1365
|
+
const columnFilters = {};
|
|
1366
|
+
for (const columnFilter of model.columnFilters) {
|
|
1367
|
+
const key = columnFilter.column;
|
|
1368
|
+
const processor = ColumnFilterProcessor.fromModel(columnFilter);
|
|
1369
|
+
columnFilters[key] = processor;
|
|
1370
|
+
}
|
|
1371
|
+
return new RowFilterProcessor(columnFilters, operator);
|
|
1372
|
+
}
|
|
1373
|
+
// ...........................................................................
|
|
1374
|
+
get processors() {
|
|
1375
|
+
return Object.values(this._columnFilters).map((item) => item.processor);
|
|
1376
|
+
}
|
|
1377
|
+
// ...........................................................................
|
|
1378
|
+
/// Returns an empty filter
|
|
1379
|
+
static get empty() {
|
|
1380
|
+
return new RowFilterProcessor({}, "and");
|
|
1381
|
+
}
|
|
1382
|
+
// ...........................................................................
|
|
1383
|
+
/// Checks if two filters are equal
|
|
1384
|
+
equals(other) {
|
|
1385
|
+
if (this.operator !== other.operator) {
|
|
1386
|
+
return false;
|
|
1387
|
+
}
|
|
1388
|
+
const thisKeys = Object.keys(this._columnFilters);
|
|
1389
|
+
const otherKeys = Object.keys(other._columnFilters);
|
|
1390
|
+
if (thisKeys.length !== otherKeys.length) {
|
|
1391
|
+
return false;
|
|
1392
|
+
}
|
|
1393
|
+
for (const key of thisKeys) {
|
|
1394
|
+
const a = this._columnFilters[key];
|
|
1395
|
+
const b = other._columnFilters[key];
|
|
1396
|
+
if (a?.processor.equals(b?.processor) === false) {
|
|
1397
|
+
return false;
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
return true;
|
|
1401
|
+
}
|
|
1402
|
+
// ...........................................................................
|
|
1403
|
+
applyTo(join) {
|
|
1404
|
+
if (join.rowCount === 0) {
|
|
1405
|
+
return join.data;
|
|
1406
|
+
}
|
|
1407
|
+
this._throwOnWrongRoutes(join.columnSelection);
|
|
1408
|
+
const columnCount = join.columnCount;
|
|
1409
|
+
const columnHashes = join.columnSelection.routeHashes;
|
|
1410
|
+
const filterArray = new Array(columnCount).fill(
|
|
1411
|
+
null
|
|
1412
|
+
);
|
|
1413
|
+
let hasFilters = false;
|
|
1414
|
+
for (let c = 0; c < columnCount; c++) {
|
|
1415
|
+
const hash = columnHashes[c];
|
|
1416
|
+
const filter = this._columnFilters[hash];
|
|
1417
|
+
if (filter) {
|
|
1418
|
+
filterArray[c] = filter.processor;
|
|
1419
|
+
hasFilters = true;
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
if (!hasFilters) {
|
|
1423
|
+
return join.data;
|
|
1424
|
+
}
|
|
1425
|
+
let rowIndices = [];
|
|
1426
|
+
switch (this.operator) {
|
|
1427
|
+
case "and":
|
|
1428
|
+
rowIndices = this._filterRowsAnd(join, filterArray);
|
|
1429
|
+
break;
|
|
1430
|
+
case "or":
|
|
1431
|
+
rowIndices = this._filterRowsOr(join, filterArray);
|
|
1432
|
+
break;
|
|
1433
|
+
}
|
|
1434
|
+
const result = {};
|
|
1435
|
+
const rowIndexSet = new Set(rowIndices);
|
|
1436
|
+
let idx = 0;
|
|
1437
|
+
for (const [sliceId, row] of Object.entries(join.data)) {
|
|
1438
|
+
if (rowIndexSet.has(idx)) {
|
|
1439
|
+
result[sliceId] = row;
|
|
1440
|
+
}
|
|
1441
|
+
idx++;
|
|
1442
|
+
}
|
|
1443
|
+
return result;
|
|
1444
|
+
}
|
|
1445
|
+
// ######################
|
|
1446
|
+
// Private
|
|
1447
|
+
// ######################
|
|
1448
|
+
// ...........................................................................
|
|
1449
|
+
_columnFilters;
|
|
1450
|
+
// ...........................................................................
|
|
1451
|
+
_initColumnFilters(columnFilters) {
|
|
1452
|
+
const result = {};
|
|
1453
|
+
const columnKeys = Object.keys(columnFilters);
|
|
1454
|
+
const columnRoutes = columnKeys.map((k) => Route.fromFlat(k));
|
|
1455
|
+
const columnSelection = ColumnSelection.fromRoutes(columnRoutes);
|
|
1456
|
+
const { routeHashes, routes } = columnSelection;
|
|
1457
|
+
for (let i = 0; i < routeHashes.length; i++) {
|
|
1458
|
+
const routeHash = routeHashes[i];
|
|
1459
|
+
const route = routes[i];
|
|
1460
|
+
const processor = columnFilters[route];
|
|
1461
|
+
result[routeHash] = {
|
|
1462
|
+
processor,
|
|
1463
|
+
routeHash,
|
|
1464
|
+
route
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
return result;
|
|
1468
|
+
}
|
|
1469
|
+
// ...........................................................................
|
|
1470
|
+
_filterRowsAnd(join, filters) {
|
|
1471
|
+
const rowCount = join.rowCount;
|
|
1472
|
+
let remainingIndices = new Array(rowCount);
|
|
1473
|
+
for (let i = 0; i < rowCount; i++) {
|
|
1474
|
+
remainingIndices[i] = i;
|
|
1475
|
+
}
|
|
1476
|
+
const columnCount = join.columnCount;
|
|
1477
|
+
for (let c = 0; c < columnCount; c++) {
|
|
1478
|
+
remainingIndices = this._filterColumnAnd(
|
|
1479
|
+
join,
|
|
1480
|
+
c,
|
|
1481
|
+
remainingIndices,
|
|
1482
|
+
filters
|
|
1483
|
+
);
|
|
1484
|
+
}
|
|
1485
|
+
return remainingIndices;
|
|
1486
|
+
}
|
|
1487
|
+
// ...........................................................................
|
|
1488
|
+
_filterColumnAnd(join, columnIndex, remainingIndices, filters) {
|
|
1489
|
+
const result = [];
|
|
1490
|
+
const filter = filters[columnIndex];
|
|
1491
|
+
if (filter == null) {
|
|
1492
|
+
return remainingIndices;
|
|
1493
|
+
}
|
|
1494
|
+
for (const i of remainingIndices) {
|
|
1495
|
+
const cellValue = join.value(i, columnIndex);
|
|
1496
|
+
if (filter.matches(cellValue)) {
|
|
1497
|
+
result.push(i);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
return result;
|
|
1501
|
+
}
|
|
1502
|
+
// ...........................................................................
|
|
1503
|
+
_filterRowsOr(join, filters) {
|
|
1504
|
+
const applyTo = new Array(join.rowCount).fill(false);
|
|
1505
|
+
const columnCount = join.columnCount;
|
|
1506
|
+
for (let c = 0; c < columnCount; c++) {
|
|
1507
|
+
this._filterColumnOr(join, c, applyTo, filters);
|
|
1508
|
+
}
|
|
1509
|
+
let rowCount = 0;
|
|
1510
|
+
for (let r = 0; r < applyTo.length; r++) {
|
|
1511
|
+
if (applyTo[r]) {
|
|
1512
|
+
rowCount++;
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
const result = new Array(rowCount);
|
|
1516
|
+
let resultIndex = 0;
|
|
1517
|
+
for (let r = 0; r < applyTo.length; r++) {
|
|
1518
|
+
if (applyTo[r]) {
|
|
1519
|
+
result[resultIndex] = r;
|
|
1520
|
+
resultIndex++;
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
return result;
|
|
1524
|
+
}
|
|
1525
|
+
// ...........................................................................
|
|
1526
|
+
_filterColumnOr(join, columnIndex, applyTo, filters) {
|
|
1527
|
+
const filter = filters[columnIndex];
|
|
1528
|
+
if (filter == null) {
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
for (let r = 0; r < join.rowCount; r++) {
|
|
1532
|
+
if (applyTo[r]) {
|
|
1533
|
+
continue;
|
|
1534
|
+
}
|
|
1535
|
+
const cellValue = join.value(r, columnIndex);
|
|
1536
|
+
if (filter.matches(cellValue)) {
|
|
1537
|
+
applyTo[r] = true;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
// ...........................................................................
|
|
1542
|
+
_throwOnWrongRoutes(columnSelection) {
|
|
1543
|
+
const availableRoutes = columnSelection.routes;
|
|
1544
|
+
for (const item of Object.values(this._columnFilters)) {
|
|
1545
|
+
const route = item.route;
|
|
1546
|
+
if (availableRoutes.includes(route) === false) {
|
|
1547
|
+
throw new Error(
|
|
1548
|
+
`RowFilterProcessor: Error while applying filter to view: There is a column filter for route "${route}", but the view does not have a column with this route.
|
|
1549
|
+
|
|
1550
|
+
Available routes:
|
|
1551
|
+
${availableRoutes.map((a) => `- ${a}`).join("\n")}`
|
|
1552
|
+
);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
class Join {
|
|
1558
|
+
constructor(baseRows, _baseColumnSelection) {
|
|
1559
|
+
this._baseColumnSelection = _baseColumnSelection;
|
|
1560
|
+
this._base = this._hashedRows(baseRows);
|
|
1561
|
+
}
|
|
1562
|
+
_base = {};
|
|
1563
|
+
_processes = [];
|
|
1564
|
+
// ...........................................................................
|
|
1565
|
+
/**
|
|
1566
|
+
* Applies a filter to the join and returns the filtered view
|
|
1567
|
+
*
|
|
1568
|
+
* @param filter The filter to apply
|
|
1569
|
+
*/
|
|
1570
|
+
filter(filter) {
|
|
1571
|
+
const proc = RowFilterProcessor.fromModel(filter);
|
|
1572
|
+
const data = proc.applyTo(this);
|
|
1573
|
+
const process = {
|
|
1574
|
+
type: "filter",
|
|
1575
|
+
instance: filter,
|
|
1576
|
+
data,
|
|
1577
|
+
columnSelection: this.columnSelection
|
|
1578
|
+
};
|
|
1579
|
+
this._processes.push(process);
|
|
1580
|
+
return this;
|
|
1581
|
+
}
|
|
1582
|
+
// ...........................................................................
|
|
1583
|
+
/**
|
|
1584
|
+
* Applies a set value action to the join and returns the edited join
|
|
1585
|
+
*
|
|
1586
|
+
* @param setValue The set value action to apply
|
|
1587
|
+
*/
|
|
1588
|
+
setValue(setValue) {
|
|
1589
|
+
const data = {};
|
|
1590
|
+
for (const [sliceId, joinRowH] of Object.entries(this.data)) {
|
|
1591
|
+
const cols = [...joinRowH.columns];
|
|
1592
|
+
for (const col of cols) {
|
|
1593
|
+
if (Route.fromFlat(setValue.route).equalsWithoutRefs(col.route))
|
|
1594
|
+
col.insert = setValue.value;
|
|
1595
|
+
else continue;
|
|
1596
|
+
}
|
|
1597
|
+
data[sliceId] = {
|
|
1598
|
+
rowHash: Hash.default.calcHash(cols.map((c) => c.value)),
|
|
1599
|
+
columns: cols
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
const process = {
|
|
1603
|
+
type: "setValue",
|
|
1604
|
+
instance: setValue,
|
|
1605
|
+
data,
|
|
1606
|
+
columnSelection: this.columnSelection
|
|
1607
|
+
};
|
|
1608
|
+
this._processes.push(process);
|
|
1609
|
+
return this;
|
|
1610
|
+
}
|
|
1611
|
+
// ...........................................................................
|
|
1612
|
+
/**
|
|
1613
|
+
* Applies multiple set value actions to the join and returns the edited join
|
|
1614
|
+
*
|
|
1615
|
+
* @param setValues The set value actions to apply
|
|
1616
|
+
*/
|
|
1617
|
+
setValues(setValues) {
|
|
1618
|
+
let result = this.clone();
|
|
1619
|
+
for (const setValue of setValues) {
|
|
1620
|
+
result = result.setValue(setValue);
|
|
1621
|
+
}
|
|
1622
|
+
return result;
|
|
1623
|
+
}
|
|
1624
|
+
// ...........................................................................
|
|
1625
|
+
/**
|
|
1626
|
+
* Selects columns from the join and returns the resulting join
|
|
1627
|
+
*
|
|
1628
|
+
* @param columnSelection The column selection to apply
|
|
1629
|
+
*/
|
|
1630
|
+
select(columnSelection) {
|
|
1631
|
+
const columnCount = columnSelection.count;
|
|
1632
|
+
const masterColumnIndices = new Array(columnCount);
|
|
1633
|
+
let i = 0;
|
|
1634
|
+
for (const hash of columnSelection.routeHashes) {
|
|
1635
|
+
const index = this.columnSelection.columnIndex(hash);
|
|
1636
|
+
masterColumnIndices[i] = index;
|
|
1637
|
+
i++;
|
|
1638
|
+
}
|
|
1639
|
+
const data = {};
|
|
1640
|
+
for (let i2 = 0; i2 < this.rowCount; i2++) {
|
|
1641
|
+
const [sliceId, row] = Object.entries(this.data)[i2];
|
|
1642
|
+
const selectedColumns = [];
|
|
1643
|
+
for (let j = 0; j < masterColumnIndices.length; j++) {
|
|
1644
|
+
selectedColumns.push(row.columns[masterColumnIndices[j]]);
|
|
1645
|
+
}
|
|
1646
|
+
data[sliceId] = {
|
|
1647
|
+
rowHash: Hash.default.calcHash(
|
|
1648
|
+
selectedColumns.map((c) => c.value)
|
|
1649
|
+
),
|
|
1650
|
+
columns: selectedColumns
|
|
1651
|
+
};
|
|
1652
|
+
}
|
|
1653
|
+
const process = {
|
|
1654
|
+
type: "selection",
|
|
1655
|
+
instance: columnSelection,
|
|
1656
|
+
data,
|
|
1657
|
+
columnSelection
|
|
1658
|
+
};
|
|
1659
|
+
this._processes.push(process);
|
|
1660
|
+
return this;
|
|
1661
|
+
}
|
|
1662
|
+
// ...........................................................................
|
|
1663
|
+
/**
|
|
1664
|
+
* Sorts the join rows and returns the sorted join
|
|
1665
|
+
*
|
|
1666
|
+
* @param rowSort The row sort to apply
|
|
1667
|
+
*/
|
|
1668
|
+
sort(rowSort) {
|
|
1669
|
+
const sortedIndices = rowSort.applyTo(this);
|
|
1670
|
+
const data = {};
|
|
1671
|
+
for (let i = 0; i < sortedIndices.length; i++) {
|
|
1672
|
+
const sliceId = sortedIndices[i];
|
|
1673
|
+
data[sliceId] = this.data[sliceId];
|
|
1674
|
+
}
|
|
1675
|
+
const process = {
|
|
1676
|
+
type: "sort",
|
|
1677
|
+
instance: rowSort,
|
|
1678
|
+
data,
|
|
1679
|
+
columnSelection: this.columnSelection
|
|
1680
|
+
};
|
|
1681
|
+
this._processes.push(process);
|
|
1682
|
+
return this;
|
|
1683
|
+
}
|
|
1684
|
+
// ...........................................................................
|
|
1685
|
+
/**
|
|
1686
|
+
* Returns insert Object of the join
|
|
1687
|
+
*/
|
|
1688
|
+
insert() {
|
|
1689
|
+
const cakeInserts = this._insertCakeObject(this.data);
|
|
1690
|
+
const mergedCakeInsertObj = merge(...cakeInserts);
|
|
1691
|
+
const route = mergedCakeInsertObj[this.cakeRoute.root.tableKey].route.flat;
|
|
1692
|
+
const value = mergedCakeInsertObj[this.cakeRoute.root.tableKey].value;
|
|
1693
|
+
const insert = {
|
|
1694
|
+
command: "add",
|
|
1695
|
+
route,
|
|
1696
|
+
value
|
|
1697
|
+
};
|
|
1698
|
+
return insert;
|
|
1699
|
+
}
|
|
1700
|
+
// ...........................................................................
|
|
1701
|
+
/**
|
|
1702
|
+
* Returns the value at the given row and column index
|
|
1703
|
+
*
|
|
1704
|
+
* @param row The row index
|
|
1705
|
+
* @param column The column index
|
|
1706
|
+
* @returns The value at the given row and column
|
|
1707
|
+
*/
|
|
1708
|
+
value(row, column) {
|
|
1709
|
+
return this.rows[row][column];
|
|
1710
|
+
}
|
|
1711
|
+
// ...........................................................................
|
|
1712
|
+
/**
|
|
1713
|
+
* Clones the join
|
|
1714
|
+
*
|
|
1715
|
+
* @returns The cloned join
|
|
1716
|
+
*/
|
|
1717
|
+
clone() {
|
|
1718
|
+
const cloned = Object.create(this);
|
|
1719
|
+
cloned._data = this._base;
|
|
1720
|
+
cloned._processes = [...this._processes];
|
|
1721
|
+
return cloned;
|
|
1722
|
+
}
|
|
1723
|
+
// ...........................................................................
|
|
1724
|
+
/**
|
|
1725
|
+
* Returns all component routes of the join
|
|
1726
|
+
*/
|
|
1727
|
+
get componentRoutes() {
|
|
1728
|
+
return Array.from(
|
|
1729
|
+
new Set(
|
|
1730
|
+
Object.values(this.columnSelection.columns).map(
|
|
1731
|
+
(c) => Route.fromFlat(c.route).upper().flatWithoutRefs
|
|
1732
|
+
)
|
|
1733
|
+
)
|
|
1734
|
+
).map((r) => Route.fromFlat(r));
|
|
1735
|
+
}
|
|
1736
|
+
// ...........................................................................
|
|
1737
|
+
/**
|
|
1738
|
+
* Returns all layer routes of the join
|
|
1739
|
+
*/
|
|
1740
|
+
get layerRoutes() {
|
|
1741
|
+
return Array.from(
|
|
1742
|
+
new Set(this.componentRoutes.map((r) => r.upper().flat))
|
|
1743
|
+
).map((r) => Route.fromFlat(r));
|
|
1744
|
+
}
|
|
1745
|
+
// ...........................................................................
|
|
1746
|
+
/**
|
|
1747
|
+
* Returns the cake route of the join
|
|
1748
|
+
*/
|
|
1749
|
+
get cakeRoute() {
|
|
1750
|
+
const cakeRoute = Array.from(
|
|
1751
|
+
new Set(this.layerRoutes.map((r) => r.upper().flat))
|
|
1752
|
+
).map((r) => Route.fromFlat(r));
|
|
1753
|
+
if (cakeRoute.length !== 1) {
|
|
1754
|
+
throw new Error(
|
|
1755
|
+
`Join: Error while getting cake route: The join has ${cakeRoute.length} different cake routes. Cannot determine a single cake route.`
|
|
1756
|
+
);
|
|
1757
|
+
}
|
|
1758
|
+
return cakeRoute[0];
|
|
1759
|
+
}
|
|
1760
|
+
// ...........................................................................
|
|
1761
|
+
/**
|
|
1762
|
+
* Returns the number of rows in the join
|
|
1763
|
+
*/
|
|
1764
|
+
get rowCount() {
|
|
1765
|
+
return Object.keys(this.data).length;
|
|
1766
|
+
}
|
|
1767
|
+
// ...........................................................................
|
|
1768
|
+
/**
|
|
1769
|
+
* Returns the number of columns in the join
|
|
1770
|
+
*/
|
|
1771
|
+
get columnCount() {
|
|
1772
|
+
return this.columnSelection.count;
|
|
1773
|
+
}
|
|
1774
|
+
// ...........................................................................
|
|
1775
|
+
/**
|
|
1776
|
+
* Returns the row indices (sliceIds) of the join
|
|
1777
|
+
*/
|
|
1778
|
+
get rowIndices() {
|
|
1779
|
+
return Object.keys(this.data);
|
|
1780
|
+
}
|
|
1781
|
+
// ...........................................................................
|
|
1782
|
+
/**
|
|
1783
|
+
* Returns the join row for the given slice id
|
|
1784
|
+
*
|
|
1785
|
+
* @param sliceId - The slice id
|
|
1786
|
+
* @returns The join row
|
|
1787
|
+
*/
|
|
1788
|
+
row(sliceId) {
|
|
1789
|
+
return this.data[sliceId].columns;
|
|
1790
|
+
}
|
|
1791
|
+
// ...........................................................................
|
|
1792
|
+
/**
|
|
1793
|
+
* Returns the data of the join
|
|
1794
|
+
*/
|
|
1795
|
+
get data() {
|
|
1796
|
+
if (this._processes.length > 0) {
|
|
1797
|
+
return this._processes[this._processes.length - 1].data;
|
|
1798
|
+
}
|
|
1799
|
+
return this._base;
|
|
1800
|
+
}
|
|
1801
|
+
// ...........................................................................
|
|
1802
|
+
/**
|
|
1803
|
+
* Returns the column types of the join
|
|
1804
|
+
*/
|
|
1805
|
+
get columnTypes() {
|
|
1806
|
+
return this.columnSelection.columns.map((col) => col.type);
|
|
1807
|
+
}
|
|
1808
|
+
// ...........................................................................
|
|
1809
|
+
/**
|
|
1810
|
+
* Returns the column selection of the join
|
|
1811
|
+
*/
|
|
1812
|
+
get columnSelection() {
|
|
1813
|
+
if (this._processes.length > 0) {
|
|
1814
|
+
return this._processes[this._processes.length - 1].columnSelection;
|
|
1815
|
+
}
|
|
1816
|
+
return this._baseColumnSelection;
|
|
1817
|
+
}
|
|
1818
|
+
// ...........................................................................
|
|
1819
|
+
/**
|
|
1820
|
+
* Returns all rows of the join w/ nulled missing values
|
|
1821
|
+
*
|
|
1822
|
+
* @return The rows of the join
|
|
1823
|
+
*/
|
|
1824
|
+
get rows() {
|
|
1825
|
+
const result = [];
|
|
1826
|
+
const sliceIds = Object.keys(this.data);
|
|
1827
|
+
for (const sliceId of sliceIds) {
|
|
1828
|
+
const dataColumns = this.data[sliceId].columns;
|
|
1829
|
+
const row = [];
|
|
1830
|
+
for (const colInfo of this.columnSelection.columns) {
|
|
1831
|
+
const joinCol = dataColumns.find((dataCol) => {
|
|
1832
|
+
const colInfoRoute = Route.fromFlat(colInfo.route);
|
|
1833
|
+
const dataColRoute = dataCol.route;
|
|
1834
|
+
return colInfoRoute.equalsWithoutRefs(dataColRoute);
|
|
1835
|
+
});
|
|
1836
|
+
row.push(joinCol ? joinCol.insert ?? joinCol.value : null);
|
|
1837
|
+
}
|
|
1838
|
+
result.push(row);
|
|
1839
|
+
}
|
|
1840
|
+
return result;
|
|
1841
|
+
}
|
|
1842
|
+
static empty() {
|
|
1843
|
+
return new Join({}, ColumnSelection.empty());
|
|
1844
|
+
}
|
|
1845
|
+
//#############################################################
|
|
1846
|
+
// ############# Private Methods ##############################
|
|
1847
|
+
//#############################################################
|
|
1848
|
+
// ...........................................................................
|
|
1849
|
+
/**
|
|
1850
|
+
* Builds cake insert objects from the given join rows
|
|
1851
|
+
* @param rows - The join rows
|
|
1852
|
+
* @returns The cake insert objects
|
|
1853
|
+
*/
|
|
1854
|
+
_insertCakeObject(rows) {
|
|
1855
|
+
const cakeInsertObjects = [];
|
|
1856
|
+
const cakeRoute = this.cakeRoute;
|
|
1857
|
+
for (const [sliceId, row] of Object.entries(rows)) {
|
|
1858
|
+
const layerInsertObjectList = this._insertLayerObjects(
|
|
1859
|
+
sliceId,
|
|
1860
|
+
row.columns
|
|
1861
|
+
);
|
|
1862
|
+
for (const layerInsertObject of layerInsertObjectList) {
|
|
1863
|
+
const cakeInsertObject = {};
|
|
1864
|
+
for (const [layerRoute, layerInsertObj] of Object.entries(
|
|
1865
|
+
layerInsertObject
|
|
1866
|
+
)) {
|
|
1867
|
+
cakeInsertObject[cakeRoute.root.tableKey] = {
|
|
1868
|
+
route: layerInsertObj.route,
|
|
1869
|
+
value: {
|
|
1870
|
+
[layerRoute]: layerInsertObj.value
|
|
1871
|
+
}
|
|
1872
|
+
};
|
|
1873
|
+
}
|
|
1874
|
+
cakeInsertObjects.push(cakeInsertObject);
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
return cakeInsertObjects;
|
|
1878
|
+
}
|
|
1879
|
+
// ...........................................................................
|
|
1880
|
+
/**
|
|
1881
|
+
* Wraps component insert objects into layer insert objects
|
|
1882
|
+
* @param sliceId - The slice id
|
|
1883
|
+
* @param componentInsertObjects - The component insert objects
|
|
1884
|
+
* @returns
|
|
1885
|
+
*/
|
|
1886
|
+
_insertLayerObjects(sliceId, insertRow) {
|
|
1887
|
+
const layerRoutes = this.layerRoutes;
|
|
1888
|
+
const layerInsertObjects = [];
|
|
1889
|
+
const insertComponentObjects = this._insertComponentObjects(
|
|
1890
|
+
sliceId,
|
|
1891
|
+
insertRow
|
|
1892
|
+
);
|
|
1893
|
+
for (const layerRoute of layerRoutes) {
|
|
1894
|
+
for (const [compRouteFlat, compInsertObj] of Object.entries(
|
|
1895
|
+
insertComponentObjects
|
|
1896
|
+
)) {
|
|
1897
|
+
const compRoute = Route.fromFlat(compRouteFlat);
|
|
1898
|
+
if (layerRoute.includes(compRoute)) {
|
|
1899
|
+
const layerInsertObj = {};
|
|
1900
|
+
layerInsertObj[layerRoute.root.tableKey] = {
|
|
1901
|
+
route: compInsertObj.route,
|
|
1902
|
+
value: {
|
|
1903
|
+
[sliceId]: compInsertObj.value
|
|
1904
|
+
}
|
|
1905
|
+
};
|
|
1906
|
+
layerInsertObjects.push(layerInsertObj);
|
|
1907
|
+
} else {
|
|
1908
|
+
continue;
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
return layerInsertObjects;
|
|
1913
|
+
}
|
|
1914
|
+
// ...........................................................................
|
|
1915
|
+
/**
|
|
1916
|
+
* Merges columns into component insert objects
|
|
1917
|
+
* @param insertColumns - The columns to merge
|
|
1918
|
+
* @returns
|
|
1919
|
+
*/
|
|
1920
|
+
_insertComponentObjects(sliceId, insertColumns) {
|
|
1921
|
+
const componentRoutes = this.componentRoutes;
|
|
1922
|
+
const columns = this._mergeInsertRow(sliceId, insertColumns);
|
|
1923
|
+
const result = {};
|
|
1924
|
+
for (const compRoute of componentRoutes) {
|
|
1925
|
+
let compChanged = false;
|
|
1926
|
+
const compInsert = {};
|
|
1927
|
+
for (const c of columns) {
|
|
1928
|
+
if (compRoute.includes(c.route)) {
|
|
1929
|
+
if (c.insert !== null) {
|
|
1930
|
+
compChanged = true;
|
|
1931
|
+
compInsert[c.route.propertyKey] = c.insert;
|
|
1932
|
+
} else {
|
|
1933
|
+
compInsert[c.route.propertyKey] = c.value;
|
|
1934
|
+
}
|
|
1935
|
+
} else {
|
|
1936
|
+
continue;
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
result[compRoute.flat] = {
|
|
1940
|
+
route: compRoute,
|
|
1941
|
+
value: compChanged ? compInsert : null
|
|
1942
|
+
};
|
|
1943
|
+
}
|
|
1944
|
+
return result;
|
|
1945
|
+
}
|
|
1946
|
+
// ...........................................................................
|
|
1947
|
+
/**
|
|
1948
|
+
* Merges the insert values into the base row
|
|
1949
|
+
* @param sliceId - The slice id
|
|
1950
|
+
* @param insertRow - The insert row
|
|
1951
|
+
* @returns The merged join row
|
|
1952
|
+
*/
|
|
1953
|
+
_mergeInsertRow(sliceId, insertRow) {
|
|
1954
|
+
const baseColumns = this._base[sliceId].columns;
|
|
1955
|
+
const mergedRow = [];
|
|
1956
|
+
for (const baseCol of baseColumns) {
|
|
1957
|
+
const insertCol = insertRow.find(
|
|
1958
|
+
(col) => col.route.equalsWithoutRefs(baseCol.route)
|
|
1959
|
+
);
|
|
1960
|
+
if (insertCol) {
|
|
1961
|
+
mergedRow.push({
|
|
1962
|
+
route: baseCol.route,
|
|
1963
|
+
value: baseCol.value,
|
|
1964
|
+
insert: insertCol.insert
|
|
1965
|
+
});
|
|
1966
|
+
} else {
|
|
1967
|
+
mergedRow.push(baseCol);
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
return mergedRow;
|
|
1971
|
+
}
|
|
1972
|
+
// ...........................................................................
|
|
1973
|
+
/**
|
|
1974
|
+
* Hashes the given join rows. If insert value is present, it is used for hashing.
|
|
1975
|
+
*
|
|
1976
|
+
* @param rows The join rows to hash
|
|
1977
|
+
* @returns The hashed join rows
|
|
602
1978
|
*/
|
|
603
|
-
|
|
1979
|
+
_hashedRows(rows) {
|
|
1980
|
+
const sliceIds = Object.keys(rows);
|
|
1981
|
+
const hashedRows = {};
|
|
1982
|
+
for (const sliceId of sliceIds) {
|
|
1983
|
+
const columns = rows[sliceId];
|
|
1984
|
+
const rowHash = Hash.default.calcHash(
|
|
1985
|
+
columns.map((col) => col.insert ?? col.value)
|
|
1986
|
+
);
|
|
1987
|
+
hashedRows[sliceId] = {
|
|
1988
|
+
rowHash,
|
|
1989
|
+
columns
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1992
|
+
return hashedRows;
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
class Notify {
|
|
1996
|
+
_callbacks = /* @__PURE__ */ new Map();
|
|
1997
|
+
// ...........................................................................
|
|
1998
|
+
constructor() {
|
|
1999
|
+
}
|
|
2000
|
+
// ...........................................................................
|
|
2001
|
+
/**
|
|
2002
|
+
* Registers a callback for a specific route.
|
|
2003
|
+
* @param route The route to register the callback for.
|
|
2004
|
+
* @param callback The callback function to be invoked when a notification is sent.
|
|
2005
|
+
*/
|
|
2006
|
+
register(route, callback) {
|
|
2007
|
+
this._callbacks.set(route.flat, [
|
|
2008
|
+
...this._callbacks.get(route.flat) || [],
|
|
2009
|
+
callback
|
|
2010
|
+
]);
|
|
2011
|
+
}
|
|
2012
|
+
// ...........................................................................
|
|
2013
|
+
/**
|
|
2014
|
+
* Unregisters a callback for a specific route.
|
|
2015
|
+
* @param route The route to unregister the callback from.
|
|
2016
|
+
* @param callback The callback function to be removed.
|
|
2017
|
+
*/
|
|
2018
|
+
unregister(route, callback) {
|
|
2019
|
+
const callbacks = this._callbacks.get(route.flat);
|
|
2020
|
+
if (callbacks) {
|
|
2021
|
+
this._callbacks.set(
|
|
2022
|
+
route.flat,
|
|
2023
|
+
callbacks.filter((cb) => cb !== callback)
|
|
2024
|
+
);
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
// ...........................................................................
|
|
2028
|
+
/**
|
|
2029
|
+
* Notifies all registered callbacks for a specific route with the provided edit protocol row.
|
|
2030
|
+
* @param route The route to notify callbacks for.
|
|
2031
|
+
* @param insertHistoryRow The edit protocol row to pass to the callbacks.
|
|
2032
|
+
*/
|
|
2033
|
+
notify(route, insertHistoryRow) {
|
|
2034
|
+
const callbacks = this._callbacks.get(route.flat);
|
|
2035
|
+
if (callbacks) {
|
|
2036
|
+
for (const cb of callbacks) {
|
|
2037
|
+
cb(insertHistoryRow);
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
// ...........................................................................
|
|
2042
|
+
/**
|
|
2043
|
+
* Returns the current map of registered callbacks.
|
|
2044
|
+
* @returns A map where keys are route strings and values are arrays of callback functions.
|
|
2045
|
+
*/
|
|
2046
|
+
get callbacks() {
|
|
2047
|
+
return this._callbacks;
|
|
2048
|
+
}
|
|
2049
|
+
// ...........................................................................
|
|
2050
|
+
/**
|
|
2051
|
+
* Retrieves the list of callbacks registered for a specific route.
|
|
2052
|
+
* @param route The route to get callbacks for.
|
|
2053
|
+
* @returns An array of callback functions registered for the specified route.
|
|
2054
|
+
*/
|
|
2055
|
+
getCallBacksForRoute(route) {
|
|
2056
|
+
return this._callbacks.get(route.flat) || [];
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
class Db {
|
|
2060
|
+
/**
|
|
2061
|
+
* Constructor
|
|
2062
|
+
* @param _io - The Io instance used to read and write data
|
|
2063
|
+
*/
|
|
2064
|
+
constructor(_io) {
|
|
2065
|
+
this._io = _io;
|
|
2066
|
+
this.core = new Core(this._io);
|
|
2067
|
+
this.notify = new Notify();
|
|
2068
|
+
}
|
|
2069
|
+
/**
|
|
2070
|
+
* Core functionalities like importing data, setting and getting tables
|
|
2071
|
+
*/
|
|
2072
|
+
core;
|
|
2073
|
+
/**
|
|
2074
|
+
* Notification system to register callbacks on data changes
|
|
2075
|
+
*/
|
|
2076
|
+
notify;
|
|
2077
|
+
_cache = /* @__PURE__ */ new Map();
|
|
2078
|
+
// ...........................................................................
|
|
2079
|
+
/**
|
|
2080
|
+
* Get data from a route with optional filtering
|
|
2081
|
+
* @param route - The route to get data from
|
|
2082
|
+
* @param where - Optional filter to apply to the data
|
|
2083
|
+
* @returns An array of Rljson objects matching the route and filter
|
|
2084
|
+
* @throws {Error} If the route is not valid or if any controller cannot be created
|
|
2085
|
+
*/
|
|
2086
|
+
async get(route, where) {
|
|
2087
|
+
if (!route.isValid) throw new Error(`Route ${route.flat} is not valid.`);
|
|
2088
|
+
const isolatedRoute = await this.isolatePropertyKeyFromRoute(route);
|
|
2089
|
+
const cacheHash = `${isolatedRoute.flat}|${JSON.stringify(where)}`;
|
|
2090
|
+
const isCached = this._cache.has(cacheHash);
|
|
2091
|
+
if (isCached) {
|
|
2092
|
+
return this._cache.get(cacheHash);
|
|
2093
|
+
} else {
|
|
2094
|
+
const controllers = await this._indexedControllers(isolatedRoute);
|
|
2095
|
+
const data = await this._get(isolatedRoute, where, controllers);
|
|
2096
|
+
this._cache.set(cacheHash, data);
|
|
2097
|
+
return data;
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
async _get(route, where, controllers, segmentLevel) {
|
|
2101
|
+
if (segmentLevel === void 0) segmentLevel = 0;
|
|
2102
|
+
const segment = route.segments[segmentLevel];
|
|
2103
|
+
const segmentIsDeepest = segmentLevel === route.segments.length - 1;
|
|
2104
|
+
const segmentController = controllers[segment.tableKey];
|
|
2105
|
+
const segmentRef = await this._getReferenceOfRouteSegment(segment);
|
|
2106
|
+
let segmentWhere = typeof where === "object" ? where : { _hash: where };
|
|
2107
|
+
segmentWhere = segment.tableKey in segmentWhere ? segmentWhere[segment.tableKey] : segmentWhere;
|
|
2108
|
+
segmentWhere = segmentRef ? { ...segmentWhere, ...{ _hash: segmentRef } } : segmentWhere;
|
|
2109
|
+
const childSegmentLevel = segmentLevel + 1;
|
|
2110
|
+
const childSegment = route.segments[childSegmentLevel];
|
|
2111
|
+
const segmentWhereWithoutChildWhere = { ...segmentWhere };
|
|
2112
|
+
if (!segmentIsDeepest && childSegment.tableKey in segmentWhere)
|
|
2113
|
+
delete segmentWhereWithoutChildWhere[childSegment.tableKey];
|
|
2114
|
+
let parent = await segmentController.get(segmentWhereWithoutChildWhere, {});
|
|
2115
|
+
if (segmentIsDeepest && route.hasPropertyKey) {
|
|
2116
|
+
parent = this.isolatePropertyFromComponents(parent, route.propertyKey);
|
|
2117
|
+
}
|
|
2118
|
+
const children = [];
|
|
2119
|
+
const filteredParentRows = /* @__PURE__ */ new Map();
|
|
2120
|
+
if (!segmentIsDeepest) {
|
|
2121
|
+
const childRefs = await segmentController.getChildRefs(
|
|
2122
|
+
segmentWhereWithoutChildWhere,
|
|
2123
|
+
{}
|
|
2124
|
+
);
|
|
2125
|
+
for (const { tableKey, columnKey, ref } of childRefs) {
|
|
2126
|
+
if (tableKey !== childSegment.tableKey) continue;
|
|
2127
|
+
const child = await this._get(
|
|
2128
|
+
route,
|
|
2129
|
+
{ ...segmentWhere, ...{ _hash: ref } },
|
|
2130
|
+
controllers,
|
|
2131
|
+
childSegmentLevel
|
|
2132
|
+
);
|
|
2133
|
+
children.push(child);
|
|
2134
|
+
for (const childObjs of child[tableKey]._data) {
|
|
2135
|
+
const childRef = childObjs["_hash"];
|
|
2136
|
+
for (const row of parent[segment.tableKey]._data) {
|
|
2137
|
+
if (filteredParentRows.has(row["_hash"]))
|
|
2138
|
+
continue;
|
|
2139
|
+
const includesChild = segmentController.filterRow(
|
|
2140
|
+
row,
|
|
2141
|
+
columnKey ?? tableKey,
|
|
2142
|
+
childRef
|
|
2143
|
+
);
|
|
2144
|
+
if (includesChild) {
|
|
2145
|
+
filteredParentRows.set(
|
|
2146
|
+
row["_hash"],
|
|
2147
|
+
row
|
|
2148
|
+
);
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
const parentWithFilteredRows = {
|
|
2154
|
+
[segment.tableKey]: {
|
|
2155
|
+
...parent[segment.tableKey],
|
|
2156
|
+
...{
|
|
2157
|
+
_data: Array.from(filteredParentRows.values())
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
};
|
|
2161
|
+
return merge(parentWithFilteredRows, ...children);
|
|
2162
|
+
}
|
|
2163
|
+
return parent;
|
|
2164
|
+
}
|
|
2165
|
+
// ...........................................................................
|
|
2166
|
+
/**
|
|
2167
|
+
* Get the reference (hash) of a route segment, considering default refs and insertHistory refs
|
|
2168
|
+
* @param segment - The route segment to get the reference for
|
|
2169
|
+
* @returns
|
|
2170
|
+
*/
|
|
2171
|
+
async _getReferenceOfRouteSegment(segment) {
|
|
2172
|
+
if (Route.segmentHasRef(segment)) {
|
|
2173
|
+
if (Route.segmentHasDefaultRef(segment)) {
|
|
2174
|
+
return Route.segmentRef(segment);
|
|
2175
|
+
} else {
|
|
2176
|
+
return await this.getRefOfTimeId(
|
|
2177
|
+
segment.tableKey,
|
|
2178
|
+
Route.segmentRef(segment)
|
|
2179
|
+
);
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
return null;
|
|
2183
|
+
}
|
|
2184
|
+
// ...........................................................................
|
|
2185
|
+
/**
|
|
2186
|
+
* Joins data from layers in an Rljson into a single dataset
|
|
2187
|
+
* @param rljson - The Rljson to join data for
|
|
2188
|
+
*/
|
|
2189
|
+
async join(columnSelection, cakeKey, cakeRef) {
|
|
2190
|
+
const data = await this._getBaseDataForColumnSelection(columnSelection);
|
|
2191
|
+
const cakesTable = data[cakeKey];
|
|
2192
|
+
const cake = cakesTable._data.find((c) => c._hash === cakeRef);
|
|
2193
|
+
if (!cake) {
|
|
2194
|
+
throw new Error(
|
|
2195
|
+
`Db.join: Cake with ref "${cakeRef}" not found in cake table "${cakeKey}".`
|
|
2196
|
+
);
|
|
2197
|
+
}
|
|
2198
|
+
const layers = /* @__PURE__ */ new Map();
|
|
2199
|
+
for (const layerKey of Object.keys(cake.layers)) {
|
|
2200
|
+
if (!data[layerKey]) continue;
|
|
2201
|
+
const layersTable = data[layerKey];
|
|
2202
|
+
const layer = layersTable._data.find(
|
|
2203
|
+
(l) => l._hash === cake.layers[layerKey]
|
|
2204
|
+
);
|
|
2205
|
+
layers.set(layerKey, layer);
|
|
2206
|
+
}
|
|
2207
|
+
const mergedSliceIds = /* @__PURE__ */ new Set();
|
|
2208
|
+
for (const layer of layers.values()) {
|
|
2209
|
+
const sliceIdsTable = layer.sliceIdsTable;
|
|
2210
|
+
const sliceIdsTableRow = layer.sliceIdsTableRow;
|
|
2211
|
+
const {
|
|
2212
|
+
[sliceIdsTable]: { _data: sliceIds }
|
|
2213
|
+
} = await this.core.readRows(sliceIdsTable, { _hash: sliceIdsTableRow });
|
|
2214
|
+
for (const sid of sliceIds) {
|
|
2215
|
+
for (const s of sid.add) {
|
|
2216
|
+
mergedSliceIds.add(s);
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
const columnCfgs = /* @__PURE__ */ new Map();
|
|
2221
|
+
const columnInfos = /* @__PURE__ */ new Map();
|
|
2222
|
+
for (const [layerKey, layer] of layers.entries()) {
|
|
2223
|
+
const componentKey = layer.componentsTable;
|
|
2224
|
+
const { columns: colCfgs } = await this.core.tableCfg(componentKey);
|
|
2225
|
+
const columnCfg = [];
|
|
2226
|
+
const columnInfo = [];
|
|
2227
|
+
for (let i = 0; i < colCfgs.length; i++) {
|
|
2228
|
+
if (colCfgs[i].key === "_hash") continue;
|
|
2229
|
+
const colCfg = colCfgs[i];
|
|
2230
|
+
columnCfg.push(colCfg);
|
|
2231
|
+
columnInfo.push({
|
|
2232
|
+
...colCfg,
|
|
2233
|
+
alias: `${colCfg.key}`,
|
|
2234
|
+
route: Route.fromFlat(
|
|
2235
|
+
`/${cakeKey}/${layerKey}/${componentKey}/${colCfg.key}`
|
|
2236
|
+
).flat.slice(1),
|
|
2237
|
+
titleShort: colCfg.key,
|
|
2238
|
+
titleLong: colCfg.key
|
|
2239
|
+
});
|
|
2240
|
+
}
|
|
2241
|
+
columnInfos.set(componentKey, columnInfo);
|
|
2242
|
+
columnCfgs.set(componentKey, columnCfg);
|
|
2243
|
+
}
|
|
2244
|
+
const rowMap = /* @__PURE__ */ new Map();
|
|
2245
|
+
for (const sliceId of mergedSliceIds) {
|
|
2246
|
+
let sliceIdRow = [];
|
|
2247
|
+
for (const [layerKey, layer] of layers.entries()) {
|
|
2248
|
+
const layerRef = layer._hash;
|
|
2249
|
+
const componentKey = layer.componentsTable;
|
|
2250
|
+
const componentRef = layer.add[sliceId];
|
|
2251
|
+
const componentsTable = data[componentKey];
|
|
2252
|
+
const component = componentsTable._data.find(
|
|
2253
|
+
(r) => r._hash === componentRef
|
|
2254
|
+
);
|
|
2255
|
+
const colCfgs = columnCfgs.get(componentKey);
|
|
2256
|
+
const joinColumns = [];
|
|
2257
|
+
for (let i = 0; i < colCfgs.length; i++) {
|
|
2258
|
+
const columnCfg = colCfgs[i];
|
|
2259
|
+
joinColumns.push({
|
|
2260
|
+
route: Route.fromFlat(
|
|
2261
|
+
`${cakeKey}@${cakeRef}/${layerKey}@${layerRef}/${componentKey}@${componentRef}/${columnCfg.key}`
|
|
2262
|
+
).toRouteWithProperty(),
|
|
2263
|
+
value: component[columnCfg.key] ?? null,
|
|
2264
|
+
insert: null
|
|
2265
|
+
});
|
|
2266
|
+
}
|
|
2267
|
+
sliceIdRow = [...sliceIdRow, ...joinColumns];
|
|
2268
|
+
}
|
|
2269
|
+
rowMap.set(sliceId, sliceIdRow);
|
|
2270
|
+
}
|
|
2271
|
+
const joinRows = {};
|
|
2272
|
+
for (const [sliceId, joinColumns] of rowMap.entries()) {
|
|
2273
|
+
Object.assign(joinRows, {
|
|
2274
|
+
[sliceId]: joinColumns
|
|
2275
|
+
});
|
|
2276
|
+
}
|
|
2277
|
+
const joinColumnInfos = Array.from(columnInfos.values()).flat();
|
|
2278
|
+
const joinColumnSelection = new ColumnSelection(joinColumnInfos);
|
|
2279
|
+
return new Join(joinRows, joinColumnSelection).select(columnSelection);
|
|
2280
|
+
}
|
|
2281
|
+
// ...........................................................................
|
|
2282
|
+
/**
|
|
2283
|
+
* Fetches data for the given ColumnSelection
|
|
2284
|
+
* @param columnSelection - The ColumnSelection to fetch data for
|
|
2285
|
+
*/
|
|
2286
|
+
async _getBaseDataForColumnSelection(columnSelection) {
|
|
2287
|
+
const uniqueComponentRoutes = /* @__PURE__ */ new Set();
|
|
2288
|
+
for (const colInfo of columnSelection.columns) {
|
|
2289
|
+
const route = Route.fromFlat(colInfo.route).toRouteWithProperty();
|
|
2290
|
+
uniqueComponentRoutes.add(route.toRouteWithoutProperty().flat);
|
|
2291
|
+
}
|
|
2292
|
+
const data = {};
|
|
2293
|
+
for (const compRouteFlat of uniqueComponentRoutes) {
|
|
2294
|
+
const uniqueComponentRoute = Route.fromFlat(compRouteFlat);
|
|
2295
|
+
const componentData = await this.get(uniqueComponentRoute, {});
|
|
2296
|
+
Object.assign(data, componentData);
|
|
2297
|
+
}
|
|
2298
|
+
return data;
|
|
2299
|
+
}
|
|
2300
|
+
// ...........................................................................
|
|
2301
|
+
/**
|
|
2302
|
+
* Runs an Insert by executing the appropriate controller(s) based on the Insert's route
|
|
2303
|
+
* @param Insert - The Insert to run
|
|
2304
|
+
* @returns The result of the Insert as an InsertHistoryRow
|
|
2305
|
+
* @throws {Error} If the Insert is not valid or if any controller cannot be created
|
|
2306
|
+
*/
|
|
2307
|
+
async insert(insert, options) {
|
|
2308
|
+
const initialRoute = Route.fromFlat(insert.route);
|
|
2309
|
+
const runs = await this._resolveInsert(insert);
|
|
2310
|
+
const errors = validateInsert(insert);
|
|
2311
|
+
if (!!errors.hasErrors) {
|
|
2312
|
+
throw new Error(
|
|
2313
|
+
`Db.insert: Insert is not valid:
|
|
2314
|
+
${JSON.stringify(errors, null, 2)}`
|
|
2315
|
+
);
|
|
2316
|
+
}
|
|
2317
|
+
return this._insert(insert, initialRoute, runs, options);
|
|
2318
|
+
}
|
|
2319
|
+
// ...........................................................................
|
|
2320
|
+
/**
|
|
2321
|
+
* Recursively runs controllers based on the route of the Insert
|
|
2322
|
+
* @param insert - The Insert to run
|
|
2323
|
+
* @param route - The route of the Insert
|
|
2324
|
+
* @param runFns - A record of controller run functions, keyed by table name
|
|
2325
|
+
* @returns The result of the Insert
|
|
2326
|
+
* @throws {Error} If the route is not valid or if any controller cannot be created
|
|
2327
|
+
*/
|
|
2328
|
+
async _insert(insert, route, runFns, options) {
|
|
604
2329
|
let result;
|
|
605
2330
|
let tableKey;
|
|
606
2331
|
const segment = route.segment(0);
|
|
@@ -608,7 +2333,7 @@ ${JSON.stringify(errors, null, 2)}`);
|
|
|
608
2333
|
let previous = [];
|
|
609
2334
|
if (Route.segmentHasRef(segment)) {
|
|
610
2335
|
const routeRef = Route.segmentRef(segment);
|
|
611
|
-
if (Route.
|
|
2336
|
+
if (Route.segmentHasInsertHistoryRef(segment)) {
|
|
612
2337
|
previous = [...previous, routeRef];
|
|
613
2338
|
}
|
|
614
2339
|
if (Route.segmentHasDefaultRef(segment)) {
|
|
@@ -621,12 +2346,12 @@ ${JSON.stringify(errors, null, 2)}`);
|
|
|
621
2346
|
}
|
|
622
2347
|
if (!route.isRoot) {
|
|
623
2348
|
const childRoute = route.deeper(1);
|
|
624
|
-
const childKeys = this._childKeys(
|
|
2349
|
+
const childKeys = this._childKeys(insert.value);
|
|
625
2350
|
const childRefs = {};
|
|
626
2351
|
for (const k of childKeys) {
|
|
627
|
-
const childValue =
|
|
628
|
-
const
|
|
629
|
-
const childResult = await this.
|
|
2352
|
+
const childValue = insert.value[k];
|
|
2353
|
+
const childInsert = { ...insert, value: childValue };
|
|
2354
|
+
const childResult = await this._insert(childInsert, childRoute, runFns);
|
|
630
2355
|
const childRefKey = childRoute.top.tableKey + "Ref";
|
|
631
2356
|
const childRef = childResult[childRefKey];
|
|
632
2357
|
childRefs[k] = childRef;
|
|
@@ -634,12 +2359,12 @@ ${JSON.stringify(errors, null, 2)}`);
|
|
|
634
2359
|
const runFn = runFns[tableKey];
|
|
635
2360
|
result = {
|
|
636
2361
|
...await runFn(
|
|
637
|
-
|
|
2362
|
+
insert.command,
|
|
638
2363
|
{
|
|
639
|
-
...
|
|
2364
|
+
...insert.value,
|
|
640
2365
|
...childRefs
|
|
641
2366
|
},
|
|
642
|
-
|
|
2367
|
+
insert.origin
|
|
643
2368
|
),
|
|
644
2369
|
previous
|
|
645
2370
|
};
|
|
@@ -647,21 +2372,21 @@ ${JSON.stringify(errors, null, 2)}`);
|
|
|
647
2372
|
tableKey = route.root.tableKey;
|
|
648
2373
|
const runFn = runFns[tableKey];
|
|
649
2374
|
result = {
|
|
650
|
-
...await runFn(
|
|
2375
|
+
...await runFn(insert.command, insert.value, insert.origin),
|
|
651
2376
|
previous
|
|
652
2377
|
};
|
|
653
2378
|
}
|
|
654
|
-
result.route =
|
|
655
|
-
await this.
|
|
2379
|
+
result.route = insert.route;
|
|
2380
|
+
await this._writeInsertHistory(tableKey, result);
|
|
656
2381
|
if (!options?.skipNotification)
|
|
657
|
-
this.notify.notify(Route.fromFlat(
|
|
2382
|
+
this.notify.notify(Route.fromFlat(insert.route), result);
|
|
658
2383
|
return result;
|
|
659
2384
|
}
|
|
660
2385
|
// ...........................................................................
|
|
661
2386
|
/**
|
|
662
|
-
* Registers a callback to be called when an
|
|
2387
|
+
* Registers a callback to be called when an Insert is made on the given route
|
|
663
2388
|
* @param route - The route to register the callback on
|
|
664
|
-
* @param callback - The callback to be called when an
|
|
2389
|
+
* @param callback - The callback to be called when an Insert is made
|
|
665
2390
|
*/
|
|
666
2391
|
registerObserver(route, callback) {
|
|
667
2392
|
this.notify.register(route, callback);
|
|
@@ -677,34 +2402,34 @@ ${JSON.stringify(errors, null, 2)}`);
|
|
|
677
2402
|
}
|
|
678
2403
|
// ...........................................................................
|
|
679
2404
|
/**
|
|
680
|
-
* Resolves an
|
|
681
|
-
* @param
|
|
2405
|
+
* Resolves an Insert by returning the run functions of all controllers involved in the Insert's route
|
|
2406
|
+
* @param Insert - The Insert to resolve
|
|
682
2407
|
* @returns A record of controller run functions, keyed by table name
|
|
683
2408
|
* @throws {Error} If the route is not valid or if any controller cannot be created
|
|
684
2409
|
*/
|
|
685
|
-
async
|
|
2410
|
+
async _resolveInsert(Insert2) {
|
|
686
2411
|
const controllers = await this._indexedControllers(
|
|
687
|
-
Route.fromFlat(
|
|
2412
|
+
Route.fromFlat(Insert2.route)
|
|
688
2413
|
);
|
|
689
2414
|
const runFns = {};
|
|
690
2415
|
for (const tableKey of Object.keys(controllers)) {
|
|
691
|
-
runFns[tableKey] = controllers[tableKey].
|
|
2416
|
+
runFns[tableKey] = controllers[tableKey].insert.bind(
|
|
2417
|
+
controllers[tableKey]
|
|
2418
|
+
);
|
|
692
2419
|
}
|
|
693
2420
|
return runFns;
|
|
694
2421
|
}
|
|
695
2422
|
// ...........................................................................
|
|
696
2423
|
/**
|
|
697
2424
|
* Returns the keys of child refs in a value based on a route
|
|
698
|
-
* @param route - The route to check
|
|
699
2425
|
* @param value - The value to check
|
|
700
2426
|
* @returns An array of keys of child refs in the value
|
|
701
2427
|
*/
|
|
702
|
-
_childKeys(
|
|
2428
|
+
_childKeys(value) {
|
|
703
2429
|
const keys = Object.keys(value);
|
|
704
2430
|
const childKeys = [];
|
|
705
2431
|
for (const k of keys) {
|
|
706
2432
|
if (typeof value[k] !== "object") continue;
|
|
707
|
-
if (k.endsWith("Ref") && route.next?.tableKey + "Ref" !== k) continue;
|
|
708
2433
|
childKeys.push(k);
|
|
709
2434
|
}
|
|
710
2435
|
return childKeys;
|
|
@@ -718,25 +2443,19 @@ ${JSON.stringify(errors, null, 2)}`);
|
|
|
718
2443
|
* @throws {Error} If the table does not exist or if the table type is not supported
|
|
719
2444
|
*/
|
|
720
2445
|
async getController(tableKey, refs) {
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
const tableExists = this.core.hasTable(tableKey);
|
|
725
|
-
if (!tableExists) {
|
|
726
|
-
throw new Error(`Table ${tableKey} does not exist.`);
|
|
2446
|
+
const hasTable = await this.core.hasTable(tableKey);
|
|
2447
|
+
if (!hasTable) {
|
|
2448
|
+
throw new Error(`Db.getController: Table ${tableKey} does not exist.`);
|
|
727
2449
|
}
|
|
728
2450
|
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
2451
|
return createController(contentType, this.core, tableKey, refs);
|
|
733
2452
|
}
|
|
734
2453
|
// ...........................................................................
|
|
735
2454
|
async _indexedControllers(route) {
|
|
736
|
-
if (!route.isValid) throw new Error(`Route ${route.flat} is not valid.`);
|
|
737
2455
|
const controllers = {};
|
|
738
|
-
|
|
739
|
-
|
|
2456
|
+
const isolatedRoute = await this.isolatePropertyKeyFromRoute(route);
|
|
2457
|
+
for (let i = 0; i < isolatedRoute.segments.length; i++) {
|
|
2458
|
+
const segment = isolatedRoute.segments[i];
|
|
740
2459
|
const tableKey = segment.tableKey;
|
|
741
2460
|
const segmentRef = Route.segmentRef(segment);
|
|
742
2461
|
const base = segmentRef ? isTimeId(segmentRef) ? await this.getRefOfTimeId(tableKey, segmentRef) : segmentRef : null;
|
|
@@ -749,42 +2468,38 @@ ${JSON.stringify(errors, null, 2)}`);
|
|
|
749
2468
|
}
|
|
750
2469
|
// ...........................................................................
|
|
751
2470
|
/**
|
|
752
|
-
* Adds an
|
|
753
|
-
* @param table - The table the
|
|
754
|
-
* @param
|
|
755
|
-
* @throws {Error} If the
|
|
2471
|
+
* Adds an InsertHistory row to the InsertHistory table of a table
|
|
2472
|
+
* @param table - The table the Insert was made on
|
|
2473
|
+
* @param InsertHistoryRow - The InsertHistory row to add
|
|
2474
|
+
* @throws {Error} If the InsertHistory table does not exist
|
|
756
2475
|
*/
|
|
757
|
-
async
|
|
758
|
-
const
|
|
759
|
-
const hasTable = await this.core.hasTable(protocolTable);
|
|
760
|
-
if (!hasTable) {
|
|
761
|
-
throw new Error(`Table ${table} does not exist`);
|
|
762
|
-
}
|
|
2476
|
+
async _writeInsertHistory(table, insertHistoryRow) {
|
|
2477
|
+
const insertHistoryTable = table + "InsertHistory";
|
|
763
2478
|
await this.core.import({
|
|
764
|
-
[
|
|
765
|
-
_data: [
|
|
766
|
-
_type: "
|
|
2479
|
+
[insertHistoryTable]: {
|
|
2480
|
+
_data: [insertHistoryRow],
|
|
2481
|
+
_type: "insertHistory"
|
|
767
2482
|
}
|
|
768
2483
|
});
|
|
769
2484
|
}
|
|
770
2485
|
// ...........................................................................
|
|
771
2486
|
/**
|
|
772
|
-
* Get the
|
|
773
|
-
* @param table - The table to get the
|
|
774
|
-
* @throws {Error} If the
|
|
2487
|
+
* Get the InsertHistory of a table
|
|
2488
|
+
* @param table - The table to get the InsertHistory for
|
|
2489
|
+
* @throws {Error} If the InsertHistory table does not exist
|
|
775
2490
|
*/
|
|
776
|
-
async
|
|
777
|
-
const
|
|
778
|
-
const hasTable = await this.core.hasTable(
|
|
2491
|
+
async getInsertHistory(table, options) {
|
|
2492
|
+
const insertHistoryTable = table + "InsertHistory";
|
|
2493
|
+
const hasTable = await this.core.hasTable(insertHistoryTable);
|
|
779
2494
|
if (!hasTable) {
|
|
780
|
-
throw new Error(`Table ${table} does not exist`);
|
|
2495
|
+
throw new Error(`Db.getInsertHistory: Table ${table} does not exist`);
|
|
781
2496
|
}
|
|
782
2497
|
if (options === void 0) {
|
|
783
2498
|
options = { sorted: false, ascending: true };
|
|
784
2499
|
}
|
|
785
2500
|
if (options.sorted) {
|
|
786
|
-
const dumpedTable = await this.core.dumpTable(
|
|
787
|
-
const tableData = dumpedTable[
|
|
2501
|
+
const dumpedTable = await this.core.dumpTable(insertHistoryTable);
|
|
2502
|
+
const tableData = dumpedTable[insertHistoryTable]._data;
|
|
788
2503
|
tableData.sort((a, b) => {
|
|
789
2504
|
const aTime = a.timeId.split(":")[1];
|
|
790
2505
|
const bTime = b.timeId.split(":")[1];
|
|
@@ -794,40 +2509,44 @@ ${JSON.stringify(errors, null, 2)}`);
|
|
|
794
2509
|
return bTime.localeCompare(aTime);
|
|
795
2510
|
}
|
|
796
2511
|
});
|
|
797
|
-
return {
|
|
2512
|
+
return {
|
|
2513
|
+
[insertHistoryTable]: { _data: tableData, _type: "insertHistory" }
|
|
2514
|
+
};
|
|
798
2515
|
}
|
|
799
|
-
return this.core.dumpTable(
|
|
2516
|
+
return this.core.dumpTable(insertHistoryTable);
|
|
800
2517
|
}
|
|
801
2518
|
// ...........................................................................
|
|
802
2519
|
/**
|
|
803
|
-
* Get a specific
|
|
804
|
-
* @param table - The table to get the
|
|
805
|
-
* @param ref - The reference of the
|
|
806
|
-
* @returns The
|
|
807
|
-
* @throws {Error} If the
|
|
2520
|
+
* Get a specific InsertHistory row from a table
|
|
2521
|
+
* @param table - The table to get the InsertHistory row from
|
|
2522
|
+
* @param ref - The reference of the InsertHistory row to get
|
|
2523
|
+
* @returns The InsertHistory row or null if it does not exist
|
|
2524
|
+
* @throws {Error} If the Inserts table does not exist
|
|
808
2525
|
*/
|
|
809
|
-
async
|
|
810
|
-
const
|
|
2526
|
+
async getInsertHistoryRowsByRef(table, ref) {
|
|
2527
|
+
const insertHistoryTable = table + "InsertHistory";
|
|
811
2528
|
const {
|
|
812
|
-
[
|
|
813
|
-
} = await this.core.readRows(
|
|
814
|
-
return
|
|
2529
|
+
[insertHistoryTable]: { _data: insertHistory }
|
|
2530
|
+
} = await this.core.readRows(insertHistoryTable, { [table + "Ref"]: ref });
|
|
2531
|
+
return insertHistory;
|
|
815
2532
|
}
|
|
816
2533
|
// ...........................................................................
|
|
817
2534
|
/**
|
|
818
|
-
* Get a specific
|
|
819
|
-
* @param table - The table to get the
|
|
820
|
-
* @param timeId - The timeId of the
|
|
821
|
-
* @returns The
|
|
822
|
-
* @throws {Error} If the
|
|
2535
|
+
* Get a specific InsertHistory row from a table by its timeId
|
|
2536
|
+
* @param table - The table to get the InsertHistory row from
|
|
2537
|
+
* @param timeId - The timeId of the InsertHistory row to get
|
|
2538
|
+
* @returns The InsertHistory row or null if it does not exist
|
|
2539
|
+
* @throws {Error} If the Inserts table does not exist
|
|
823
2540
|
*/
|
|
824
|
-
async
|
|
825
|
-
const
|
|
826
|
-
const { [
|
|
827
|
-
|
|
828
|
-
{
|
|
2541
|
+
async getInsertHistoryRowByTimeId(table, timeId2) {
|
|
2542
|
+
const insertHistoryTable = table + "InsertHistory";
|
|
2543
|
+
const { [insertHistoryTable]: result } = await this.core.readRows(
|
|
2544
|
+
insertHistoryTable,
|
|
2545
|
+
{
|
|
2546
|
+
timeId: timeId2
|
|
2547
|
+
}
|
|
829
2548
|
);
|
|
830
|
-
return result._data?.[0]
|
|
2549
|
+
return result._data?.[0];
|
|
831
2550
|
}
|
|
832
2551
|
// ...........................................................................
|
|
833
2552
|
/**
|
|
@@ -835,15 +2554,17 @@ ${JSON.stringify(errors, null, 2)}`);
|
|
|
835
2554
|
* @param table - The table to get the timeIds from
|
|
836
2555
|
* @param ref - The reference to get the timeIds for
|
|
837
2556
|
* @returns An array of timeIds
|
|
838
|
-
* @throws {Error} If the
|
|
2557
|
+
* @throws {Error} If the Inserts table does not exist
|
|
839
2558
|
*/
|
|
840
2559
|
async getTimeIdsForRef(table, ref) {
|
|
841
|
-
const
|
|
842
|
-
const { [
|
|
843
|
-
|
|
844
|
-
{
|
|
2560
|
+
const insertHistoryTable = table + "InsertHistory";
|
|
2561
|
+
const { [insertHistoryTable]: result } = await this.core.readRows(
|
|
2562
|
+
insertHistoryTable,
|
|
2563
|
+
{
|
|
2564
|
+
[table + "Ref"]: ref
|
|
2565
|
+
}
|
|
845
2566
|
);
|
|
846
|
-
return result._data?.map((r) => r.timeId)
|
|
2567
|
+
return result._data?.map((r) => r.timeId);
|
|
847
2568
|
}
|
|
848
2569
|
// ...........................................................................
|
|
849
2570
|
/**
|
|
@@ -851,24 +2572,70 @@ ${JSON.stringify(errors, null, 2)}`);
|
|
|
851
2572
|
* @param table - The table to get the ref from
|
|
852
2573
|
* @param timeId - The timeId to get the ref for
|
|
853
2574
|
* @returns The ref or null if it does not exist
|
|
854
|
-
* @throws {Error} If the
|
|
2575
|
+
* @throws {Error} If the Inserts table does not exist
|
|
855
2576
|
*/
|
|
856
2577
|
async getRefOfTimeId(table, timeId2) {
|
|
857
|
-
const
|
|
858
|
-
const { [
|
|
859
|
-
|
|
860
|
-
{
|
|
2578
|
+
const insertHistoryTable = table + "InsertHistory";
|
|
2579
|
+
const { [insertHistoryTable]: result } = await this.core.readRows(
|
|
2580
|
+
insertHistoryTable,
|
|
2581
|
+
{
|
|
2582
|
+
timeId: timeId2
|
|
2583
|
+
}
|
|
861
2584
|
);
|
|
862
|
-
return result._data?.[0]?.[table + "Ref"]
|
|
2585
|
+
return result._data?.[0]?.[table + "Ref"];
|
|
863
2586
|
}
|
|
2587
|
+
// ...........................................................................
|
|
864
2588
|
/**
|
|
865
|
-
*
|
|
866
|
-
* @
|
|
2589
|
+
* Isolates the property key from the last segment of a route if it is a property of a component
|
|
2590
|
+
* @param route - The route to extract property key from
|
|
2591
|
+
* @returns A route with extracted property key
|
|
867
2592
|
*/
|
|
868
|
-
|
|
869
|
-
const
|
|
870
|
-
|
|
871
|
-
|
|
2593
|
+
async isolatePropertyKeyFromRoute(route) {
|
|
2594
|
+
const lastSegment = route.segments[route.segments.length - 1];
|
|
2595
|
+
const tableKey = lastSegment.tableKey;
|
|
2596
|
+
const tableExists = await this._io.tableExists(tableKey);
|
|
2597
|
+
if (!Route.segmentHasRef(lastSegment) && !tableExists) {
|
|
2598
|
+
const result = route.upper();
|
|
2599
|
+
result.propertyKey = lastSegment.tableKey;
|
|
2600
|
+
return result;
|
|
2601
|
+
}
|
|
2602
|
+
return route;
|
|
2603
|
+
}
|
|
2604
|
+
// ...........................................................................
|
|
2605
|
+
/**
|
|
2606
|
+
* Isolates a property from all components in an Rljson
|
|
2607
|
+
* @param rljson - The Rljson to isolate the property from
|
|
2608
|
+
* @param propertyKey - The property key to isolate
|
|
2609
|
+
* @returns A new Rljson with only the isolated property
|
|
2610
|
+
*/
|
|
2611
|
+
isolatePropertyFromComponents(rljson, propertyKey) {
|
|
2612
|
+
const result = {};
|
|
2613
|
+
for (const tableKey of Object.keys(rljson)) {
|
|
2614
|
+
const table = rljson[tableKey];
|
|
2615
|
+
const newData = table._data.map((row) => {
|
|
2616
|
+
if (row.hasOwnProperty(propertyKey)) {
|
|
2617
|
+
return {
|
|
2618
|
+
[propertyKey]: row[propertyKey],
|
|
2619
|
+
_hash: row._hash
|
|
2620
|
+
};
|
|
2621
|
+
} else {
|
|
2622
|
+
return row;
|
|
2623
|
+
}
|
|
2624
|
+
});
|
|
2625
|
+
result[tableKey] = {
|
|
2626
|
+
_type: table._type,
|
|
2627
|
+
_data: newData
|
|
2628
|
+
};
|
|
2629
|
+
}
|
|
2630
|
+
return result;
|
|
2631
|
+
}
|
|
2632
|
+
// ...........................................................................
|
|
2633
|
+
/**
|
|
2634
|
+
* Get the current cache of the Db instance
|
|
2635
|
+
*/
|
|
2636
|
+
get cache() {
|
|
2637
|
+
return this._cache;
|
|
2638
|
+
}
|
|
872
2639
|
}
|
|
873
2640
|
export {
|
|
874
2641
|
Db as RljsonDb
|