@rljson/db 0.0.3 → 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/README.contributors.md +15 -225
- package/dist/README.contributors.md +15 -225
- package/dist/cars-example.d.ts +41 -0
- package/dist/core.d.ts +18 -6
- package/dist/db.d.ts +151 -3
- package/dist/db.js +2585 -42
- package/dist/notify.d.ts +39 -0
- package/dist/src/example.ts +9 -5
- package/package.json +31 -36
package/dist/db.js
CHANGED
|
@@ -1,22 +1,644 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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";
|
|
4
4
|
import { IoMem } from "@rljson/io";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
import { compileExpression } from "filtrex";
|
|
6
|
+
class BaseController {
|
|
7
|
+
constructor(_core, _tableKey) {
|
|
8
|
+
this._core = _core;
|
|
9
|
+
this._tableKey = _tableKey;
|
|
10
|
+
}
|
|
11
|
+
// ...........................................................................
|
|
12
|
+
/**
|
|
13
|
+
* Retrieves the current state of the table.
|
|
14
|
+
* @returns A promise that resolves to the current state of the table.
|
|
15
|
+
*/
|
|
16
|
+
async table() {
|
|
17
|
+
const rljson = await this._core.dumpTable(this._tableKey);
|
|
18
|
+
return rljson[this._tableKey];
|
|
19
|
+
}
|
|
20
|
+
// ...........................................................................
|
|
21
|
+
/**
|
|
22
|
+
* Fetches a specific entry from the table by its reference or by a partial match.
|
|
23
|
+
* @param where A string representing the reference of the entry to fetch, or an object representing a partial match.
|
|
24
|
+
* @returns A promise that resolves to an array of entries matching the criteria, or null if no entries are found.
|
|
25
|
+
*/
|
|
26
|
+
async get(where, filter) {
|
|
27
|
+
if (typeof where === "string") {
|
|
28
|
+
return this._getByHash(where, filter);
|
|
29
|
+
} else if (typeof where === "object" && where !== null) {
|
|
30
|
+
if (Object.keys(where).length === 1 && "_hash" in where && typeof where["_hash"] === "string") {
|
|
31
|
+
return this._getByHash(where["_hash"], filter);
|
|
32
|
+
}
|
|
33
|
+
return this._getByWhere(where, filter);
|
|
34
|
+
} else {
|
|
35
|
+
return Promise.resolve({});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// ...........................................................................
|
|
39
|
+
/**
|
|
40
|
+
* Fetches a specific entry from the table by its reference.
|
|
41
|
+
* @param hash A string representing the reference of the entry to fetch.
|
|
42
|
+
* @returns A promise that resolves to the entry matching the reference, or null if no entry is found.
|
|
43
|
+
*/
|
|
44
|
+
async _getByHash(hash, filter) {
|
|
45
|
+
let result = {};
|
|
46
|
+
if (!filter || equals(filter, {})) {
|
|
47
|
+
result = await this._core.readRow(this._tableKey, hash);
|
|
48
|
+
} else {
|
|
49
|
+
result = await this._core.readRows(this._tableKey, {
|
|
50
|
+
_hash: hash,
|
|
51
|
+
...filter
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
// ...........................................................................
|
|
57
|
+
/**
|
|
58
|
+
* Fetches entries from the table that match the specified criteria.
|
|
59
|
+
* @param where An object representing the criteria to match.
|
|
60
|
+
* @returns A promise that resolves to an array of entries matching the criteria, or null if no entries are found.
|
|
61
|
+
*/
|
|
62
|
+
async _getByWhere(where, filter) {
|
|
63
|
+
const rows = await this._core.readRows(this._tableKey, {
|
|
64
|
+
...where,
|
|
65
|
+
...filter
|
|
66
|
+
});
|
|
67
|
+
return rows;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
class CakeController extends BaseController {
|
|
71
|
+
constructor(_core, _tableKey, _refs) {
|
|
72
|
+
super(_core, _tableKey);
|
|
73
|
+
this._core = _core;
|
|
74
|
+
this._tableKey = _tableKey;
|
|
75
|
+
this._refs = _refs;
|
|
76
|
+
}
|
|
77
|
+
_table = null;
|
|
78
|
+
_baseLayers = {};
|
|
79
|
+
async init() {
|
|
80
|
+
if (this._tableKey.endsWith("Cake") === false) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`Table ${this._tableKey} is not supported by CakeController.`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
const rljson = await this._core.dumpTable(this._tableKey);
|
|
86
|
+
this._table = rljson[this._tableKey];
|
|
87
|
+
if (this._table._type !== "cakes") {
|
|
88
|
+
throw new Error(`Table ${this._tableKey} is not of type cakes.`);
|
|
89
|
+
}
|
|
90
|
+
if (this._refs && this._refs.base) {
|
|
91
|
+
const {
|
|
92
|
+
[this._tableKey]: { _data: baseCakes }
|
|
93
|
+
} = await this._core.readRow(this._tableKey, this._refs.base);
|
|
94
|
+
if (baseCakes.length === 0) {
|
|
95
|
+
throw new Error(`Base cake ${this._refs.base} does not exist.`);
|
|
96
|
+
}
|
|
97
|
+
const baseCake = baseCakes[0];
|
|
98
|
+
this._baseLayers = rmhsh(baseCake.layers);
|
|
99
|
+
} else {
|
|
100
|
+
const cake = this._table._data[0];
|
|
101
|
+
this._refs = {
|
|
102
|
+
sliceIdsTable: cake.sliceIdsTable,
|
|
103
|
+
sliceIdsRow: cake.sliceIdsRow
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
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) {
|
|
126
|
+
if (!command.startsWith("add")) {
|
|
127
|
+
throw new Error(`Command ${command} is not supported by CakeController.`);
|
|
128
|
+
}
|
|
129
|
+
const cake = {
|
|
130
|
+
layers: { ...this._baseLayers, ...value },
|
|
131
|
+
...refs || this._refs
|
|
132
|
+
};
|
|
133
|
+
const rlJson = { [this._tableKey]: { _data: [cake] } };
|
|
134
|
+
await this._core.import(rlJson);
|
|
135
|
+
const result = {
|
|
136
|
+
//Ref to component
|
|
137
|
+
[this._tableKey + "Ref"]: hsh(cake)._hash,
|
|
138
|
+
//Data from edit
|
|
139
|
+
route: "",
|
|
140
|
+
origin,
|
|
141
|
+
//Unique id/timestamp
|
|
142
|
+
timeId: timeId()
|
|
143
|
+
};
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
async get(where, filter) {
|
|
147
|
+
if (typeof where === "string") {
|
|
148
|
+
return this._getByHash(where, filter);
|
|
149
|
+
} else if (typeof where === "object" && where !== null) {
|
|
150
|
+
return this._getByWhere(where, filter);
|
|
151
|
+
} else {
|
|
152
|
+
return Promise.resolve({});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
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;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
class ComponentController extends BaseController {
|
|
166
|
+
constructor(_core, _tableKey, _refs) {
|
|
167
|
+
super(_core, _tableKey);
|
|
168
|
+
this._core = _core;
|
|
169
|
+
this._tableKey = _tableKey;
|
|
170
|
+
this._refs = _refs;
|
|
171
|
+
}
|
|
172
|
+
_tableCfg = null;
|
|
173
|
+
_resolvedColumns = null;
|
|
174
|
+
_refTableKeyToColumnKeyMap = null;
|
|
175
|
+
async init() {
|
|
176
|
+
if (!!this._refs && !this._refs.base) {
|
|
177
|
+
throw new Error(`Refs are not required on ComponentController.`);
|
|
178
|
+
}
|
|
179
|
+
const rljson = await this._core.dumpTable(this._tableKey);
|
|
180
|
+
const table = rljson[this._tableKey];
|
|
181
|
+
if (table._type !== "components") {
|
|
182
|
+
throw new Error(`Table ${this._tableKey} is not of type components.`);
|
|
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();
|
|
189
|
+
}
|
|
190
|
+
async insert(command, value, origin, refs) {
|
|
191
|
+
if (!command.startsWith("add")) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
`Command ${command} is not supported by ComponentController.`
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
if (!!refs) {
|
|
197
|
+
throw new Error(`Refs are not supported on ComponentController.`);
|
|
198
|
+
}
|
|
199
|
+
const component = value;
|
|
200
|
+
const rlJson = { [this._tableKey]: { _data: [component] } };
|
|
201
|
+
await this._core.import(rlJson);
|
|
202
|
+
return {
|
|
203
|
+
//Ref to component
|
|
204
|
+
[this._tableKey + "Ref"]: hsh(component)._hash,
|
|
205
|
+
//Data from edit
|
|
206
|
+
route: "",
|
|
207
|
+
origin,
|
|
208
|
+
//Unique id/timestamp
|
|
209
|
+
timeId: timeId()
|
|
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
|
+
}
|
|
490
|
+
}
|
|
491
|
+
class LayerController extends BaseController {
|
|
492
|
+
constructor(_core, _tableKey, _refs) {
|
|
493
|
+
super(_core, _tableKey);
|
|
494
|
+
this._core = _core;
|
|
495
|
+
this._tableKey = _tableKey;
|
|
496
|
+
this._refs = _refs;
|
|
497
|
+
}
|
|
498
|
+
async init() {
|
|
499
|
+
if (this._tableKey.endsWith("Layer") === false) {
|
|
500
|
+
throw new Error(
|
|
501
|
+
`Table ${this._tableKey} is not supported by LayerController.`
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
const rljson = await this._core.dumpTable(this._tableKey);
|
|
505
|
+
const table = rljson[this._tableKey];
|
|
506
|
+
if (table._type !== "layers") {
|
|
507
|
+
throw new Error(`Table ${this._tableKey} is not of type layers.`);
|
|
508
|
+
}
|
|
509
|
+
if (this._refs && this._refs.base) {
|
|
510
|
+
const {
|
|
511
|
+
[this._tableKey]: { _data: baseLayers }
|
|
512
|
+
} = await this._core.readRow(this._tableKey, this._refs.base);
|
|
513
|
+
if (baseLayers.length === 0) {
|
|
514
|
+
throw new Error(`Base layer ${this._refs.base} does not exist.`);
|
|
515
|
+
}
|
|
516
|
+
const baseLayer = baseLayers[0];
|
|
517
|
+
if (!this._refs.sliceIdsTable || !this._refs.sliceIdsRow || !this._refs.componentsTable) {
|
|
518
|
+
this._refs = {
|
|
519
|
+
sliceIdsTable: baseLayer.sliceIdsTable,
|
|
520
|
+
sliceIdsTableRow: baseLayer.sliceIdsTableRow,
|
|
521
|
+
componentsTable: baseLayer.componentsTable
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
} else {
|
|
525
|
+
const layer = table._data[0];
|
|
526
|
+
this._refs = {
|
|
527
|
+
sliceIdsTable: layer.sliceIdsTable,
|
|
528
|
+
sliceIdsTableRow: layer.sliceIdsTableRow,
|
|
529
|
+
componentsTable: layer.componentsTable
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
async insert(command, value, origin, refs) {
|
|
534
|
+
if (!command.startsWith("add") && !command.startsWith("remove")) {
|
|
535
|
+
throw new Error(
|
|
536
|
+
`Command ${command} is not supported by LayerController.`
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
const layer = command.startsWith("add") === true ? {
|
|
540
|
+
add: value,
|
|
541
|
+
...refs || this._refs
|
|
542
|
+
} : {
|
|
543
|
+
add: {},
|
|
544
|
+
remove: value,
|
|
545
|
+
...refs || this._refs
|
|
546
|
+
};
|
|
547
|
+
const rlJson = { [this._tableKey]: { _data: [layer] } };
|
|
548
|
+
await this._core.import(rlJson);
|
|
549
|
+
const result = {
|
|
550
|
+
//Ref to component
|
|
551
|
+
[this._tableKey + "Ref"]: hsh(layer)._hash,
|
|
552
|
+
//Data from edit
|
|
553
|
+
route: "",
|
|
554
|
+
origin,
|
|
555
|
+
//Unique id/timestamp
|
|
556
|
+
timeId: timeId()
|
|
557
|
+
};
|
|
558
|
+
return result;
|
|
559
|
+
}
|
|
560
|
+
async get(where, filter) {
|
|
561
|
+
if (typeof where === "string") {
|
|
562
|
+
return this._getByHash(where, filter);
|
|
563
|
+
} else {
|
|
564
|
+
return this._getByWhere(where, filter);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
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) {
|
|
571
|
+
const layer = row;
|
|
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
|
+
});
|
|
578
|
+
}
|
|
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;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
const createController = async (type, core, tableKey, refs) => {
|
|
594
|
+
let ctrl;
|
|
595
|
+
switch (type) {
|
|
596
|
+
case "layers":
|
|
597
|
+
ctrl = new LayerController(core, tableKey, refs);
|
|
598
|
+
break;
|
|
599
|
+
case "components":
|
|
600
|
+
ctrl = new ComponentController(core, tableKey, refs);
|
|
601
|
+
break;
|
|
602
|
+
case "cakes":
|
|
603
|
+
ctrl = new CakeController(core, tableKey, refs);
|
|
604
|
+
break;
|
|
605
|
+
default:
|
|
606
|
+
throw new Error(`Controller for type ${type} is not implemented yet.`);
|
|
607
|
+
}
|
|
608
|
+
await ctrl.init();
|
|
609
|
+
return ctrl;
|
|
610
|
+
};
|
|
611
|
+
class Core {
|
|
8
612
|
// ...........................................................................
|
|
9
613
|
constructor(_io) {
|
|
10
614
|
this._io = _io;
|
|
11
615
|
}
|
|
616
|
+
static example = async () => {
|
|
617
|
+
return new Core(await IoMem.example());
|
|
618
|
+
};
|
|
12
619
|
// ...........................................................................
|
|
620
|
+
/**
|
|
621
|
+
* Creates a table and an insertHistory for the table
|
|
622
|
+
* @param tableCfg TableCfg of table to create
|
|
623
|
+
*/
|
|
624
|
+
async createTableWithInsertHistory(tableCfg) {
|
|
625
|
+
await this.createTable(tableCfg);
|
|
626
|
+
await this.createInsertHistory(tableCfg);
|
|
627
|
+
}
|
|
13
628
|
/**
|
|
14
629
|
* Creates a table
|
|
15
|
-
* @param
|
|
16
|
-
|
|
630
|
+
* @param tableCfg TableCfg of table to create
|
|
631
|
+
*/
|
|
632
|
+
async createTable(tableCfg) {
|
|
633
|
+
return this._io.createOrExtendTable({ tableCfg });
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Creates an insertHistory table for a given table
|
|
637
|
+
* @param tableCfg TableCfg of table
|
|
17
638
|
*/
|
|
18
|
-
async
|
|
19
|
-
|
|
639
|
+
async createInsertHistory(tableCfg) {
|
|
640
|
+
const cfg = createInsertHistoryTableCfg(tableCfg);
|
|
641
|
+
await this.createTable(cfg);
|
|
20
642
|
}
|
|
21
643
|
// ...........................................................................
|
|
22
644
|
/**
|
|
@@ -30,17 +652,20 @@ const _Core = class _Core {
|
|
|
30
652
|
* @returns a dump of a table.
|
|
31
653
|
* @throws when table name does not exist
|
|
32
654
|
*/
|
|
33
|
-
dumpTable(
|
|
34
|
-
return this._io.dumpTable({
|
|
655
|
+
dumpTable(table) {
|
|
656
|
+
return this._io.dumpTable({ table });
|
|
35
657
|
}
|
|
36
658
|
// ...........................................................................
|
|
37
659
|
/**
|
|
38
660
|
* Imports data into the memory.
|
|
661
|
+
* @param data - The rljson data to import.
|
|
39
662
|
* @throws {Error} If the data is invalid.
|
|
40
663
|
*/
|
|
41
664
|
async import(data) {
|
|
42
|
-
const
|
|
43
|
-
|
|
665
|
+
const validate = new Validate();
|
|
666
|
+
validate.addValidator(new BaseValidator());
|
|
667
|
+
const result = await validate.run(data);
|
|
668
|
+
if ((result.hasErrors || result.base && result.base.hasErrors) && !result.base.refsNotFound) {
|
|
44
669
|
throw new Error(
|
|
45
670
|
"The imported rljson data is not valid:\n" + JSON.stringify(result, null, 2)
|
|
46
671
|
);
|
|
@@ -49,51 +674,1969 @@ const _Core = class _Core {
|
|
|
49
674
|
}
|
|
50
675
|
// ...........................................................................
|
|
51
676
|
async tables() {
|
|
52
|
-
return this._io.
|
|
677
|
+
return await this._io.dump();
|
|
53
678
|
}
|
|
54
679
|
// ...........................................................................
|
|
55
680
|
async hasTable(table) {
|
|
56
|
-
|
|
57
|
-
|
|
681
|
+
return await this._io.tableExists(table);
|
|
682
|
+
}
|
|
683
|
+
// ...........................................................................
|
|
684
|
+
async contentType(table) {
|
|
685
|
+
const t = await this._io.dumpTable({ table });
|
|
686
|
+
const contentType = t[table]?._type;
|
|
687
|
+
return contentType;
|
|
688
|
+
}
|
|
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;
|
|
58
703
|
}
|
|
59
704
|
// ...........................................................................
|
|
60
705
|
/** Reads a specific row from a database table */
|
|
61
706
|
readRow(table, rowHash) {
|
|
62
|
-
return this._io.
|
|
707
|
+
return this._io.readRows({ table, where: { _hash: rowHash } });
|
|
63
708
|
}
|
|
64
709
|
// ...........................................................................
|
|
65
710
|
readRows(table, where) {
|
|
66
711
|
return this._io.readRows({ table, where });
|
|
67
712
|
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
713
|
+
}
|
|
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);
|
|
722
|
+
}
|
|
723
|
+
// ...........................................................................
|
|
75
724
|
/**
|
|
76
|
-
*
|
|
77
|
-
* @param
|
|
725
|
+
* Returns unique routes from a list of routes
|
|
726
|
+
* @param routes - The list of routes
|
|
727
|
+
* @returns Unique routes
|
|
78
728
|
*/
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
729
|
+
static uniqueRoutes(routes) {
|
|
730
|
+
return Array.from(new Set(routes.map((r) => r.flat))).map(
|
|
731
|
+
(flat) => Route.fromFlat(flat)
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
// ...........................................................................
|
|
735
|
+
/**
|
|
736
|
+
* Returns a ColumnSelection from a list of route segments
|
|
737
|
+
* @param routeSegmentsList - A list of route segments
|
|
738
|
+
* @returns A ColumnSelection object
|
|
739
|
+
*/
|
|
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
|
+
});
|
|
759
|
+
}
|
|
760
|
+
return new ColumnSelection(definition);
|
|
761
|
+
}
|
|
762
|
+
// ...........................................................................
|
|
763
|
+
columns;
|
|
764
|
+
routes;
|
|
765
|
+
aliases;
|
|
766
|
+
routeHashes;
|
|
767
|
+
metadata(key) {
|
|
768
|
+
return this.columns.map((column) => column[key]);
|
|
769
|
+
}
|
|
770
|
+
// ...........................................................................
|
|
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
|
+
);
|
|
777
|
+
}
|
|
778
|
+
// ...........................................................................
|
|
779
|
+
static calcHash(str) {
|
|
780
|
+
return Hash.default.calcHash(str);
|
|
781
|
+
}
|
|
782
|
+
route(aliasRouteOrHash) {
|
|
783
|
+
return this.column(aliasRouteOrHash).route;
|
|
784
|
+
}
|
|
785
|
+
// ...........................................................................
|
|
786
|
+
alias(aliasRouteOrHash) {
|
|
787
|
+
return this.column(aliasRouteOrHash).alias;
|
|
788
|
+
}
|
|
789
|
+
// ...........................................................................
|
|
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}`);
|
|
807
|
+
}
|
|
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
|
+
);
|
|
838
|
+
}
|
|
839
|
+
const validValueRegex = /^[a-zA-Z0-9/]*$/;
|
|
840
|
+
const invalidValues = routes.filter(
|
|
841
|
+
(value) => !validValueRegex.test(value)
|
|
842
|
+
);
|
|
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)
|
|
851
|
+
);
|
|
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.`
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
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);
|
|
878
|
+
}
|
|
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
|
+
]);
|
|
1018
|
+
}
|
|
1019
|
+
// ...........................................................................
|
|
1020
|
+
static empty() {
|
|
1021
|
+
return new ColumnSelection([]);
|
|
86
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;
|
|
87
1054
|
};
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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: ""
|
|
95
1208
|
});
|
|
96
|
-
|
|
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
|
|
1978
|
+
*/
|
|
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) {
|
|
2329
|
+
let result;
|
|
2330
|
+
let tableKey;
|
|
2331
|
+
const segment = route.segment(0);
|
|
2332
|
+
tableKey = segment.tableKey;
|
|
2333
|
+
let previous = [];
|
|
2334
|
+
if (Route.segmentHasRef(segment)) {
|
|
2335
|
+
const routeRef = Route.segmentRef(segment);
|
|
2336
|
+
if (Route.segmentHasInsertHistoryRef(segment)) {
|
|
2337
|
+
previous = [...previous, routeRef];
|
|
2338
|
+
}
|
|
2339
|
+
if (Route.segmentHasDefaultRef(segment)) {
|
|
2340
|
+
const timeIds = await this.getTimeIdsForRef(
|
|
2341
|
+
tableKey,
|
|
2342
|
+
Route.segmentRef(segment)
|
|
2343
|
+
);
|
|
2344
|
+
previous = [...previous, ...timeIds];
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
if (!route.isRoot) {
|
|
2348
|
+
const childRoute = route.deeper(1);
|
|
2349
|
+
const childKeys = this._childKeys(insert.value);
|
|
2350
|
+
const childRefs = {};
|
|
2351
|
+
for (const k of childKeys) {
|
|
2352
|
+
const childValue = insert.value[k];
|
|
2353
|
+
const childInsert = { ...insert, value: childValue };
|
|
2354
|
+
const childResult = await this._insert(childInsert, childRoute, runFns);
|
|
2355
|
+
const childRefKey = childRoute.top.tableKey + "Ref";
|
|
2356
|
+
const childRef = childResult[childRefKey];
|
|
2357
|
+
childRefs[k] = childRef;
|
|
2358
|
+
}
|
|
2359
|
+
const runFn = runFns[tableKey];
|
|
2360
|
+
result = {
|
|
2361
|
+
...await runFn(
|
|
2362
|
+
insert.command,
|
|
2363
|
+
{
|
|
2364
|
+
...insert.value,
|
|
2365
|
+
...childRefs
|
|
2366
|
+
},
|
|
2367
|
+
insert.origin
|
|
2368
|
+
),
|
|
2369
|
+
previous
|
|
2370
|
+
};
|
|
2371
|
+
} else {
|
|
2372
|
+
tableKey = route.root.tableKey;
|
|
2373
|
+
const runFn = runFns[tableKey];
|
|
2374
|
+
result = {
|
|
2375
|
+
...await runFn(insert.command, insert.value, insert.origin),
|
|
2376
|
+
previous
|
|
2377
|
+
};
|
|
2378
|
+
}
|
|
2379
|
+
result.route = insert.route;
|
|
2380
|
+
await this._writeInsertHistory(tableKey, result);
|
|
2381
|
+
if (!options?.skipNotification)
|
|
2382
|
+
this.notify.notify(Route.fromFlat(insert.route), result);
|
|
2383
|
+
return result;
|
|
2384
|
+
}
|
|
2385
|
+
// ...........................................................................
|
|
2386
|
+
/**
|
|
2387
|
+
* Registers a callback to be called when an Insert is made on the given route
|
|
2388
|
+
* @param route - The route to register the callback on
|
|
2389
|
+
* @param callback - The callback to be called when an Insert is made
|
|
2390
|
+
*/
|
|
2391
|
+
registerObserver(route, callback) {
|
|
2392
|
+
this.notify.register(route, callback);
|
|
2393
|
+
}
|
|
2394
|
+
// ...........................................................................
|
|
2395
|
+
/**
|
|
2396
|
+
* Unregisters a callback from the given route
|
|
2397
|
+
* @param route - The route to unregister the callback from
|
|
2398
|
+
* @param callback - The callback to be unregistered
|
|
2399
|
+
*/
|
|
2400
|
+
unregisterObserver(route, callback) {
|
|
2401
|
+
this.notify.unregister(route, callback);
|
|
2402
|
+
}
|
|
2403
|
+
// ...........................................................................
|
|
2404
|
+
/**
|
|
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
|
|
2407
|
+
* @returns A record of controller run functions, keyed by table name
|
|
2408
|
+
* @throws {Error} If the route is not valid or if any controller cannot be created
|
|
2409
|
+
*/
|
|
2410
|
+
async _resolveInsert(Insert2) {
|
|
2411
|
+
const controllers = await this._indexedControllers(
|
|
2412
|
+
Route.fromFlat(Insert2.route)
|
|
2413
|
+
);
|
|
2414
|
+
const runFns = {};
|
|
2415
|
+
for (const tableKey of Object.keys(controllers)) {
|
|
2416
|
+
runFns[tableKey] = controllers[tableKey].insert.bind(
|
|
2417
|
+
controllers[tableKey]
|
|
2418
|
+
);
|
|
2419
|
+
}
|
|
2420
|
+
return runFns;
|
|
2421
|
+
}
|
|
2422
|
+
// ...........................................................................
|
|
2423
|
+
/**
|
|
2424
|
+
* Returns the keys of child refs in a value based on a route
|
|
2425
|
+
* @param value - The value to check
|
|
2426
|
+
* @returns An array of keys of child refs in the value
|
|
2427
|
+
*/
|
|
2428
|
+
_childKeys(value) {
|
|
2429
|
+
const keys = Object.keys(value);
|
|
2430
|
+
const childKeys = [];
|
|
2431
|
+
for (const k of keys) {
|
|
2432
|
+
if (typeof value[k] !== "object") continue;
|
|
2433
|
+
childKeys.push(k);
|
|
2434
|
+
}
|
|
2435
|
+
return childKeys;
|
|
2436
|
+
}
|
|
2437
|
+
// ...........................................................................
|
|
2438
|
+
/**
|
|
2439
|
+
* Get a controller for a specific table
|
|
2440
|
+
* @param tableKey - The key of the table to get the controller for
|
|
2441
|
+
* @param refs - Optional references required by some controllers
|
|
2442
|
+
* @returns A controller for the specified table
|
|
2443
|
+
* @throws {Error} If the table does not exist or if the table type is not supported
|
|
2444
|
+
*/
|
|
2445
|
+
async getController(tableKey, refs) {
|
|
2446
|
+
const hasTable = await this.core.hasTable(tableKey);
|
|
2447
|
+
if (!hasTable) {
|
|
2448
|
+
throw new Error(`Db.getController: Table ${tableKey} does not exist.`);
|
|
2449
|
+
}
|
|
2450
|
+
const contentType = await this.core.contentType(tableKey);
|
|
2451
|
+
return createController(contentType, this.core, tableKey, refs);
|
|
2452
|
+
}
|
|
2453
|
+
// ...........................................................................
|
|
2454
|
+
async _indexedControllers(route) {
|
|
2455
|
+
const controllers = {};
|
|
2456
|
+
const isolatedRoute = await this.isolatePropertyKeyFromRoute(route);
|
|
2457
|
+
for (let i = 0; i < isolatedRoute.segments.length; i++) {
|
|
2458
|
+
const segment = isolatedRoute.segments[i];
|
|
2459
|
+
const tableKey = segment.tableKey;
|
|
2460
|
+
const segmentRef = Route.segmentRef(segment);
|
|
2461
|
+
const base = segmentRef ? isTimeId(segmentRef) ? await this.getRefOfTimeId(tableKey, segmentRef) : segmentRef : null;
|
|
2462
|
+
controllers[tableKey] ??= await this.getController(
|
|
2463
|
+
tableKey,
|
|
2464
|
+
base ? { base } : void 0
|
|
2465
|
+
);
|
|
2466
|
+
}
|
|
2467
|
+
return controllers;
|
|
2468
|
+
}
|
|
2469
|
+
// ...........................................................................
|
|
2470
|
+
/**
|
|
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
|
|
2475
|
+
*/
|
|
2476
|
+
async _writeInsertHistory(table, insertHistoryRow) {
|
|
2477
|
+
const insertHistoryTable = table + "InsertHistory";
|
|
2478
|
+
await this.core.import({
|
|
2479
|
+
[insertHistoryTable]: {
|
|
2480
|
+
_data: [insertHistoryRow],
|
|
2481
|
+
_type: "insertHistory"
|
|
2482
|
+
}
|
|
2483
|
+
});
|
|
2484
|
+
}
|
|
2485
|
+
// ...........................................................................
|
|
2486
|
+
/**
|
|
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
|
|
2490
|
+
*/
|
|
2491
|
+
async getInsertHistory(table, options) {
|
|
2492
|
+
const insertHistoryTable = table + "InsertHistory";
|
|
2493
|
+
const hasTable = await this.core.hasTable(insertHistoryTable);
|
|
2494
|
+
if (!hasTable) {
|
|
2495
|
+
throw new Error(`Db.getInsertHistory: Table ${table} does not exist`);
|
|
2496
|
+
}
|
|
2497
|
+
if (options === void 0) {
|
|
2498
|
+
options = { sorted: false, ascending: true };
|
|
2499
|
+
}
|
|
2500
|
+
if (options.sorted) {
|
|
2501
|
+
const dumpedTable = await this.core.dumpTable(insertHistoryTable);
|
|
2502
|
+
const tableData = dumpedTable[insertHistoryTable]._data;
|
|
2503
|
+
tableData.sort((a, b) => {
|
|
2504
|
+
const aTime = a.timeId.split(":")[1];
|
|
2505
|
+
const bTime = b.timeId.split(":")[1];
|
|
2506
|
+
if (options.ascending) {
|
|
2507
|
+
return aTime.localeCompare(bTime);
|
|
2508
|
+
} else {
|
|
2509
|
+
return bTime.localeCompare(aTime);
|
|
2510
|
+
}
|
|
2511
|
+
});
|
|
2512
|
+
return {
|
|
2513
|
+
[insertHistoryTable]: { _data: tableData, _type: "insertHistory" }
|
|
2514
|
+
};
|
|
2515
|
+
}
|
|
2516
|
+
return this.core.dumpTable(insertHistoryTable);
|
|
2517
|
+
}
|
|
2518
|
+
// ...........................................................................
|
|
2519
|
+
/**
|
|
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
|
|
2525
|
+
*/
|
|
2526
|
+
async getInsertHistoryRowsByRef(table, ref) {
|
|
2527
|
+
const insertHistoryTable = table + "InsertHistory";
|
|
2528
|
+
const {
|
|
2529
|
+
[insertHistoryTable]: { _data: insertHistory }
|
|
2530
|
+
} = await this.core.readRows(insertHistoryTable, { [table + "Ref"]: ref });
|
|
2531
|
+
return insertHistory;
|
|
2532
|
+
}
|
|
2533
|
+
// ...........................................................................
|
|
2534
|
+
/**
|
|
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
|
|
2540
|
+
*/
|
|
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
|
+
}
|
|
2548
|
+
);
|
|
2549
|
+
return result._data?.[0];
|
|
2550
|
+
}
|
|
2551
|
+
// ...........................................................................
|
|
2552
|
+
/**
|
|
2553
|
+
* Get all timeIds for a specific ref in a table
|
|
2554
|
+
* @param table - The table to get the timeIds from
|
|
2555
|
+
* @param ref - The reference to get the timeIds for
|
|
2556
|
+
* @returns An array of timeIds
|
|
2557
|
+
* @throws {Error} If the Inserts table does not exist
|
|
2558
|
+
*/
|
|
2559
|
+
async getTimeIdsForRef(table, ref) {
|
|
2560
|
+
const insertHistoryTable = table + "InsertHistory";
|
|
2561
|
+
const { [insertHistoryTable]: result } = await this.core.readRows(
|
|
2562
|
+
insertHistoryTable,
|
|
2563
|
+
{
|
|
2564
|
+
[table + "Ref"]: ref
|
|
2565
|
+
}
|
|
2566
|
+
);
|
|
2567
|
+
return result._data?.map((r) => r.timeId);
|
|
2568
|
+
}
|
|
2569
|
+
// ...........................................................................
|
|
2570
|
+
/**
|
|
2571
|
+
* Get the ref for a specific timeId in a table
|
|
2572
|
+
* @param table - The table to get the ref from
|
|
2573
|
+
* @param timeId - The timeId to get the ref for
|
|
2574
|
+
* @returns The ref or null if it does not exist
|
|
2575
|
+
* @throws {Error} If the Inserts table does not exist
|
|
2576
|
+
*/
|
|
2577
|
+
async getRefOfTimeId(table, timeId2) {
|
|
2578
|
+
const insertHistoryTable = table + "InsertHistory";
|
|
2579
|
+
const { [insertHistoryTable]: result } = await this.core.readRows(
|
|
2580
|
+
insertHistoryTable,
|
|
2581
|
+
{
|
|
2582
|
+
timeId: timeId2
|
|
2583
|
+
}
|
|
2584
|
+
);
|
|
2585
|
+
return result._data?.[0]?.[table + "Ref"];
|
|
2586
|
+
}
|
|
2587
|
+
// ...........................................................................
|
|
2588
|
+
/**
|
|
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
|
|
2592
|
+
*/
|
|
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
|
+
}
|
|
2639
|
+
}
|
|
97
2640
|
export {
|
|
98
2641
|
Db as RljsonDb
|
|
99
2642
|
};
|