@cepharum/concrete-db 0.1.1 → 0.1.2-alpha.1
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/LICENSE +1 -1
- package/concrete-db.d.ts +54 -0
- package/cure.mjs +2 -29
- package/lib/collector.mjs +10 -35
- package/lib/cure.mjs +0 -28
- package/lib/{default.shape.yml → default.shape.yaml} +0 -0
- package/lib/defaults.mjs +2 -30
- package/lib/generator.mjs +0 -28
- package/lib/helper.mjs +7 -32
- package/lib/index.mjs +0 -28
- package/lib/meta.cjs +0 -28
- package/lib/options.mjs +3 -0
- package/lib/shaper.mjs +584 -102
- package/lib/term-functions.mjs +73 -28
- package/package.json +16 -12
package/lib/shaper.mjs
CHANGED
|
@@ -1,38 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* (c) 2021 cepharum GmbH, Berlin, http://cepharum.de
|
|
3
|
-
*
|
|
4
|
-
* The MIT License (MIT)
|
|
5
|
-
*
|
|
6
|
-
* Copyright (c) 2021 cepharum GmbH
|
|
7
|
-
*
|
|
8
|
-
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
9
|
-
* of this software and associated documentation files (the "Software"), to deal
|
|
10
|
-
* in the Software without restriction, including without limitation the rights
|
|
11
|
-
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
12
|
-
* copies of the Software, and to permit persons to whom the Software is
|
|
13
|
-
* furnished to do so, subject to the following conditions:
|
|
14
|
-
*
|
|
15
|
-
* The above copyright notice and this permission notice shall be included in all
|
|
16
|
-
* copies or substantial portions of the Software.
|
|
17
|
-
*
|
|
18
|
-
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
19
|
-
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
20
|
-
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
21
|
-
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
22
|
-
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
23
|
-
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
24
|
-
* SOFTWARE.
|
|
25
|
-
*
|
|
26
|
-
* @author: cepharum
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
1
|
import EventEmitter from "events";
|
|
30
2
|
|
|
31
3
|
import { Compiler, Functions } from "simple-terms";
|
|
32
4
|
import merge from "lodash.merge";
|
|
33
5
|
|
|
34
6
|
import { DefaultShaperOptions } from "./defaults.mjs";
|
|
35
|
-
import {
|
|
7
|
+
import { augmentDataSpace } from "./helper.mjs";
|
|
36
8
|
import * as TermFunctions from "./term-functions.mjs";
|
|
37
9
|
|
|
38
10
|
|
|
@@ -87,61 +59,561 @@ export class Shaper extends EventEmitter {
|
|
|
87
59
|
* Reorganizes records to by grouped by model.
|
|
88
60
|
*
|
|
89
61
|
* @param {Map<string,CollectedRecord[]>} collection collection of records to transform
|
|
90
|
-
* @returns {
|
|
62
|
+
* @returns {Object<string,Object<string, CollectedRecord[]>>} provided records grouped by model and unique item ID
|
|
91
63
|
*/
|
|
92
64
|
groupByModel( collection ) {
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
for ( const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
65
|
+
const globalItemsPerModel = {};
|
|
66
|
+
const postProcessingQueue = new Set();
|
|
67
|
+
|
|
68
|
+
for ( const source of collection.values() ) {
|
|
69
|
+
const localItemsPerModel = this.compileModelsOfSource( source );
|
|
70
|
+
|
|
71
|
+
this.processContributions( localItemsPerModel, postProcessingQueue );
|
|
72
|
+
|
|
73
|
+
this.mergeItemsPerModel( globalItemsPerModel, localItemsPerModel );
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
this.postProcessContributions( globalItemsPerModel, postProcessingQueue );
|
|
77
|
+
|
|
78
|
+
return globalItemsPerModel;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Compiles all items according to collected records of provided source and
|
|
83
|
+
* collect them grouped by items' model.
|
|
84
|
+
*
|
|
85
|
+
* @param {CollectedRecord[]} source list of records collected from a source
|
|
86
|
+
* @returns {Object<string,Object<string,Object>>} compiled items grouped by model, addressable by either item's ID
|
|
87
|
+
*/
|
|
88
|
+
compileModelsOfSource( source ) {
|
|
89
|
+
const shapesPerModelCache = {};
|
|
90
|
+
const itemsPerModel = {};
|
|
91
|
+
|
|
92
|
+
for ( let read = 0, numRecords = source.length; read < numRecords; read++ ) {
|
|
93
|
+
const item = this.compileModelItemFromRecord( shapesPerModelCache, itemsPerModel, source[read] );
|
|
94
|
+
const modelPool = itemsPerModel[item.$model];
|
|
95
|
+
|
|
96
|
+
const variantId = this.computeVariantIdOfItem( item );
|
|
97
|
+
if ( variantId == null ) {
|
|
98
|
+
modelPool[item.$id] = modelPool[item.$id] ? merge( modelPool[item.$id], item ) : item;
|
|
99
|
+
} else {
|
|
100
|
+
const target = modelPool[item.$id];
|
|
101
|
+
|
|
102
|
+
if ( !target.$variants ) {
|
|
103
|
+
target.$variants = {};
|
|
109
104
|
}
|
|
110
105
|
|
|
111
|
-
|
|
112
|
-
|
|
106
|
+
target.$variants[variantId] = target.$variants[variantId] ? merge( target.$variants[variantId], item ) : item;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return itemsPerModel;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Compiles item of model from a provided source record. This includes
|
|
115
|
+
* detecting model and selected ID of resulting item from provided record
|
|
116
|
+
* based on record's shape.
|
|
117
|
+
*
|
|
118
|
+
* @param {Object<string,ShapeModel>} shapesPerModelCache caches per-model shapes compiled before
|
|
119
|
+
* @param {Object<string,Object<string,Object>>} itemsPerModel collection of previously compiled items per model
|
|
120
|
+
* @param {CollectedRecord} record augmented data collected from a source to be compiled
|
|
121
|
+
* @returns {object} instance of model with hidden meta information injected
|
|
122
|
+
*/
|
|
123
|
+
compileModelItemFromRecord( shapesPerModelCache, itemsPerModel, record ) {
|
|
124
|
+
const { segments, shape, data } = record;
|
|
113
125
|
|
|
114
|
-
|
|
115
|
-
|
|
126
|
+
const metaProperties = {
|
|
127
|
+
$segments: segments
|
|
128
|
+
};
|
|
116
129
|
|
|
117
|
-
|
|
118
|
-
if ( !model ) {
|
|
119
|
-
model = models[modelName] = {};
|
|
130
|
+
const augmentedData = augmentDataSpace( data, metaProperties );
|
|
120
131
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
132
|
+
metaProperties.$model = this.detectModel( augmentedData, shape );
|
|
133
|
+
metaProperties.$shape = this.getShapeOfModel( shapesPerModelCache, shape, metaProperties.$model );
|
|
134
|
+
metaProperties.$id = this.identifyRecord( augmentedData, metaProperties.$shape );
|
|
135
|
+
|
|
136
|
+
if ( !itemsPerModel.hasOwnProperty( metaProperties.$model ) ) {
|
|
137
|
+
// eslint-disable-next-line no-param-reassign
|
|
138
|
+
itemsPerModel[metaProperties.$model] = Object.defineProperties( {}, {
|
|
139
|
+
$model: { value: metaProperties.$model },
|
|
140
|
+
$shape: { value: metaProperties.$shape },
|
|
141
|
+
} );
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const model = itemsPerModel[metaProperties.$model];
|
|
145
|
+
|
|
146
|
+
const { properties, defaults } = metaProperties.$shape;
|
|
147
|
+
const item = this.compileItem( augmentedData, properties, model[metaProperties.$id] ? {} : defaults );
|
|
148
|
+
|
|
149
|
+
Object.defineProperties( item, {
|
|
150
|
+
$original: { value: data },
|
|
151
|
+
$shape: { value: metaProperties.$shape },
|
|
152
|
+
$model: { value: metaProperties.$model },
|
|
153
|
+
$id: { value: metaProperties.$id },
|
|
154
|
+
$segments: { value: segments },
|
|
155
|
+
$localShape: { value: shape },
|
|
156
|
+
} );
|
|
157
|
+
|
|
158
|
+
return item;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Re-compiles provided item this time based item's properties' themselves
|
|
163
|
+
* instead of some record originally collected from a source.
|
|
164
|
+
*
|
|
165
|
+
* Re-compiling is used to compute data contributed by other models.
|
|
166
|
+
*
|
|
167
|
+
* @param {CollectedItem} item previously compiled item to re-compile
|
|
168
|
+
* @returns {CollectedItem} re-compiled item
|
|
169
|
+
*/
|
|
170
|
+
recompileItem( item ) {
|
|
171
|
+
const data = augmentDataSpace( merge( {}, item.$original, item ), { ...item, $postcontribution: true } );
|
|
172
|
+
const updatedItem = this.compileItem( data, item.$shape.properties, {} );
|
|
173
|
+
|
|
174
|
+
Object.defineProperties( updatedItem, {
|
|
175
|
+
$original: { value: item.$original },
|
|
176
|
+
$shape: { value: item.$shape },
|
|
177
|
+
$model: { value: item.$model },
|
|
178
|
+
$id: { value: item.$id },
|
|
179
|
+
$segments: { value: item.$segments },
|
|
180
|
+
$localShape: { value: item.$localShape },
|
|
181
|
+
} );
|
|
182
|
+
|
|
183
|
+
return updatedItem;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Computes ID of variant provided item is describing. If nullish, the item
|
|
188
|
+
* isn't describing any variant but the base version of the item.
|
|
189
|
+
*
|
|
190
|
+
* @param {CollectedItem} item an item to get variant ID for
|
|
191
|
+
* @returns {null|any} computed variant ID of provided item, nullish if item is not a variant
|
|
192
|
+
*/
|
|
193
|
+
computeVariantIdOfItem( item ) {
|
|
194
|
+
const term = item.$shape.variant;
|
|
195
|
+
|
|
196
|
+
if ( term == null ) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// compile data space based on item's properties overlaying the item's originating record
|
|
201
|
+
const data = augmentDataSpace( merge( {}, item.$original, item ), item );
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
return Compiler.compile( String( term ), this.termFunctions, this.termsCache )( data );
|
|
205
|
+
} catch ( cause ) {
|
|
206
|
+
throw new Error( `computing variant ID of ${item.$model}#${item.$id} failed: ${cause.message} in ${term}` );
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Fetches compiled shape of named model using provided cache.
|
|
212
|
+
*
|
|
213
|
+
* @param {Object<string,ShapeModel>} shapesPerModelCache cache to use on fetching shape for same model multiple times
|
|
214
|
+
* @param {Shape} localShape shape including desired definition of selected model's shape
|
|
215
|
+
* @param {string} modelName name of model
|
|
216
|
+
* @returns {ShapeModel} compiled shape of model incorporating common definitions for all models
|
|
217
|
+
*/
|
|
218
|
+
getShapeOfModel( shapesPerModelCache, localShape, modelName ) {
|
|
219
|
+
if ( !shapesPerModelCache.hasOwnProperty( modelName ) ) {
|
|
220
|
+
// eslint-disable-next-line no-param-reassign
|
|
221
|
+
shapesPerModelCache[modelName] = merge( {}, localShape?.common, localShape?.models?.[modelName] );
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return shapesPerModelCache[modelName];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Computes dependencies of models on contributions by provided items
|
|
229
|
+
* grouped by model.
|
|
230
|
+
*
|
|
231
|
+
* @param {Object<string,Object<string,Object>>} itemsPerModel compiled items grouped by model
|
|
232
|
+
* @returns {string[]} sorted list of names of models in provided pool starting with model depending on other models' contributions the least
|
|
233
|
+
*/
|
|
234
|
+
listContributingModels( itemsPerModel ) {
|
|
235
|
+
const contributorsPerModel = {};
|
|
236
|
+
|
|
237
|
+
// create map of immediate relations between every two models involved
|
|
238
|
+
// in a contribution
|
|
239
|
+
for ( const model of Object.keys( itemsPerModel ) ) {
|
|
240
|
+
for ( const item of Object.values( itemsPerModel[model] ) ) {
|
|
241
|
+
for ( const contributedModel of Object.keys( item.$shape.contributions || {} ) ) {
|
|
242
|
+
if ( !contributorsPerModel.hasOwnProperty( contributedModel ) ) {
|
|
243
|
+
contributorsPerModel[contributedModel] = {};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
contributorsPerModel[contributedModel][item.$model] = 1;
|
|
125
247
|
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// surface mediate dependencies and collect all contributing models' names
|
|
252
|
+
const contributingModelNames = {};
|
|
253
|
+
|
|
254
|
+
for ( const surfaceModel of Object.keys( contributorsPerModel ) ) {
|
|
255
|
+
descend( surfaceModel, surfaceModel, [] );
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// sort names of contributing models from models depending on other
|
|
259
|
+
// contributing models the least to those doing it the most
|
|
260
|
+
const names = Object.keys( contributingModelNames );
|
|
261
|
+
|
|
262
|
+
names.sort( ( left, right ) => {
|
|
263
|
+
if ( contributorsPerModel.hasOwnProperty( left ) && contributorsPerModel[left].hasOwnProperty( right ) ) {
|
|
264
|
+
// left model depends on contributions by right model
|
|
265
|
+
return 1;
|
|
266
|
+
}
|
|
126
267
|
|
|
127
|
-
|
|
128
|
-
|
|
268
|
+
if ( contributorsPerModel.hasOwnProperty( right ) && contributorsPerModel[right].hasOwnProperty( left ) ) {
|
|
269
|
+
// right model depends on contributions by left model
|
|
270
|
+
return -1;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return 0;
|
|
274
|
+
} );
|
|
275
|
+
|
|
276
|
+
return names;
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Surfaces dependencies of a named model.
|
|
280
|
+
*
|
|
281
|
+
* @param {string} surfaceModel name of model triggering current processing
|
|
282
|
+
* @param {string} currentModel name of currently processed model
|
|
283
|
+
* @param {string[]} breadcrumb names of models processed while descending into dependencies
|
|
284
|
+
* @returns {void}
|
|
285
|
+
*/
|
|
286
|
+
function descend( surfaceModel, currentModel, breadcrumb ) {
|
|
287
|
+
if ( breadcrumb.indexOf( currentModel ) > -1 ) {
|
|
288
|
+
throw new Error( `invalid circular dependency on contributing to model ${surfaceModel}: [${breadcrumb}]` );
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if ( contributorsPerModel.hasOwnProperty( currentModel ) ) {
|
|
292
|
+
const current = contributorsPerModel[currentModel];
|
|
293
|
+
const surface = contributorsPerModel[surfaceModel];
|
|
294
|
+
|
|
295
|
+
for ( const contributingModel of Object.keys( current ) ) {
|
|
296
|
+
contributingModelNames[contributingModel] = true;
|
|
297
|
+
|
|
298
|
+
if ( !surface.hasOwnProperty( contributingModel ) ) {
|
|
299
|
+
surface[contributingModel] = -1;
|
|
300
|
+
} else if ( current[contributingModel] > 0 ) {
|
|
301
|
+
breadcrumb.push( currentModel );
|
|
302
|
+
descend( surfaceModel, contributingModel, breadcrumb );
|
|
303
|
+
breadcrumb.pop();
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Processes contributions of models in provided collection of items
|
|
312
|
+
* augmenting that collection.
|
|
313
|
+
*
|
|
314
|
+
* @param {Object<string,Object<string,Object>>} itemsPerModel map of model names into either model's map of item IDs into related item
|
|
315
|
+
* @param {Set<string>} contributedItems tracks IDs of items or their variants that have been contributed to
|
|
316
|
+
* @returns {void}
|
|
317
|
+
*/
|
|
318
|
+
processContributions( itemsPerModel, contributedItems ) {
|
|
319
|
+
const contributingModels = this.listContributingModels( itemsPerModel );
|
|
320
|
+
const shapePerModelCache = {};
|
|
321
|
+
|
|
322
|
+
for ( const modelName of contributingModels ) {
|
|
323
|
+
const items = itemsPerModel[modelName];
|
|
324
|
+
|
|
325
|
+
for ( const item of Object.values( items ) ) {
|
|
326
|
+
const { contributions } = item.$shape;
|
|
327
|
+
if ( !contributions ) {
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
for ( const targetModelName of Object.keys( contributions ) ) {
|
|
332
|
+
const targetModelShape = this.getShapeOfModel( shapePerModelCache, item.$localShape, targetModelName );
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
// extract contributions to actually declared properties of target model
|
|
336
|
+
const contributionTerms = {};
|
|
337
|
+
let warn = false;
|
|
338
|
+
|
|
339
|
+
for ( const termTargetName of Object.keys( contributions[targetModelName] ) ) {
|
|
340
|
+
const propertyName = termTargetName.replace( /\s*\[]$/, "" );
|
|
341
|
+
|
|
342
|
+
if ( propertyName !== termTargetName && propertyName.startsWith( "$" ) ) {
|
|
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
|
|
344
|
+
}
|
|
129
345
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
346
|
+
if ( Object.prototype.hasOwnProperty.call( targetModelShape.properties, propertyName ) ) {
|
|
347
|
+
if ( contributionTerms.hasOwnProperty( propertyName ) ) {
|
|
348
|
+
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
|
+
}
|
|
350
|
+
|
|
351
|
+
contributionTerms[propertyName] = contributions[targetModelName][termTargetName];
|
|
352
|
+
} else {
|
|
353
|
+
warn = true;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if ( Object.keys( contributionTerms ).length < 1 ) {
|
|
358
|
+
console.warn( `ignoring eventually empty contribution of record #${item.$id} in model ${item.$model} to model ${targetModelName}` );
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if ( warn ) {
|
|
363
|
+
console.warn( `ignoring undeclared properties in contribution of record #${item.$id} in model ${item.$model} to model ${targetModelName}` ); // eslint-disable-line max-len
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
// compile values contributed to target model in context of current item
|
|
368
|
+
const augmentedData = augmentDataSpace( item, item );
|
|
369
|
+
const contribution = this.compileItem( augmentedData, contributionTerms, {}, ["$id"] );
|
|
370
|
+
|
|
371
|
+
if ( Object.keys( contribution ).length < 1 ) {
|
|
372
|
+
console.warn( `ignoring empty contribution of record #${item.$id} in model ${item.$model} to model ${targetModelName}` );
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
|
|
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
|
+
|
|
383
|
+
const targetIds = Array.isArray( targetId ) ? targetId : [targetId];
|
|
384
|
+
|
|
385
|
+
// check if this contribution is regarding a variant of selected target item
|
|
386
|
+
let variantId;
|
|
387
|
+
|
|
388
|
+
if ( targetModelShape.variant ) {
|
|
389
|
+
// try computing variant ID based on this item's contribution to target item
|
|
390
|
+
variantId = this.computeVariantIdOfItem( {
|
|
391
|
+
...contribution,
|
|
392
|
+
$shape: targetModelShape,
|
|
393
|
+
$id: targetId,
|
|
394
|
+
$model: targetModelName,
|
|
395
|
+
$original: { ...item.$original, data: {} },
|
|
396
|
+
$segments: item.$segments,
|
|
397
|
+
$localShape: item.$localShape,
|
|
398
|
+
} );
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if ( variantId == null ) {
|
|
402
|
+
variantId = null;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
for ( const id of targetIds ) {
|
|
407
|
+
// track contributions to selected target item or its variant
|
|
408
|
+
contributedItems.add( JSON.stringify( [ targetModelName, id, variantId ] ) );
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
// gain access on selected target item, create its containers as necessary
|
|
412
|
+
let target;
|
|
413
|
+
|
|
414
|
+
if ( !itemsPerModel.hasOwnProperty( targetModelName ) ) {
|
|
415
|
+
// eslint-disable-next-line no-param-reassign
|
|
416
|
+
itemsPerModel[targetModelName] = Object.defineProperties( {}, {
|
|
417
|
+
$model: { value: targetModelName },
|
|
418
|
+
$shape: { value: targetModelShape },
|
|
419
|
+
} );
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const model = itemsPerModel[targetModelName];
|
|
423
|
+
|
|
424
|
+
if ( !model.hasOwnProperty( id ) ) {
|
|
425
|
+
model[id] = Object.defineProperties( {}, {
|
|
426
|
+
$original: { value: {} },
|
|
427
|
+
$shape: { value: targetModelShape },
|
|
428
|
+
$model: { value: targetModelName },
|
|
429
|
+
$id: { value: id },
|
|
430
|
+
$segments: { value: item.$segments },
|
|
431
|
+
$localShape: { value: item.$localShape, configurable: true },
|
|
432
|
+
} );
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const targetItem = model[id];
|
|
436
|
+
|
|
437
|
+
if ( variantId == null ) {
|
|
438
|
+
target = targetItem;
|
|
439
|
+
} else {
|
|
440
|
+
if ( !targetItem.$variants ) {
|
|
441
|
+
targetItem.$variants = {};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if ( !targetItem.$variants.hasOwnProperty( variantId ) ) {
|
|
445
|
+
targetItem.$variants[variantId] = {};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
target = targetItem.$variants[variantId];
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
// merge this item's contribution with any previous contribution to the selected target
|
|
453
|
+
for ( const name of Object.keys( contribution ) ) {
|
|
454
|
+
if ( String( name ).startsWith( "$" ) ) {
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const value = contribution[name];
|
|
459
|
+
|
|
460
|
+
if ( contributions[targetModelName][name] ) {
|
|
461
|
+
// contribution has been declared without suffix "[]"
|
|
462
|
+
// -> do not collect, but replace value in target
|
|
463
|
+
target[name] = value;
|
|
464
|
+
} else {
|
|
465
|
+
// contribution has been declared with suffix "[]"
|
|
466
|
+
// -> collect value in target
|
|
467
|
+
if ( Array.isArray( target[name] ) ) { // eslint-disable-line no-lonely-if,max-depth
|
|
468
|
+
target[name].push( value );
|
|
469
|
+
} else if ( target[name] == null ) {
|
|
470
|
+
target[name] = [value];
|
|
471
|
+
} else {
|
|
472
|
+
target[name] = [ target[name], value ];
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Re-compiles all items in provided pool that have been contributed to.
|
|
484
|
+
*
|
|
485
|
+
* @param {Object<string,Object<string,CollectedItem>>} itemsPerModel pool of items per model to update
|
|
486
|
+
* @param {Set<string>} contributions lists IDs of items or their variants that have been contributed to
|
|
487
|
+
* @returns {Object<string,Object<string,CollectedItem>>} pool of items per model as provided
|
|
488
|
+
*/
|
|
489
|
+
postProcessContributions( itemsPerModel, contributions ) {
|
|
490
|
+
for ( const track of contributions.values() ) {
|
|
491
|
+
const [ model, id, variantId ] = JSON.parse( track );
|
|
492
|
+
|
|
493
|
+
if ( !itemsPerModel.hasOwnProperty( model ) ) {
|
|
494
|
+
console.warn( `inconsistency: missing items pool of model ${model} though contributions have been tracked` );
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const modelPool = itemsPerModel[model];
|
|
499
|
+
|
|
500
|
+
if ( !modelPool.hasOwnProperty( id ) ) {
|
|
501
|
+
console.warn( `inconsistency: missing item ${id} of model ${model} though contributions have been tracked` );
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const item = modelPool[id];
|
|
506
|
+
|
|
507
|
+
if ( variantId == null ) {
|
|
508
|
+
modelPool[id] = this.recompileItem( item );
|
|
509
|
+
} else {
|
|
510
|
+
if ( !item.$variants ) {
|
|
511
|
+
console.warn( `inconsistency: missing variants of item ${id} of model ${model} though contributions have been tracked` );
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const variants = item.$variants;
|
|
516
|
+
|
|
517
|
+
if ( !variants.hasOwnProperty( variantId ) ) {
|
|
518
|
+
console.warn( `inconsistency: missing variant ${variantId} of item ${id} of model ${model} though contributions have been tracked` );
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
variants[variantId] = this.recompileItem( variants[variantId] );
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return itemsPerModel;
|
|
527
|
+
}
|
|
528
|
+
|
|
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 },
|
|
134
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
|
+
}
|
|
135
570
|
|
|
136
|
-
|
|
137
|
-
|
|
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
|
+
}
|
|
138
610
|
} else {
|
|
139
|
-
|
|
611
|
+
_to[name] = from[name];
|
|
140
612
|
}
|
|
141
613
|
}
|
|
142
614
|
}
|
|
143
615
|
|
|
144
|
-
return
|
|
616
|
+
return _to;
|
|
145
617
|
}
|
|
146
618
|
|
|
147
619
|
/**
|
|
@@ -193,33 +665,32 @@ export class Shaper extends EventEmitter {
|
|
|
193
665
|
/**
|
|
194
666
|
* Compiles resulting item by evaluating properties declared in model shape.
|
|
195
667
|
*
|
|
196
|
-
* @param {object} data source of record
|
|
197
|
-
* @param {
|
|
198
|
-
* @param {
|
|
668
|
+
* @param {object} data (augmented) source of record
|
|
669
|
+
* @param {ShapeProperties} properties definition of resulting item's properties
|
|
670
|
+
* @param {ShapePropertyDefaults} defaults definition of default values for resulting item's properties
|
|
671
|
+
* @param {string[]} accept list names of properties to accept even when starting with a dollar sign
|
|
199
672
|
* @returns {object} compiled item
|
|
200
673
|
*/
|
|
201
|
-
compileItem( data,
|
|
202
|
-
const properties = modelShape?.properties || {};
|
|
203
|
-
const defaults = modelShape?.defaults || {};
|
|
674
|
+
compileItem( data, properties = {}, defaults = {}, accept = [] ) {
|
|
204
675
|
const item = {};
|
|
205
676
|
|
|
206
677
|
for ( const property of Object.keys( properties ) ) {
|
|
207
|
-
if ( !String( property ).startsWith( "$" ) ) {
|
|
208
|
-
const
|
|
209
|
-
if ( typeof
|
|
678
|
+
if ( accept.indexOf( property ) > -1 || !String( property ).startsWith( "$" ) ) {
|
|
679
|
+
const term = properties[property];
|
|
680
|
+
if ( typeof term !== "string" || term.trim().length === 0 ) {
|
|
210
681
|
throw new Error( `invalid or missing term for computing value of property ${data.$model}#${property}` );
|
|
211
682
|
}
|
|
212
683
|
|
|
213
684
|
let value;
|
|
214
685
|
|
|
215
686
|
try {
|
|
216
|
-
value = Compiler.compile(
|
|
687
|
+
value = Compiler.compile( term, this.termFunctions, this.termsCache )( data );
|
|
217
688
|
} catch ( cause ) {
|
|
218
|
-
throw new Error( `fetching value of property ${data.$model}#${property} failed: ${cause.message} in ${
|
|
689
|
+
throw new Error( `fetching value of property ${data.$model}#${property} failed: ${cause.message} in ${term}` );
|
|
219
690
|
}
|
|
220
691
|
|
|
221
692
|
if ( value == null ) {
|
|
222
|
-
if (
|
|
693
|
+
if ( defaults.hasOwnProperty( property ) ) {
|
|
223
694
|
const defaultValue = defaults[property];
|
|
224
695
|
const trimmed = typeof defaultValue === "string" ? defaultValue.trim() : "";
|
|
225
696
|
|
|
@@ -238,14 +709,16 @@ export class Shaper extends EventEmitter {
|
|
|
238
709
|
}
|
|
239
710
|
}
|
|
240
711
|
|
|
241
|
-
|
|
712
|
+
// deeply merge fresh object with compiled item to get rid of deep data
|
|
713
|
+
// exposed to terms as Proxy
|
|
714
|
+
return merge( {}, item );
|
|
242
715
|
}
|
|
243
716
|
|
|
244
717
|
/**
|
|
245
718
|
* Populates pool of endpoints with collected records of discovered models.
|
|
246
719
|
*
|
|
247
720
|
* @param {DatabaseEndpoints} endpoints map of routes into data sets to expose eventually
|
|
248
|
-
* @param {
|
|
721
|
+
* @param {Object<string,Object<string,CollectedItem>>} models pool of items per discovered model
|
|
249
722
|
* @returns {DatabaseEndpoints} map of routes into data sets to expose eventually
|
|
250
723
|
*/
|
|
251
724
|
populateModels( endpoints, models ) {
|
|
@@ -272,13 +745,13 @@ export class Shaper extends EventEmitter {
|
|
|
272
745
|
* a model.
|
|
273
746
|
*
|
|
274
747
|
* @param {DatabaseEndpoints} endpoints map of routes into data sets to expose eventually
|
|
275
|
-
* @param {
|
|
748
|
+
* @param {Object<string,Object<string,CollectedItem>>} models pool of items per discovered model
|
|
276
749
|
* @param {string} modelName name of model to process
|
|
277
750
|
* @returns {DatabaseEndpoints} map of routes into data sets to expose eventually
|
|
278
751
|
*/
|
|
279
752
|
populateModelCollections( endpoints, models, modelName ) {
|
|
280
753
|
const model = models[modelName];
|
|
281
|
-
const collections = model.$shape
|
|
754
|
+
const collections = model.$shape.collections || {};
|
|
282
755
|
const cache = {};
|
|
283
756
|
|
|
284
757
|
// prepare to process collection instances prior to collection references
|
|
@@ -339,14 +812,14 @@ export class Shaper extends EventEmitter {
|
|
|
339
812
|
|
|
340
813
|
for ( const key in collection ) {
|
|
341
814
|
if ( collection.hasOwnProperty( key ) ) {
|
|
342
|
-
this.populateSlicedEndpoint( endpoints, normalized.replace( ptnKey, key ), collection[key], false, modelName );
|
|
815
|
+
this.populateSlicedEndpoint( endpoints, normalized.replace( ptnKey, key ), collection[key], false, modelName, definition );
|
|
343
816
|
}
|
|
344
817
|
}
|
|
345
818
|
} else {
|
|
346
819
|
throw new Error( `invalid use of {key} in route "${normalized}" of regular collection of model "${modelName}"` );
|
|
347
820
|
}
|
|
348
821
|
} else {
|
|
349
|
-
this.populateSlicedEndpoint( endpoints, normalized, collection, isKeyed, modelName );
|
|
822
|
+
this.populateSlicedEndpoint( endpoints, normalized, collection, isKeyed, modelName, definition );
|
|
350
823
|
}
|
|
351
824
|
}
|
|
352
825
|
}
|
|
@@ -361,12 +834,13 @@ export class Shaper extends EventEmitter {
|
|
|
361
834
|
*
|
|
362
835
|
* @param {DatabaseEndpoints} endpoints map of routes into data sets to expose eventually
|
|
363
836
|
* @param {string} route route of current collection
|
|
364
|
-
* @param {{items: Array<object>}|
|
|
837
|
+
* @param {{items: Array<object>}|Object<string, Array<object>>} collection collection of items to expose
|
|
365
838
|
* @param {boolean} isKeyed indicates if collection lists item per key or all items in a single list
|
|
366
839
|
* @param {string} modelName name of model this collection is defined for
|
|
840
|
+
* @param {object} definition provided collection's definition in shape
|
|
367
841
|
* @returns {void}
|
|
368
842
|
*/
|
|
369
|
-
populateSlicedEndpoint( endpoints, route, collection, isKeyed, modelName ) {
|
|
843
|
+
populateSlicedEndpoint( endpoints, route, collection, isKeyed, modelName, definition ) {
|
|
370
844
|
ptnOffset.lastIndex = 0;
|
|
371
845
|
const match = ptnOffset.exec( route );
|
|
372
846
|
|
|
@@ -387,22 +861,30 @@ export class Shaper extends EventEmitter {
|
|
|
387
861
|
const normalized = normalizePathname( prefix + route.replace( ptnOffset, offset ) );
|
|
388
862
|
endpoints[normalized] = { count: items.length, items: items.slice( offset, offset + limit ) }; // eslint-disable-line no-param-reassign
|
|
389
863
|
}
|
|
390
|
-
} else {
|
|
864
|
+
} else if ( isKeyed ) {
|
|
391
865
|
const normalized = normalizePathname( this.prefix + modelName + "/" + route );
|
|
866
|
+
const base = endpoints[normalized] = {}; // eslint-disable-line no-param-reassign
|
|
392
867
|
|
|
393
|
-
|
|
394
|
-
const
|
|
868
|
+
for ( const key of Object.keys( collection ) ) {
|
|
869
|
+
const items = collection[key].items;
|
|
395
870
|
|
|
396
|
-
|
|
397
|
-
const items = collection[key].items;
|
|
871
|
+
base[key] = { count: items.length };
|
|
398
872
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
873
|
+
if ( definition.reduce ) {
|
|
874
|
+
// generate single endpoint listing all items of collection grouped by key
|
|
875
|
+
base[key].items = items;
|
|
876
|
+
} else {
|
|
877
|
+
// expose separate endpoint per group of items
|
|
878
|
+
const subNormalized = normalizePathname( this.prefix + modelName + "/" + route + "/" + key );
|
|
403
879
|
|
|
404
|
-
|
|
880
|
+
endpoints[subNormalized] = { count: items.length, items }; // eslint-disable-line no-param-reassign
|
|
881
|
+
}
|
|
405
882
|
}
|
|
883
|
+
} else {
|
|
884
|
+
const normalized = normalizePathname( this.prefix + modelName + "/" + route );
|
|
885
|
+
const items = collection.items;
|
|
886
|
+
|
|
887
|
+
endpoints[normalized] = { count: items.length, items }; // eslint-disable-line no-param-reassign
|
|
406
888
|
}
|
|
407
889
|
}
|
|
408
890
|
|
|
@@ -410,7 +892,7 @@ export class Shaper extends EventEmitter {
|
|
|
410
892
|
* Creates managed data space for evaluating terms in context of a set of
|
|
411
893
|
* items.
|
|
412
894
|
*
|
|
413
|
-
* @param {
|
|
895
|
+
* @param {Object<string,Object<string,CollectedItem>>} models pool of items per discovered model
|
|
414
896
|
* @param {string} modelName name of model to process
|
|
415
897
|
* @param {ShapeCollection} collectionDefinition rules controlling compilation of collection
|
|
416
898
|
* @returns {{handler: {get: ((function(*, *=): (*))|*)}, context: {$models, $model, $collection}}} handler and extensible context for term evaluation
|
|
@@ -438,7 +920,7 @@ export class Shaper extends EventEmitter {
|
|
|
438
920
|
/**
|
|
439
921
|
* Compiles collection according to its definition in provided shape.
|
|
440
922
|
*
|
|
441
|
-
* @param {
|
|
923
|
+
* @param {Object<string,Object<string,CollectedItem>>} models pool of items per discovered model
|
|
442
924
|
* @param {string} modelName name of model to process
|
|
443
925
|
* @param {ShapeCollection} definition rules controlling compilation of collection
|
|
444
926
|
* @param {string} route route for resulting collection
|
|
@@ -451,7 +933,7 @@ export class Shaper extends EventEmitter {
|
|
|
451
933
|
const collection = isKeyed ? {} : { items: [] };
|
|
452
934
|
|
|
453
935
|
|
|
454
|
-
const { handler, context } = this.createTermContextCollection();
|
|
936
|
+
const { handler, context } = this.createTermContextCollection( models, modelName, definition );
|
|
455
937
|
|
|
456
938
|
|
|
457
939
|
// prepare terms used in compiling collection
|
|
@@ -492,7 +974,7 @@ export class Shaper extends EventEmitter {
|
|
|
492
974
|
|
|
493
975
|
// process all items of model
|
|
494
976
|
for ( const id in model ) {
|
|
495
|
-
if ( model
|
|
977
|
+
if ( model[id] && typeof id === "string" && id[0] !== "$" ) {
|
|
496
978
|
const item = model[id];
|
|
497
979
|
const data = new Proxy( item, handler );
|
|
498
980
|
let extracted;
|
|
@@ -548,7 +1030,7 @@ export class Shaper extends EventEmitter {
|
|
|
548
1030
|
/**
|
|
549
1031
|
* Sorts provided collection either in-place or as a copy.
|
|
550
1032
|
*
|
|
551
|
-
* @param {
|
|
1033
|
+
* @param {Object<string,Object<string,CollectedItem>>} models pool of items per discovered model
|
|
552
1034
|
* @param {string} modelName name of model to process
|
|
553
1035
|
* @param {ShapeCollection} collectionDefinition rules controlling compilation of collection
|
|
554
1036
|
* @param {string} route route for resulting collection
|