@cepharum/concrete-db 0.1.2-alpha.1 → 0.2.0
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/concrete-db.d.ts +37 -21
- package/cure.mjs +37 -4
- package/lib/collector.mjs +22 -1
- package/lib/cure.mjs +1 -1
- package/lib/default.shape.yaml +2 -0
- package/lib/generator.mjs +22 -3
- package/lib/helper.mjs +2 -0
- package/lib/shaper.mjs +355 -229
- package/lib/term-functions.mjs +27 -16
- package/package.json +2 -2
package/lib/shaper.mjs
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
// noinspection ExceptionCaughtLocallyJS
|
|
2
|
+
|
|
1
3
|
import EventEmitter from "events";
|
|
2
4
|
|
|
3
5
|
import { Compiler, Functions } from "simple-terms";
|
|
4
6
|
import merge from "lodash.merge";
|
|
5
7
|
|
|
6
|
-
import { DefaultShaperOptions } from "./defaults.mjs";
|
|
8
|
+
import { DefaultShape, DefaultShaperOptions } from "./defaults.mjs";
|
|
7
9
|
import { augmentDataSpace } from "./helper.mjs";
|
|
8
10
|
import * as TermFunctions from "./term-functions.mjs";
|
|
9
11
|
|
|
@@ -27,16 +29,17 @@ export class Shaper extends EventEmitter {
|
|
|
27
29
|
constructor( options ) {
|
|
28
30
|
super();
|
|
29
31
|
|
|
32
|
+
const compiledOptions = { ...DefaultShaperOptions, ...options };
|
|
33
|
+
|
|
30
34
|
Object.defineProperties( this, {
|
|
31
|
-
options: { value:
|
|
35
|
+
options: { value: compiledOptions, enumerable: true },
|
|
32
36
|
|
|
33
37
|
termsCache: { value: new Map() },
|
|
34
38
|
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
// SECURITY: prevent custom library functions from replacing regular ones by intention
|
|
40
|
+
termFunctions: { value: { ...compiledOptions.library, ...Functions, ...TermFunctions } },
|
|
37
41
|
|
|
38
|
-
|
|
39
|
-
prefix: { value: normalizePathname( this.options.prefix, true ) },
|
|
42
|
+
prefix: { value: normalizePathname( compiledOptions.prefix, true ) },
|
|
40
43
|
} );
|
|
41
44
|
}
|
|
42
45
|
|
|
@@ -44,13 +47,19 @@ export class Shaper extends EventEmitter {
|
|
|
44
47
|
* Creates database from provided collection of records.
|
|
45
48
|
*
|
|
46
49
|
* @param {Map<string,CollectedRecord[]>} recordsCollection collection of records to transform
|
|
50
|
+
* @param {Shape[]} localShapes local shapes of folders records have been collected from
|
|
47
51
|
* @returns {Database} resulting database records
|
|
48
52
|
*/
|
|
49
|
-
transform( recordsCollection ) {
|
|
50
|
-
|
|
53
|
+
transform( recordsCollection, localShapes ) {
|
|
54
|
+
if ( recordsCollection.size > 0 && !localShapes?.length ) {
|
|
55
|
+
throw new Error( "invalid use of Shaper: missing list of local shapes per folder records have been collected from" );
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const mergedLocalShapes = merge( {}, DefaultShape, ...localShapes );
|
|
59
|
+
const models = this.handleContributions( this.groupByModel( recordsCollection ), mergedLocalShapes );
|
|
51
60
|
const endpoints = {};
|
|
52
61
|
|
|
53
|
-
this.populateModels( endpoints, models );
|
|
62
|
+
this.populateModels( endpoints, models, mergedLocalShapes );
|
|
54
63
|
|
|
55
64
|
return { prefix: this.prefix, endpoints };
|
|
56
65
|
}
|
|
@@ -58,44 +67,71 @@ export class Shaper extends EventEmitter {
|
|
|
58
67
|
/**
|
|
59
68
|
* Reorganizes records to by grouped by model.
|
|
60
69
|
*
|
|
61
|
-
* @param {Map<string,CollectedRecord[]>}
|
|
62
|
-
* @returns {Object<string,Object<string,
|
|
70
|
+
* @param {Map<string,CollectedRecord[]>} collectedRecordsByName collection of records to transform
|
|
71
|
+
* @returns {Object<string,Object<string, ModelItem>>} provided records grouped by model and unique item ID
|
|
63
72
|
*/
|
|
64
|
-
groupByModel(
|
|
73
|
+
groupByModel( collectedRecordsByName ) {
|
|
65
74
|
const globalItemsPerModel = {};
|
|
75
|
+
let index = 0;
|
|
76
|
+
|
|
77
|
+
if ( this.options.verbose ) {
|
|
78
|
+
console.error( `shaping records ...` );
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for ( const records of collectedRecordsByName.values() ) {
|
|
82
|
+
if ( this.options.verbose ) {
|
|
83
|
+
process.stderr.write( `\r[${++index}/${collectedRecordsByName.size}]` );
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
this.compileRecords( globalItemsPerModel, records );
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return globalItemsPerModel;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Implements separate stage for computing contributions per model item
|
|
94
|
+
* based on either model's compiled shape.
|
|
95
|
+
*
|
|
96
|
+
* @param {Object<string,Object<string, CollectedRecord[]>>} globalItemsPerModel collections of items per model
|
|
97
|
+
* @param {Shape} mergedLocalShapes local shapes of source folders merged in order of collecting records
|
|
98
|
+
* @returns {Object<string,Object<string, CollectedRecord[]>>} collections of items per model as provided
|
|
99
|
+
*/
|
|
100
|
+
handleContributions( globalItemsPerModel, mergedLocalShapes ) {
|
|
66
101
|
const postProcessingQueue = new Set();
|
|
67
102
|
|
|
68
|
-
|
|
69
|
-
|
|
103
|
+
if ( this.options.verbose ) {
|
|
104
|
+
console.error( `\nprocessing contributions ...` );
|
|
105
|
+
}
|
|
70
106
|
|
|
71
|
-
|
|
107
|
+
this.processContributions( globalItemsPerModel, postProcessingQueue, mergedLocalShapes );
|
|
72
108
|
|
|
73
|
-
|
|
109
|
+
if ( this.options.verbose ) {
|
|
110
|
+
console.error( `post-processing ${postProcessingQueue.size} contributions` );
|
|
74
111
|
}
|
|
75
112
|
|
|
76
|
-
this.
|
|
113
|
+
this.recompileContributionTargets( globalItemsPerModel, postProcessingQueue );
|
|
77
114
|
|
|
78
115
|
return globalItemsPerModel;
|
|
79
116
|
}
|
|
80
117
|
|
|
81
118
|
/**
|
|
82
|
-
* Compiles all items according to collected records
|
|
83
|
-
*
|
|
119
|
+
* Compiles all items according to collected records as provided and collect
|
|
120
|
+
* them grouped by described items' models.
|
|
84
121
|
*
|
|
85
|
-
* @param {
|
|
86
|
-
* @
|
|
122
|
+
* @param {Object<string,Object<string, ModelItem>>} itemsPerModel collections of items per model to be populated
|
|
123
|
+
* @param {CollectedRecord[]} records list of records collected from a source
|
|
124
|
+
* @returns {Object<string,Object<string,ModelItem>>} compiled items grouped by model, addressable by either item's ID
|
|
87
125
|
*/
|
|
88
|
-
|
|
126
|
+
compileRecords( itemsPerModel, records ) {
|
|
89
127
|
const shapesPerModelCache = {};
|
|
90
|
-
const itemsPerModel = {};
|
|
91
128
|
|
|
92
|
-
for ( let read = 0, numRecords =
|
|
93
|
-
const item = this.compileModelItemFromRecord( shapesPerModelCache, itemsPerModel,
|
|
129
|
+
for ( let read = 0, numRecords = records.length; read < numRecords; read++ ) {
|
|
130
|
+
const item = this.compileModelItemFromRecord( shapesPerModelCache, itemsPerModel, records[read] );
|
|
94
131
|
const modelPool = itemsPerModel[item.$model];
|
|
95
132
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
modelPool[item.$id] = modelPool[item.$id] ? merge( modelPool[item.$id], item ) : item;
|
|
133
|
+
if ( item.$variantId == null ) {
|
|
134
|
+
modelPool[item.$id] = this.mergeModelItem( modelPool[item.$id], item );
|
|
99
135
|
} else {
|
|
100
136
|
const target = modelPool[item.$id];
|
|
101
137
|
|
|
@@ -103,7 +139,7 @@ export class Shaper extends EventEmitter {
|
|
|
103
139
|
target.$variants = {};
|
|
104
140
|
}
|
|
105
141
|
|
|
106
|
-
target.$variants[variantId] =
|
|
142
|
+
target.$variants[item.$variantId] = this.mergeModelItem( target.$variants[item.$variantId], item );
|
|
107
143
|
}
|
|
108
144
|
}
|
|
109
145
|
|
|
@@ -130,15 +166,12 @@ export class Shaper extends EventEmitter {
|
|
|
130
166
|
const augmentedData = augmentDataSpace( data, metaProperties );
|
|
131
167
|
|
|
132
168
|
metaProperties.$model = this.detectModel( augmentedData, shape );
|
|
133
|
-
metaProperties.$shape = this.getShapeOfModel(
|
|
169
|
+
metaProperties.$shape = this.getShapeOfModel( {}, shape, metaProperties.$model );
|
|
134
170
|
metaProperties.$id = this.identifyRecord( augmentedData, metaProperties.$shape );
|
|
135
171
|
|
|
136
172
|
if ( !itemsPerModel.hasOwnProperty( metaProperties.$model ) ) {
|
|
137
173
|
// eslint-disable-next-line no-param-reassign
|
|
138
|
-
itemsPerModel[metaProperties.$model] =
|
|
139
|
-
$model: { value: metaProperties.$model },
|
|
140
|
-
$shape: { value: metaProperties.$shape },
|
|
141
|
-
} );
|
|
174
|
+
itemsPerModel[metaProperties.$model] = {};
|
|
142
175
|
}
|
|
143
176
|
|
|
144
177
|
const model = itemsPerModel[metaProperties.$model];
|
|
@@ -147,37 +180,36 @@ export class Shaper extends EventEmitter {
|
|
|
147
180
|
const item = this.compileItem( augmentedData, properties, model[metaProperties.$id] ? {} : defaults );
|
|
148
181
|
|
|
149
182
|
Object.defineProperties( item, {
|
|
150
|
-
$
|
|
151
|
-
$
|
|
183
|
+
$segments: { value: segments },
|
|
184
|
+
$original: { value: data, configurable: true },
|
|
185
|
+
$shape: { value: metaProperties.$shape, configurable: true },
|
|
152
186
|
$model: { value: metaProperties.$model },
|
|
153
187
|
$id: { value: metaProperties.$id },
|
|
154
|
-
$
|
|
155
|
-
$localShape: { value: shape },
|
|
188
|
+
$variantId: { value: this.computeVariantIdOfItem( metaProperties.$shape.properties.$variant, augmentedData ) },
|
|
156
189
|
} );
|
|
157
190
|
|
|
158
191
|
return item;
|
|
159
192
|
}
|
|
160
193
|
|
|
161
194
|
/**
|
|
162
|
-
* Re-compiles provided item this time based item's properties'
|
|
163
|
-
* instead of some record originally collected from a source.
|
|
195
|
+
* Re-compiles provided item this time based on item's properties'
|
|
196
|
+
* themselves instead of some record originally collected from a source.
|
|
164
197
|
*
|
|
165
198
|
* Re-compiling is used to compute data contributed by other models.
|
|
166
199
|
*
|
|
167
|
-
* @param {
|
|
168
|
-
* @returns {
|
|
200
|
+
* @param {ModelItem} item previously compiled item to re-compile
|
|
201
|
+
* @returns {ModelItem} re-compiled item
|
|
169
202
|
*/
|
|
170
203
|
recompileItem( item ) {
|
|
171
|
-
const data = augmentDataSpace( merge( {}, item.$original, item ), { ...item, $
|
|
204
|
+
const data = augmentDataSpace( merge( {}, item.$original, item ), { ...item, $post: true } );
|
|
172
205
|
const updatedItem = this.compileItem( data, item.$shape.properties, {} );
|
|
173
206
|
|
|
174
207
|
Object.defineProperties( updatedItem, {
|
|
175
|
-
$
|
|
176
|
-
$
|
|
208
|
+
$segments: { value: item.$segments },
|
|
209
|
+
$original: { value: item.$original, configurable: true },
|
|
210
|
+
$shape: { value: item.$shape, configurable: true },
|
|
177
211
|
$model: { value: item.$model },
|
|
178
212
|
$id: { value: item.$id },
|
|
179
|
-
$segments: { value: item.$segments },
|
|
180
|
-
$localShape: { value: item.$localShape },
|
|
181
213
|
} );
|
|
182
214
|
|
|
183
215
|
return updatedItem;
|
|
@@ -187,12 +219,11 @@ export class Shaper extends EventEmitter {
|
|
|
187
219
|
* Computes ID of variant provided item is describing. If nullish, the item
|
|
188
220
|
* isn't describing any variant but the base version of the item.
|
|
189
221
|
*
|
|
190
|
-
* @param {
|
|
222
|
+
* @param {string?} term term expressing computation of variant ID
|
|
223
|
+
* @param {ModelItem} item an item to get variant ID for
|
|
191
224
|
* @returns {null|any} computed variant ID of provided item, nullish if item is not a variant
|
|
192
225
|
*/
|
|
193
|
-
computeVariantIdOfItem( item ) {
|
|
194
|
-
const term = item.$shape.variant;
|
|
195
|
-
|
|
226
|
+
computeVariantIdOfItem( term, item ) {
|
|
196
227
|
if ( term == null ) {
|
|
197
228
|
return null;
|
|
198
229
|
}
|
|
@@ -201,7 +232,14 @@ export class Shaper extends EventEmitter {
|
|
|
201
232
|
const data = augmentDataSpace( merge( {}, item.$original, item ), item );
|
|
202
233
|
|
|
203
234
|
try {
|
|
204
|
-
|
|
235
|
+
const value = Compiler.compile( String( term ), this.termFunctions, this.termsCache )( data );
|
|
236
|
+
|
|
237
|
+
// support processed term requesting to fail computation of variant ID
|
|
238
|
+
if ( value instanceof Error ) {
|
|
239
|
+
throw value;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return value;
|
|
205
243
|
} catch ( cause ) {
|
|
206
244
|
throw new Error( `computing variant ID of ${item.$model}#${item.$id} failed: ${cause.message} in ${term}` );
|
|
207
245
|
}
|
|
@@ -218,7 +256,7 @@ export class Shaper extends EventEmitter {
|
|
|
218
256
|
getShapeOfModel( shapesPerModelCache, localShape, modelName ) {
|
|
219
257
|
if ( !shapesPerModelCache.hasOwnProperty( modelName ) ) {
|
|
220
258
|
// eslint-disable-next-line no-param-reassign
|
|
221
|
-
shapesPerModelCache[modelName] = merge( {}, localShape?.common, localShape?.models?.[modelName] );
|
|
259
|
+
shapesPerModelCache[modelName] = merge( { defaults: DefaultShape.common.defaults }, localShape?.common, localShape?.models?.[modelName] );
|
|
222
260
|
}
|
|
223
261
|
|
|
224
262
|
return shapesPerModelCache[modelName];
|
|
@@ -313,9 +351,10 @@ export class Shaper extends EventEmitter {
|
|
|
313
351
|
*
|
|
314
352
|
* @param {Object<string,Object<string,Object>>} itemsPerModel map of model names into either model's map of item IDs into related item
|
|
315
353
|
* @param {Set<string>} contributedItems tracks IDs of items or their variants that have been contributed to
|
|
354
|
+
* @param {Shape} mergedLocalShapes local shapes of source folders merged in order of collecting records
|
|
316
355
|
* @returns {void}
|
|
317
356
|
*/
|
|
318
|
-
processContributions( itemsPerModel, contributedItems ) {
|
|
357
|
+
processContributions( itemsPerModel, contributedItems, mergedLocalShapes ) {
|
|
319
358
|
const contributingModels = this.listContributingModels( itemsPerModel );
|
|
320
359
|
const shapePerModelCache = {};
|
|
321
360
|
|
|
@@ -328,33 +367,47 @@ export class Shaper extends EventEmitter {
|
|
|
328
367
|
continue;
|
|
329
368
|
}
|
|
330
369
|
|
|
331
|
-
for ( const
|
|
332
|
-
const
|
|
370
|
+
for ( const contributedModelName of Object.keys( contributions ) ) {
|
|
371
|
+
const isOptionalContribution = contributedModelName.trim().endsWith( "?" );
|
|
372
|
+
const targetModelName = contributedModelName.replace( /\s*\?\s*$/, "" );
|
|
373
|
+
|
|
374
|
+
const targetModelShape = this.getShapeOfModel( shapePerModelCache, mergedLocalShapes, targetModelName );
|
|
333
375
|
|
|
334
376
|
|
|
335
377
|
// extract contributions to actually declared properties of target model
|
|
336
378
|
const contributionTerms = {};
|
|
379
|
+
const contributionModes = {};
|
|
337
380
|
let warn = false;
|
|
381
|
+
let numActualProperties = 0;
|
|
338
382
|
|
|
339
|
-
for ( const termTargetName of Object.keys( contributions[
|
|
340
|
-
const
|
|
383
|
+
for ( const termTargetName of Object.keys( contributions[contributedModelName] ) ) {
|
|
384
|
+
const match = /^\s*(\S.+?)\s*(\[\])?\s*(\.\.\.|\u2026)?\s*$/u.exec( termTargetName );
|
|
385
|
+
const propertyName = match ? match[1] : termTargetName;
|
|
341
386
|
|
|
342
|
-
if (
|
|
343
|
-
throw new Error( `invalid collecting contribution of record #${item.$id} in model ${item.$model} to property ${propertyName} of model ${targetModelName}` ); // eslint-disable-line max-len
|
|
387
|
+
if ( match && ( match[2] || match[3] ) && propertyName.startsWith( "$" ) ) {
|
|
388
|
+
throw new Error( `invalid collecting or distributed contribution of record #${item.$id} in model ${item.$model} to property ${propertyName} of model ${targetModelName}` ); // eslint-disable-line max-len
|
|
344
389
|
}
|
|
345
390
|
|
|
346
|
-
if ( Object.prototype.hasOwnProperty.call( targetModelShape.properties, propertyName ) ) {
|
|
391
|
+
if ( propertyName === "$variant" || propertyName === "$id" || Object.prototype.hasOwnProperty.call( targetModelShape.properties, propertyName ) ) {
|
|
347
392
|
if ( contributionTerms.hasOwnProperty( propertyName ) ) {
|
|
348
393
|
throw new Error( `double contribution of record #${item.$id} in model ${item.$model} to property ${propertyName} of model ${targetModelName}` ); // eslint-disable-line max-len
|
|
349
394
|
}
|
|
350
395
|
|
|
351
|
-
|
|
396
|
+
if ( !propertyName.startsWith( "$" ) ) {
|
|
397
|
+
numActualProperties++;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
contributionTerms[propertyName] = contributions[contributedModelName][termTargetName];
|
|
401
|
+
contributionModes[propertyName] = {
|
|
402
|
+
$isCollecting: Boolean( match && match[2] ),
|
|
403
|
+
$isDistributed: Boolean( match && match[3] ),
|
|
404
|
+
};
|
|
352
405
|
} else {
|
|
353
406
|
warn = true;
|
|
354
407
|
}
|
|
355
408
|
}
|
|
356
409
|
|
|
357
|
-
if (
|
|
410
|
+
if ( numActualProperties < 1 && !isOptionalContribution ) {
|
|
358
411
|
console.warn( `ignoring eventually empty contribution of record #${item.$id} in model ${item.$model} to model ${targetModelName}` );
|
|
359
412
|
continue;
|
|
360
413
|
}
|
|
@@ -364,46 +417,44 @@ export class Shaper extends EventEmitter {
|
|
|
364
417
|
}
|
|
365
418
|
|
|
366
419
|
|
|
367
|
-
//
|
|
368
|
-
const
|
|
369
|
-
|
|
420
|
+
// pre-compute ID(s) of item(s) to contribute to
|
|
421
|
+
const contributionMeta = {
|
|
422
|
+
$segments: item.$segments,
|
|
423
|
+
$original: item.$original,
|
|
424
|
+
$shape: item.$shape,
|
|
425
|
+
$model: item.$model,
|
|
426
|
+
$id: item.$id,
|
|
427
|
+
};
|
|
370
428
|
|
|
371
|
-
|
|
372
|
-
|
|
429
|
+
const augmentedData = augmentDataSpace( item, contributionMeta );
|
|
430
|
+
const targetId = this.computeId( contributionTerms.$id, augmentedData );
|
|
431
|
+
|
|
432
|
+
if ( !isOptionalContribution && ( !targetId || ( Array.isArray( targetId ) && !targetId.length ) ) ) {
|
|
433
|
+
console.warn( `ignoring contribution of record #${item.$id} in model ${item.$model} to model ${targetModelName} due to lack of target ID` ); // eslint-disable-line max-len
|
|
373
434
|
continue;
|
|
374
435
|
}
|
|
375
436
|
|
|
437
|
+
const targetIds = Array.isArray( targetId ) ? targetId : [targetId];
|
|
376
438
|
|
|
377
|
-
// use contribution data's $id property to pick item of target model to contribute to
|
|
378
|
-
const targetId = contribution.$id;
|
|
379
|
-
if ( !targetId ) {
|
|
380
|
-
throw new Error( `contribution of record #${item.$id} in model ${item.$model} to model ${targetModelName} failed due to lack of ID` );
|
|
381
|
-
}
|
|
382
439
|
|
|
383
|
-
|
|
440
|
+
for ( let index = 0, count = targetIds.length; index < count; index++ ) {
|
|
441
|
+
const id = targetIds[index];
|
|
442
|
+
|
|
443
|
+
// compile contribution per target item
|
|
444
|
+
contributionMeta.$targetid = id;
|
|
445
|
+
contributionMeta.$targetindex = index;
|
|
384
446
|
|
|
385
|
-
// check if this contribution is regarding a variant of selected target item
|
|
386
|
-
let variantId;
|
|
387
447
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
448
|
+
// try computing variant ID based on contributing item
|
|
449
|
+
const variantId = this.computeVariantIdOfItem( contributionTerms.$variant, {
|
|
450
|
+
...item,
|
|
451
|
+
$segments: item.$segments,
|
|
452
|
+
$original: item.$original,
|
|
392
453
|
$shape: targetModelShape,
|
|
393
|
-
$id: targetId,
|
|
394
454
|
$model: targetModelName,
|
|
395
|
-
$
|
|
396
|
-
$segments: item.$segments,
|
|
397
|
-
$localShape: item.$localShape,
|
|
455
|
+
$id: targetId,
|
|
398
456
|
} );
|
|
399
|
-
}
|
|
400
457
|
|
|
401
|
-
if ( variantId == null ) {
|
|
402
|
-
variantId = null;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
for ( const id of targetIds ) {
|
|
407
458
|
// track contributions to selected target item or its variant
|
|
408
459
|
contributedItems.add( JSON.stringify( [ targetModelName, id, variantId ] ) );
|
|
409
460
|
|
|
@@ -413,22 +464,19 @@ export class Shaper extends EventEmitter {
|
|
|
413
464
|
|
|
414
465
|
if ( !itemsPerModel.hasOwnProperty( targetModelName ) ) {
|
|
415
466
|
// eslint-disable-next-line no-param-reassign
|
|
416
|
-
itemsPerModel[targetModelName] =
|
|
417
|
-
$model: { value: targetModelName },
|
|
418
|
-
$shape: { value: targetModelShape },
|
|
419
|
-
} );
|
|
467
|
+
itemsPerModel[targetModelName] = {};
|
|
420
468
|
}
|
|
421
469
|
|
|
422
470
|
const model = itemsPerModel[targetModelName];
|
|
471
|
+
const targetItemExists = model.hasOwnProperty( id );
|
|
423
472
|
|
|
424
|
-
if ( !
|
|
473
|
+
if ( !targetItemExists ) {
|
|
425
474
|
model[id] = Object.defineProperties( {}, {
|
|
426
|
-
$
|
|
427
|
-
$
|
|
475
|
+
$segments: { value: item.$segments },
|
|
476
|
+
$original: { value: {}, configurable: true },
|
|
477
|
+
$shape: { value: targetModelShape, configurable: true },
|
|
428
478
|
$model: { value: targetModelName },
|
|
429
479
|
$id: { value: id },
|
|
430
|
-
$segments: { value: item.$segments },
|
|
431
|
-
$localShape: { value: item.$localShape, configurable: true },
|
|
432
480
|
} );
|
|
433
481
|
}
|
|
434
482
|
|
|
@@ -449,20 +497,31 @@ export class Shaper extends EventEmitter {
|
|
|
449
497
|
}
|
|
450
498
|
|
|
451
499
|
|
|
452
|
-
//
|
|
500
|
+
// compile actual properties of contribution
|
|
501
|
+
const contributionDefaults = targetItemExists || variantId != null ? {} : targetModelShape.defaults;
|
|
502
|
+
const contribution = this.compileItem( augmentedData, contributionTerms, contributionDefaults );
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
// merge this item's contribution with existing
|
|
506
|
+
// properties of selected target using reduced version
|
|
507
|
+
// of merge strategy found in mergeModelItem()
|
|
453
508
|
for ( const name of Object.keys( contribution ) ) {
|
|
454
509
|
if ( String( name ).startsWith( "$" ) ) {
|
|
455
510
|
continue;
|
|
456
511
|
}
|
|
457
512
|
|
|
458
|
-
const
|
|
513
|
+
const { $isCollecting, $isDistributed } = contributionModes[name] || {};
|
|
514
|
+
let value = contribution[name];
|
|
459
515
|
|
|
460
|
-
if (
|
|
461
|
-
// contribution
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
516
|
+
if ( $isDistributed ) {
|
|
517
|
+
// contribution is providing another value for every target item
|
|
518
|
+
if ( Array.isArray( value ) && value.length === targetIds.length ) { // eslint-disable-line max-depth
|
|
519
|
+
value = value[index];
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if ( $isCollecting ) {
|
|
524
|
+
// contribution has been declared with one or more suffices, e.g. []
|
|
466
525
|
// -> collect value in target
|
|
467
526
|
if ( Array.isArray( target[name] ) ) { // eslint-disable-line no-lonely-if,max-depth
|
|
468
527
|
target[name].push( value );
|
|
@@ -471,6 +530,10 @@ export class Shaper extends EventEmitter {
|
|
|
471
530
|
} else {
|
|
472
531
|
target[name] = [ target[name], value ];
|
|
473
532
|
}
|
|
533
|
+
} else {
|
|
534
|
+
// contribution has been declared without any suffix
|
|
535
|
+
// -> do not collect, but replace value in target
|
|
536
|
+
target[name] = value;
|
|
474
537
|
}
|
|
475
538
|
}
|
|
476
539
|
}
|
|
@@ -479,14 +542,75 @@ export class Shaper extends EventEmitter {
|
|
|
479
542
|
}
|
|
480
543
|
}
|
|
481
544
|
|
|
545
|
+
/**
|
|
546
|
+
* Deeply merges provided source item into given target item including their
|
|
547
|
+
* attached model shape and original data.
|
|
548
|
+
*
|
|
549
|
+
* @param {ModelItem} target model item to adjust by merging
|
|
550
|
+
* @param {ModelItem} source model item to merge into provided target
|
|
551
|
+
* @returns {ModelItem} provided target
|
|
552
|
+
*/
|
|
553
|
+
mergeModelItem( target, source ) {
|
|
554
|
+
const _to = target || {};
|
|
555
|
+
const oldNames = target ? Object.keys( _to.$shape.properties ) : [];
|
|
556
|
+
const newNames = Object.keys( source.$shape.properties );
|
|
557
|
+
|
|
558
|
+
// merge properties of items
|
|
559
|
+
for ( const name of Object.keys( source ) ) {
|
|
560
|
+
if ( !String( name ).startsWith( "$" ) ) {
|
|
561
|
+
const wasCollecting = oldNames.some( n => n.replace( /\s*\[]$/, "" ).trim() === name );
|
|
562
|
+
const isCollecting = newNames.some( n => n.replace( /\s*\[]$/, "" ).trim() === name );
|
|
563
|
+
|
|
564
|
+
if ( _to.hasOwnProperty( name ) ) {
|
|
565
|
+
if ( isCollecting ) {
|
|
566
|
+
if ( wasCollecting && Array.isArray( _to[name] ) ) {
|
|
567
|
+
_to[name].splice( _to[name].length, 0, ...source[name] );
|
|
568
|
+
} else if ( _to[name] == null ) {
|
|
569
|
+
_to[name] = source[name];
|
|
570
|
+
} else {
|
|
571
|
+
_to[name] = [ _to[name], ...source[name] ];
|
|
572
|
+
}
|
|
573
|
+
} else {
|
|
574
|
+
_to[name] = source[name];
|
|
575
|
+
}
|
|
576
|
+
} else {
|
|
577
|
+
_to[name] = source[name];
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// assure some freshly created target has all common meta properties
|
|
583
|
+
if ( !target ) {
|
|
584
|
+
Object.defineProperties( _to, {
|
|
585
|
+
$segments: { value: source.$segments },
|
|
586
|
+
$model: { value: source.$model },
|
|
587
|
+
$id: { value: source.$id },
|
|
588
|
+
} );
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// merge some complex meta properties
|
|
592
|
+
Object.defineProperties( _to, {
|
|
593
|
+
$shape: {
|
|
594
|
+
value: merge( _to.$shape || {}, source.$shape ),
|
|
595
|
+
configurable: true,
|
|
596
|
+
},
|
|
597
|
+
$original: {
|
|
598
|
+
value: merge( _to.$original || {}, source.$original ),
|
|
599
|
+
configurable: true,
|
|
600
|
+
},
|
|
601
|
+
} );
|
|
602
|
+
|
|
603
|
+
return _to;
|
|
604
|
+
}
|
|
605
|
+
|
|
482
606
|
/**
|
|
483
607
|
* Re-compiles all items in provided pool that have been contributed to.
|
|
484
608
|
*
|
|
485
|
-
* @param {Object<string,Object<string,
|
|
609
|
+
* @param {Object<string,Object<string,ModelItem>>} itemsPerModel pool of items per model to update
|
|
486
610
|
* @param {Set<string>} contributions lists IDs of items or their variants that have been contributed to
|
|
487
|
-
* @returns {Object<string,Object<string,
|
|
611
|
+
* @returns {Object<string,Object<string,ModelItem>>} pool of items per model as provided
|
|
488
612
|
*/
|
|
489
|
-
|
|
613
|
+
recompileContributionTargets( itemsPerModel, contributions ) {
|
|
490
614
|
for ( const track of contributions.values() ) {
|
|
491
615
|
const [ model, id, variantId ] = JSON.parse( track );
|
|
492
616
|
|
|
@@ -515,7 +639,7 @@ export class Shaper extends EventEmitter {
|
|
|
515
639
|
const variants = item.$variants;
|
|
516
640
|
|
|
517
641
|
if ( !variants.hasOwnProperty( variantId ) ) {
|
|
518
|
-
console.warn( `inconsistency: missing variant
|
|
642
|
+
console.warn( `inconsistency: missing variant #${variantId} of item #${id} of model ${model} though contributions have been tracked` );
|
|
519
643
|
continue;
|
|
520
644
|
}
|
|
521
645
|
|
|
@@ -526,96 +650,6 @@ export class Shaper extends EventEmitter {
|
|
|
526
650
|
return itemsPerModel;
|
|
527
651
|
}
|
|
528
652
|
|
|
529
|
-
/**
|
|
530
|
-
* Merges items collected per model from a single source into provided
|
|
531
|
-
* target collection which is covering multiple possible sources.
|
|
532
|
-
*
|
|
533
|
-
* @param {Object<string,Object<string,Object>>} target collection of items per model to be augmented by provided source collection
|
|
534
|
-
* @param {Object<string,Object<string,Object>>} source collection items per model to merge into target
|
|
535
|
-
* @returns {void}
|
|
536
|
-
*/
|
|
537
|
-
mergeItemsPerModel( target, source ) {
|
|
538
|
-
for ( const modelName of Object.keys( source ) ) {
|
|
539
|
-
const sourcePool = source[modelName];
|
|
540
|
-
|
|
541
|
-
if ( target.hasOwnProperty( modelName ) ) {
|
|
542
|
-
// blend shapes of pools claiming to refer to same model
|
|
543
|
-
const mergedShape = merge( {}, target[modelName].$shape, sourcePool.$shape );
|
|
544
|
-
|
|
545
|
-
Object.defineProperties( target[modelName], {
|
|
546
|
-
$shape: { value: mergedShape, configurable: true },
|
|
547
|
-
} );
|
|
548
|
-
} else {
|
|
549
|
-
// create pool of items for target model as it hasn't been populated before
|
|
550
|
-
// eslint-disable-next-line no-param-reassign
|
|
551
|
-
target[modelName] = Object.defineProperties( {}, {
|
|
552
|
-
$model: { value: sourcePool.$model },
|
|
553
|
-
$shape: { value: sourcePool.$shape, configurable: true },
|
|
554
|
-
} );
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
const targetPool = target[modelName];
|
|
558
|
-
|
|
559
|
-
for ( const itemId of Object.keys( sourcePool ) ) {
|
|
560
|
-
if ( targetPool.hasOwnProperty( itemId ) ) {
|
|
561
|
-
const from = sourcePool[itemId];
|
|
562
|
-
const to = targetPool[itemId];
|
|
563
|
-
|
|
564
|
-
targetPool[itemId] = this.mergeItem( to, from );
|
|
565
|
-
|
|
566
|
-
if ( from.$variants ) {
|
|
567
|
-
if ( !to.$variants ) {
|
|
568
|
-
to.$variants = {};
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
for ( const variantId of Object.keys( from.$variants ) ) {
|
|
572
|
-
to.$variants[variantId] = this.mergeItem( to.$variants[variantId] || {}, from.$variants[variantId] );
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
} else {
|
|
576
|
-
targetPool[itemId] = sourcePool[itemId];
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
/**
|
|
583
|
-
* Updates a given set of properties blending it with another set of item
|
|
584
|
-
* properties.
|
|
585
|
-
*
|
|
586
|
-
* @param {object} to target object to merge properties of source into
|
|
587
|
-
* @param {object} from properties to merge into target
|
|
588
|
-
* @returns {object} provided target object, or new object if provided target was falsy
|
|
589
|
-
*/
|
|
590
|
-
mergeItem( to, from ) {
|
|
591
|
-
const _to = to || {};
|
|
592
|
-
|
|
593
|
-
for ( const name of Object.keys( from ) ) {
|
|
594
|
-
if ( !String( name ).startsWith( "$" ) ) {
|
|
595
|
-
// TODO reconsider this merging strategy for being beneficial
|
|
596
|
-
// they way it is by now (e.g. for it keeps nullish data on
|
|
597
|
-
// either end)
|
|
598
|
-
if ( Array.isArray( from[name] ) ) {
|
|
599
|
-
if ( Array.isArray( _to[name] ) ) {
|
|
600
|
-
_to[name].splice( _to[name].length, 0, ...from[name] );
|
|
601
|
-
} else if ( _to.hasOwnProperty( name ) ) {
|
|
602
|
-
_to[name] = [ _to[name], ...from[name] ];
|
|
603
|
-
} else {
|
|
604
|
-
_to[name] = [...from[name]];
|
|
605
|
-
}
|
|
606
|
-
} else if ( Array.isArray( _to[name] ) ) {
|
|
607
|
-
if ( from.hasOwnProperty( name ) ) {
|
|
608
|
-
_to[name].push( from[name] );
|
|
609
|
-
}
|
|
610
|
-
} else {
|
|
611
|
-
_to[name] = from[name];
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
return _to;
|
|
617
|
-
}
|
|
618
|
-
|
|
619
653
|
/**
|
|
620
654
|
* Detects model of a record.
|
|
621
655
|
*
|
|
@@ -632,6 +666,11 @@ export class Shaper extends EventEmitter {
|
|
|
632
666
|
|
|
633
667
|
const name = Compiler.compile( selector, this.termFunctions, this.termsCache )( data );
|
|
634
668
|
|
|
669
|
+
// support processed term requesting to fail model detection
|
|
670
|
+
if ( name instanceof Error ) {
|
|
671
|
+
throw new Error( `detecting model of record failed: ${name.message} in context of ${JSON.stringify( data )}` );
|
|
672
|
+
}
|
|
673
|
+
|
|
635
674
|
if ( !name ) {
|
|
636
675
|
throw new Error( `detecting model of record failed: ${JSON.stringify( data )}` );
|
|
637
676
|
}
|
|
@@ -647,13 +686,7 @@ export class Shaper extends EventEmitter {
|
|
|
647
686
|
* @returns {string} ID of record
|
|
648
687
|
*/
|
|
649
688
|
identifyRecord( data, modelShape ) {
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
if ( !selector || typeof selector !== "string" ) {
|
|
653
|
-
throw new Error( `missing or invalid definition of $id in shape of ${data.$segments.join( "/" )}` );
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
const id = Compiler.compile( selector )( data );
|
|
689
|
+
const id = this.computeId( modelShape?.properties?.$id, data );
|
|
657
690
|
|
|
658
691
|
if ( !id ) {
|
|
659
692
|
throw new Error( `detecting ID of item related to record ${data.$segments.join( "/" )} failed` );
|
|
@@ -676,35 +709,62 @@ export class Shaper extends EventEmitter {
|
|
|
676
709
|
|
|
677
710
|
for ( const property of Object.keys( properties ) ) {
|
|
678
711
|
if ( accept.indexOf( property ) > -1 || !String( property ).startsWith( "$" ) ) {
|
|
712
|
+
const propertyName = property.replace( /\s*\[]$/, "" );
|
|
713
|
+
const isCollecting = property !== propertyName;
|
|
714
|
+
|
|
679
715
|
const term = properties[property];
|
|
680
716
|
if ( typeof term !== "string" || term.trim().length === 0 ) {
|
|
681
|
-
throw new Error( `invalid or missing term for computing value of property ${data.$model}
|
|
717
|
+
throw new Error( `invalid or missing term for computing value of property ${propertyName} of ${data.$model}` );
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
if ( isCollecting && properties.hasOwnProperty( propertyName ) ) {
|
|
721
|
+
throw new Error( `double definition: property ${propertyName} of ${data.$model} must be either regular or collecting property` );
|
|
682
722
|
}
|
|
683
723
|
|
|
684
724
|
let value;
|
|
685
725
|
|
|
686
726
|
try {
|
|
687
727
|
value = Compiler.compile( term, this.termFunctions, this.termsCache )( data );
|
|
728
|
+
|
|
729
|
+
// support processed term requesting to fail computation of property's value
|
|
730
|
+
if ( value instanceof Error ) {
|
|
731
|
+
throw value;
|
|
732
|
+
}
|
|
688
733
|
} catch ( cause ) {
|
|
689
|
-
throw new Error( `fetching value of property ${data.$model}
|
|
734
|
+
throw new Error( `fetching value of property ${propertyName} of ${data.$model} failed: ${cause.message} in ${term}` );
|
|
690
735
|
}
|
|
691
736
|
|
|
692
737
|
if ( value == null ) {
|
|
693
|
-
if ( defaults.hasOwnProperty( property ) ) {
|
|
694
|
-
const defaultValue = defaults[property];
|
|
738
|
+
if ( defaults.hasOwnProperty( propertyName ) || defaults.hasOwnProperty( property ) ) {
|
|
739
|
+
const defaultValue = defaults.hasOwnProperty( propertyName ) ? defaults[propertyName] : defaults[property];
|
|
695
740
|
const trimmed = typeof defaultValue === "string" ? defaultValue.trim() : "";
|
|
696
741
|
|
|
697
742
|
try {
|
|
698
743
|
const code = trimmed.startsWith( "=" ) ? trimmed.slice( 1 ) : undefined;
|
|
699
744
|
value = code ? Compiler.compile( code, this.termFunctions, this.termsCache )( data ) : defaultValue;
|
|
745
|
+
|
|
746
|
+
// support processed term requesting to fail computation of default value
|
|
747
|
+
if ( value instanceof Error ) {
|
|
748
|
+
throw value;
|
|
749
|
+
}
|
|
700
750
|
} catch ( cause ) {
|
|
701
|
-
throw new Error( `fetching default of property ${data.$model}
|
|
751
|
+
throw new Error( `fetching default of property ${propertyName} of ${data.$model} failed: "${cause.message}" in "${defaultValue}"` );
|
|
702
752
|
}
|
|
703
753
|
}
|
|
704
754
|
}
|
|
705
755
|
|
|
706
756
|
if ( value != null ) {
|
|
707
|
-
|
|
757
|
+
if ( isCollecting ) {
|
|
758
|
+
if ( Array.isArray( item[propertyName] ) ) { // eslint-disable-line no-lonely-if
|
|
759
|
+
item[propertyName].push( value );
|
|
760
|
+
} else if ( item[propertyName] == null ) {
|
|
761
|
+
item[propertyName] = [value];
|
|
762
|
+
} else {
|
|
763
|
+
item[propertyName] = [ item[propertyName], value ];
|
|
764
|
+
}
|
|
765
|
+
} else {
|
|
766
|
+
item[propertyName] = value;
|
|
767
|
+
}
|
|
708
768
|
}
|
|
709
769
|
}
|
|
710
770
|
}
|
|
@@ -714,18 +774,57 @@ export class Shaper extends EventEmitter {
|
|
|
714
774
|
return merge( {}, item );
|
|
715
775
|
}
|
|
716
776
|
|
|
777
|
+
/**
|
|
778
|
+
* Computes ID using provided term in context of given data space.
|
|
779
|
+
*
|
|
780
|
+
* @param {string} term term to process
|
|
781
|
+
* @param {object} data data space available to term processing
|
|
782
|
+
* @returns {any} processed term's result
|
|
783
|
+
*/
|
|
784
|
+
computeId( term, data ) {
|
|
785
|
+
if ( !term || typeof term !== "string" ) {
|
|
786
|
+
throw new Error( `missing or invalid term for computing ID of ${data.$segments.join( "/" )}` );
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
try {
|
|
790
|
+
const value = Compiler.compile( term, this.termFunctions, this.termsCache )( data );
|
|
791
|
+
|
|
792
|
+
// support processed term requesting to fail computation of ID
|
|
793
|
+
if ( value instanceof Error ) {
|
|
794
|
+
throw value;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
return value;
|
|
798
|
+
} catch ( cause ) {
|
|
799
|
+
throw new Error( `computing ID failed: ${cause.message} in ${term}` );
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
717
803
|
/**
|
|
718
804
|
* Populates pool of endpoints with collected records of discovered models.
|
|
719
805
|
*
|
|
720
806
|
* @param {DatabaseEndpoints} endpoints map of routes into data sets to expose eventually
|
|
721
|
-
* @param {Object<string,Object<string,
|
|
807
|
+
* @param {Object<string,Object<string,ModelItem>>} models pool of items per discovered model
|
|
808
|
+
* @param {Shape} mergedLocalShapes local shapes of source folders merged in order of collecting records
|
|
722
809
|
* @returns {DatabaseEndpoints} map of routes into data sets to expose eventually
|
|
723
810
|
*/
|
|
724
|
-
populateModels( endpoints, models ) {
|
|
725
|
-
|
|
811
|
+
populateModels( endpoints, models, mergedLocalShapes ) {
|
|
812
|
+
const modelNames = Object.keys( models );
|
|
813
|
+
let index = 0;
|
|
814
|
+
|
|
815
|
+
for ( const modelName of modelNames ) {
|
|
726
816
|
const model = models[modelName];
|
|
817
|
+
const itemIds = Object.keys( model );
|
|
727
818
|
|
|
728
|
-
|
|
819
|
+
if ( this.options.verbose ) {
|
|
820
|
+
console.error( `creating endpoints for model ${modelName} [${++index}/${modelNames.length}] ...` );
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
if ( this.options.verbose ) {
|
|
824
|
+
console.error( ` - ${itemIds.length} items ...` );
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
for ( const itemId of itemIds ) {
|
|
729
828
|
const normalized = normalizePathname( `${this.prefix}${modelName}/item/${itemId}` );
|
|
730
829
|
if ( normalized.endsWith( "/" ) ) {
|
|
731
830
|
throw new Error( `item ID "${itemId}" of model "${modelName}" results in invalid route for endpoint exposing it` );
|
|
@@ -734,7 +833,7 @@ export class Shaper extends EventEmitter {
|
|
|
734
833
|
endpoints[normalized] = model[itemId]; // eslint-disable-line no-param-reassign
|
|
735
834
|
}
|
|
736
835
|
|
|
737
|
-
this.populateModelCollections( endpoints, models, modelName );
|
|
836
|
+
this.populateModelCollections( endpoints, models, modelName, mergedLocalShapes );
|
|
738
837
|
}
|
|
739
838
|
|
|
740
839
|
return endpoints;
|
|
@@ -745,13 +844,14 @@ export class Shaper extends EventEmitter {
|
|
|
745
844
|
* a model.
|
|
746
845
|
*
|
|
747
846
|
* @param {DatabaseEndpoints} endpoints map of routes into data sets to expose eventually
|
|
748
|
-
* @param {Object<string,Object<string,
|
|
847
|
+
* @param {Object<string,Object<string,ModelItem>>} models pool of items per discovered model
|
|
749
848
|
* @param {string} modelName name of model to process
|
|
849
|
+
* @param {Shape} mergedLocalShapes local shapes of source folders merged in order of collecting records
|
|
750
850
|
* @returns {DatabaseEndpoints} map of routes into data sets to expose eventually
|
|
751
851
|
*/
|
|
752
|
-
populateModelCollections( endpoints, models, modelName ) {
|
|
753
|
-
const
|
|
754
|
-
const collections =
|
|
852
|
+
populateModelCollections( endpoints, models, modelName, mergedLocalShapes ) {
|
|
853
|
+
const finalModelShape = merge( {}, mergedLocalShapes.common, mergedLocalShapes.models[modelName] );
|
|
854
|
+
const collections = finalModelShape.collections || {};
|
|
755
855
|
const cache = {};
|
|
756
856
|
|
|
757
857
|
// prepare to process collection instances prior to collection references
|
|
@@ -770,9 +870,15 @@ export class Shaper extends EventEmitter {
|
|
|
770
870
|
} );
|
|
771
871
|
|
|
772
872
|
// process collection by collection
|
|
873
|
+
let index = 0;
|
|
874
|
+
|
|
773
875
|
for ( const route of keys ) {
|
|
774
876
|
const normalized = normalizePathname( route );
|
|
775
877
|
|
|
878
|
+
if ( this.options.verbose ) {
|
|
879
|
+
console.error( ` - collection /${normalized} [${++index}/${keys.length}] ...` );
|
|
880
|
+
}
|
|
881
|
+
|
|
776
882
|
if ( String( normalized ).startsWith( "item/" ) || /(^|\/)\.\.(\/|$)|[\\%]/.test( normalized ) ) {
|
|
777
883
|
throw new Error( `invalid custom route "${normalized}" for collection of model "${modelName}"` );
|
|
778
884
|
}
|
|
@@ -892,15 +998,15 @@ export class Shaper extends EventEmitter {
|
|
|
892
998
|
* Creates managed data space for evaluating terms in context of a set of
|
|
893
999
|
* items.
|
|
894
1000
|
*
|
|
895
|
-
* @param {Object<string,Object<string,
|
|
1001
|
+
* @param {Object<string,Object<string,ModelItem>>} models pool of items per discovered model
|
|
896
1002
|
* @param {string} modelName name of model to process
|
|
897
1003
|
* @param {ShapeCollection} collectionDefinition rules controlling compilation of collection
|
|
898
|
-
* @returns {{handler: {get: ((function(*, *=): (*))|*)}, context: {$
|
|
1004
|
+
* @returns {{handler: {get: ((function(*, *=): (*))|*)}, context: {$database, $model, $collection}}} handler and extensible context for term evaluation
|
|
899
1005
|
*/
|
|
900
1006
|
createTermContextCollection( models, modelName, collectionDefinition ) {
|
|
901
1007
|
const context = {
|
|
902
1008
|
$collection: collectionDefinition,
|
|
903
|
-
$
|
|
1009
|
+
$database: models,
|
|
904
1010
|
$model: modelName,
|
|
905
1011
|
};
|
|
906
1012
|
|
|
@@ -920,7 +1026,7 @@ export class Shaper extends EventEmitter {
|
|
|
920
1026
|
/**
|
|
921
1027
|
* Compiles collection according to its definition in provided shape.
|
|
922
1028
|
*
|
|
923
|
-
* @param {Object<string,Object<string,
|
|
1029
|
+
* @param {Object<string,Object<string,ModelItem>>} models pool of items per discovered model
|
|
924
1030
|
* @param {string} modelName name of model to process
|
|
925
1031
|
* @param {ShapeCollection} definition rules controlling compilation of collection
|
|
926
1032
|
* @param {string} route route for resulting collection
|
|
@@ -943,12 +1049,22 @@ export class Shaper extends EventEmitter {
|
|
|
943
1049
|
|
|
944
1050
|
try {
|
|
945
1051
|
keyFn = isKeyed ? Compiler.compile( $key, this.termFunctions, this.termsCache ) : undefined;
|
|
1052
|
+
|
|
1053
|
+
// support processed term requesting to fail
|
|
1054
|
+
if ( keyFn instanceof Error ) {
|
|
1055
|
+
throw keyFn;
|
|
1056
|
+
}
|
|
946
1057
|
} catch ( cause ) {
|
|
947
1058
|
throw new Error( `invalid rule for defining key of collection at "${route}" in ${modelName}: ${cause.message}` );
|
|
948
1059
|
}
|
|
949
1060
|
|
|
950
1061
|
try {
|
|
951
1062
|
filterFn = $filter && typeof $filter === "string" ? Compiler.compile( $filter, this.termFunctions, this.termsCache ) : undefined;
|
|
1063
|
+
|
|
1064
|
+
// support processed term requesting to fail
|
|
1065
|
+
if ( filterFn instanceof Error ) {
|
|
1066
|
+
throw filterFn;
|
|
1067
|
+
}
|
|
952
1068
|
} catch ( cause ) {
|
|
953
1069
|
throw new Error( `invalid rule for defining filter of collection at "${route}" in ${modelName}: ${cause.message}` );
|
|
954
1070
|
}
|
|
@@ -960,6 +1076,11 @@ export class Shaper extends EventEmitter {
|
|
|
960
1076
|
if ( code && typeof code === "string" ) {
|
|
961
1077
|
const fn = Compiler.compile( code, this.termFunctions, this.termsCache );
|
|
962
1078
|
|
|
1079
|
+
// support processed term requesting to fail computed definition of collection
|
|
1080
|
+
if ( fn instanceof Error ) {
|
|
1081
|
+
throw fn;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
963
1084
|
fn.target = target;
|
|
964
1085
|
|
|
965
1086
|
return fn;
|
|
@@ -1030,7 +1151,7 @@ export class Shaper extends EventEmitter {
|
|
|
1030
1151
|
/**
|
|
1031
1152
|
* Sorts provided collection either in-place or as a copy.
|
|
1032
1153
|
*
|
|
1033
|
-
* @param {Object<string,Object<string,
|
|
1154
|
+
* @param {Object<string,Object<string,ModelItem>>} models pool of items per discovered model
|
|
1034
1155
|
* @param {string} modelName name of model to process
|
|
1035
1156
|
* @param {ShapeCollection} collectionDefinition rules controlling compilation of collection
|
|
1036
1157
|
* @param {string} route route for resulting collection
|
|
@@ -1044,6 +1165,11 @@ export class Shaper extends EventEmitter {
|
|
|
1044
1165
|
|
|
1045
1166
|
try {
|
|
1046
1167
|
$sort = $sort && typeof $sort === "string" ? Compiler.compile( $sort, this.termFunctions, this.termsCache ) : $sort?.property ? $sort : undefined;
|
|
1168
|
+
|
|
1169
|
+
// support processed term requesting to fail
|
|
1170
|
+
if ( $sort instanceof Error ) {
|
|
1171
|
+
throw $sort;
|
|
1172
|
+
}
|
|
1047
1173
|
} catch ( cause ) {
|
|
1048
1174
|
throw new Error( `invalid rule for defining sorting of collection at "${route}" in ${modelName}: ${cause.message}` );
|
|
1049
1175
|
}
|