@cepharum/concrete-db 0.2.2 → 0.4.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/LICENSE +1 -1
- package/cure.mjs +6 -7
- package/lib/generator.mjs +20 -3
- package/lib/helper.mjs +14 -4
- package/lib/shaper.mjs +99 -117
- package/package.json +28 -20
package/LICENSE
CHANGED
package/cure.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import File from "fs";
|
|
3
3
|
import Path from "path";
|
|
4
|
+
import { pathToFileURL } from "url";
|
|
4
5
|
|
|
5
6
|
import minimist from "minimist";
|
|
6
7
|
import YAML from "yaml";
|
|
@@ -28,7 +29,8 @@ Supported options are:
|
|
|
28
29
|
--debug be extra verbose
|
|
29
30
|
-s / --shape=file selects global shape controlling transformation
|
|
30
31
|
-o / --output=folder selects folder resulting files are written to
|
|
31
|
-
-c / --clean remove
|
|
32
|
+
-c / --clean remove stale files from output folder that aren't
|
|
33
|
+
part of the freshly cured database
|
|
32
34
|
-l / --library=file selects file exposing custom term functions to use
|
|
33
35
|
in addition to regular ones
|
|
34
36
|
` );
|
|
@@ -56,17 +58,14 @@ if ( args.V || args.version ) {
|
|
|
56
58
|
|
|
57
59
|
options.library = {};
|
|
58
60
|
for ( const library of libraries ) {
|
|
59
|
-
|
|
61
|
+
const pathname = Path.resolve( ".", library );
|
|
60
62
|
|
|
61
63
|
if ( options.verbose ) {
|
|
62
64
|
console.error( `including library ${pathname}` );
|
|
63
65
|
}
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const functions = await import( pathname ); // eslint-disable-line no-await-in-loop
|
|
67
|
+
// import via file URL so absolute paths work cross-platform (incl. Windows drive letters)
|
|
68
|
+
const functions = await import( pathToFileURL( pathname ).href ); // eslint-disable-line no-await-in-loop
|
|
70
69
|
|
|
71
70
|
for ( const name of Object.keys( functions ) ) {
|
|
72
71
|
if ( options.verbose > 1 ) {
|
package/lib/generator.mjs
CHANGED
|
@@ -44,6 +44,10 @@ export class Generator extends EventEmitter {
|
|
|
44
44
|
console.error( `cleaning output folder ${subfolder} ...` );
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
// routes of all endpoints to keep (matching the names of files written
|
|
48
|
+
// below), used to detect stale files left over from previous curing
|
|
49
|
+
const known = new Set( Object.keys( endpoints ).map( key => key.replace( /\.json$/, "" ) ) );
|
|
50
|
+
|
|
47
51
|
await FileEssentials.find( subfolder, {
|
|
48
52
|
depthFirst: true,
|
|
49
53
|
filter: ( localPath, fullPath, stat ) => {
|
|
@@ -54,10 +58,23 @@ export class Generator extends EventEmitter {
|
|
|
54
58
|
return stat.isFile();
|
|
55
59
|
},
|
|
56
60
|
converter: ( localPath, fullPath, stat ) => {
|
|
57
|
-
|
|
61
|
+
if ( stat.isDirectory() ) {
|
|
62
|
+
// keep the freshly created prefix folder itself even when
|
|
63
|
+
// it is (currently) empty
|
|
64
|
+
if ( Path.resolve( fullPath ) === Path.resolve( subfolder ) ) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// drop directories that became empty after removing stale
|
|
69
|
+
// files (depth-first ensures children are handled first);
|
|
70
|
+
// keep directories still holding files of the database
|
|
71
|
+
return File.promises.rmdir( fullPath ).catch( () => undefined );
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const route = Path.relative( folder, fullPath ).replace( /\.json$/, "" ).split( Path.sep ).join( "/" );
|
|
58
75
|
|
|
59
|
-
if ( !
|
|
60
|
-
return
|
|
76
|
+
if ( !known.has( route ) ) {
|
|
77
|
+
return File.promises.unlink( fullPath );
|
|
61
78
|
}
|
|
62
79
|
|
|
63
80
|
return undefined;
|
package/lib/helper.mjs
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
export * from "simple-terms/lib/helper.js";
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Wraps provided object in a proxy offering case-insensitive access on
|
|
5
3
|
* object's properties. Any accessed property's value is wrapped before
|
|
@@ -93,10 +91,22 @@ export class ForeignKey {
|
|
|
93
91
|
/**
|
|
94
92
|
* Deeply freezes provided object and all its properties recursively.
|
|
95
93
|
*
|
|
94
|
+
* Non-object values are returned as-is. Already frozen objects are skipped which
|
|
95
|
+
* keeps the recursion safe in case of cyclic references.
|
|
96
|
+
*
|
|
96
97
|
* @param {object} object object to freeze deeply; will be frozen on return, too
|
|
97
98
|
* @returns {object} deeply frozen object
|
|
98
99
|
*/
|
|
99
100
|
export function deepFreeze( object ) {
|
|
100
|
-
|
|
101
|
-
|
|
101
|
+
if ( !object || typeof object !== "object" || Object.isFrozen( object ) ) {
|
|
102
|
+
return object;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
Object.freeze( object );
|
|
106
|
+
|
|
107
|
+
for ( const value of Object.values( object ) ) {
|
|
108
|
+
deepFreeze( value );
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return object;
|
|
102
112
|
}
|
package/lib/shaper.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// noinspection ExceptionCaughtLocallyJS
|
|
2
|
+
/* eslint-disable no-await-in-loop */
|
|
2
3
|
|
|
3
4
|
import EventEmitter from "events";
|
|
4
5
|
|
|
@@ -50,16 +51,17 @@ export class Shaper extends EventEmitter {
|
|
|
50
51
|
* @param {Shape[]} localShapes local shapes of folders records have been collected from
|
|
51
52
|
* @returns {Database} resulting database records
|
|
52
53
|
*/
|
|
53
|
-
transform( recordsCollection, localShapes ) {
|
|
54
|
+
async transform( recordsCollection, localShapes ) {
|
|
54
55
|
if ( recordsCollection.size > 0 && !localShapes?.length ) {
|
|
55
56
|
throw new Error( "invalid use of Shaper: missing list of local shapes per folder records have been collected from" );
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
const mergedLocalShapes = merge( {}, DefaultShape, ...localShapes );
|
|
59
|
-
const
|
|
60
|
+
const grouped = await this.groupByModel( recordsCollection );
|
|
61
|
+
const models = await this.handleContributions( grouped, mergedLocalShapes );
|
|
60
62
|
const endpoints = {};
|
|
61
63
|
|
|
62
|
-
this.populateModels( endpoints, models, mergedLocalShapes );
|
|
64
|
+
await this.populateModels( endpoints, models, mergedLocalShapes );
|
|
63
65
|
|
|
64
66
|
return { prefix: this.prefix, endpoints };
|
|
65
67
|
}
|
|
@@ -70,7 +72,7 @@ export class Shaper extends EventEmitter {
|
|
|
70
72
|
* @param {Map<string,CollectedRecord[]>} collectedRecordsByName collection of records to transform
|
|
71
73
|
* @returns {Object<string,Object<string, ModelItem>>} provided records grouped by model and unique item ID
|
|
72
74
|
*/
|
|
73
|
-
groupByModel( collectedRecordsByName ) {
|
|
75
|
+
async groupByModel( collectedRecordsByName ) {
|
|
74
76
|
const globalItemsPerModel = {};
|
|
75
77
|
let index = 0;
|
|
76
78
|
|
|
@@ -83,7 +85,7 @@ export class Shaper extends EventEmitter {
|
|
|
83
85
|
process.stderr.write( `\r[${++index}/${collectedRecordsByName.size}]` );
|
|
84
86
|
}
|
|
85
87
|
|
|
86
|
-
this.compileRecords( globalItemsPerModel, records );
|
|
88
|
+
await this.compileRecords( globalItemsPerModel, records );
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
return globalItemsPerModel;
|
|
@@ -97,20 +99,20 @@ export class Shaper extends EventEmitter {
|
|
|
97
99
|
* @param {Shape} mergedLocalShapes local shapes of source folders merged in order of collecting records
|
|
98
100
|
* @returns {Object<string,Object<string, CollectedRecord[]>>} collections of items per model as provided
|
|
99
101
|
*/
|
|
100
|
-
handleContributions( globalItemsPerModel, mergedLocalShapes ) {
|
|
102
|
+
async handleContributions( globalItemsPerModel, mergedLocalShapes ) {
|
|
101
103
|
const postProcessingQueue = new Set();
|
|
102
104
|
|
|
103
105
|
if ( this.options.verbose ) {
|
|
104
106
|
console.error( `\nprocessing contributions ...` );
|
|
105
107
|
}
|
|
106
108
|
|
|
107
|
-
this.processContributions( globalItemsPerModel, postProcessingQueue, mergedLocalShapes );
|
|
109
|
+
await this.processContributions( globalItemsPerModel, postProcessingQueue, mergedLocalShapes );
|
|
108
110
|
|
|
109
111
|
if ( this.options.verbose ) {
|
|
110
112
|
console.error( `post-processing ${postProcessingQueue.size} contributions` );
|
|
111
113
|
}
|
|
112
114
|
|
|
113
|
-
this.recompileContributionTargets( globalItemsPerModel, postProcessingQueue );
|
|
115
|
+
await this.recompileContributionTargets( globalItemsPerModel, postProcessingQueue );
|
|
114
116
|
|
|
115
117
|
return globalItemsPerModel;
|
|
116
118
|
}
|
|
@@ -123,11 +125,11 @@ export class Shaper extends EventEmitter {
|
|
|
123
125
|
* @param {CollectedRecord[]} records list of records collected from a source
|
|
124
126
|
* @returns {Object<string,Object<string,ModelItem>>} compiled items grouped by model, addressable by either item's ID
|
|
125
127
|
*/
|
|
126
|
-
compileRecords( itemsPerModel, records ) {
|
|
128
|
+
async compileRecords( itemsPerModel, records ) {
|
|
127
129
|
const shapesPerModelCache = {};
|
|
128
130
|
|
|
129
131
|
for ( let read = 0, numRecords = records.length; read < numRecords; read++ ) {
|
|
130
|
-
const item = this.compileModelItemFromRecord( shapesPerModelCache, itemsPerModel, records[read] );
|
|
132
|
+
const item = await this.compileModelItemFromRecord( shapesPerModelCache, itemsPerModel, records[read] );
|
|
131
133
|
const modelPool = itemsPerModel[item.$model];
|
|
132
134
|
|
|
133
135
|
if ( item.$variantId == null ) {
|
|
@@ -154,9 +156,9 @@ export class Shaper extends EventEmitter {
|
|
|
154
156
|
* @param {Object<string,ShapeModel>} shapesPerModelCache caches per-model shapes compiled before
|
|
155
157
|
* @param {Object<string,Object<string,Object>>} itemsPerModel collection of previously compiled items per model
|
|
156
158
|
* @param {CollectedRecord} record augmented data collected from a source to be compiled
|
|
157
|
-
* @returns {
|
|
159
|
+
* @returns {Promise<ModelItem>} instance of model with hidden meta information injected
|
|
158
160
|
*/
|
|
159
|
-
compileModelItemFromRecord( shapesPerModelCache, itemsPerModel, record ) {
|
|
161
|
+
async compileModelItemFromRecord( shapesPerModelCache, itemsPerModel, record ) {
|
|
160
162
|
const { segments, shape, data } = record;
|
|
161
163
|
|
|
162
164
|
const metaProperties = {
|
|
@@ -165,19 +167,19 @@ export class Shaper extends EventEmitter {
|
|
|
165
167
|
|
|
166
168
|
const augmentedData = augmentDataSpace( data, metaProperties );
|
|
167
169
|
|
|
168
|
-
metaProperties.$model = this.detectModel( augmentedData, shape );
|
|
170
|
+
metaProperties.$model = await this.detectModel( augmentedData, shape );
|
|
169
171
|
metaProperties.$shape = this.getShapeOfModel( shapesPerModelCache, shape, metaProperties.$model );
|
|
170
|
-
metaProperties.$id = this.identifyRecord( augmentedData, metaProperties.$shape );
|
|
172
|
+
metaProperties.$id = await this.identifyRecord( augmentedData, metaProperties.$shape );
|
|
171
173
|
|
|
172
174
|
if ( !itemsPerModel.hasOwnProperty( metaProperties.$model ) ) {
|
|
173
|
-
|
|
175
|
+
|
|
174
176
|
itemsPerModel[metaProperties.$model] = {};
|
|
175
177
|
}
|
|
176
178
|
|
|
177
179
|
const model = itemsPerModel[metaProperties.$model];
|
|
178
180
|
|
|
179
181
|
const { properties, defaults } = metaProperties.$shape;
|
|
180
|
-
const item = this.compileItem( augmentedData, properties, model[metaProperties.$id] ? {} : defaults );
|
|
182
|
+
const item = await this.compileItem( augmentedData, properties, model[metaProperties.$id] ? {} : defaults );
|
|
181
183
|
|
|
182
184
|
Object.defineProperties( item, {
|
|
183
185
|
$segments: { value: segments },
|
|
@@ -185,7 +187,7 @@ export class Shaper extends EventEmitter {
|
|
|
185
187
|
$shape: { value: metaProperties.$shape, configurable: true },
|
|
186
188
|
$model: { value: metaProperties.$model },
|
|
187
189
|
$id: { value: metaProperties.$id },
|
|
188
|
-
$variantId: { value: this.computeVariantIdOfItem( metaProperties.$shape.properties.$variant, augmentedData ) },
|
|
190
|
+
$variantId: { value: await this.computeVariantIdOfItem( metaProperties.$shape.properties.$variant, augmentedData ) },
|
|
189
191
|
} );
|
|
190
192
|
|
|
191
193
|
return item;
|
|
@@ -200,9 +202,9 @@ export class Shaper extends EventEmitter {
|
|
|
200
202
|
* @param {ModelItem} item previously compiled item to re-compile
|
|
201
203
|
* @returns {ModelItem} re-compiled item
|
|
202
204
|
*/
|
|
203
|
-
recompileItem( item ) {
|
|
205
|
+
async recompileItem( item ) {
|
|
204
206
|
const data = augmentDataSpace( merge( {}, item.$original, item ), { ...item, $post: true } );
|
|
205
|
-
const updatedItem = this.compileItem( data, item.$shape.properties, {} );
|
|
207
|
+
const updatedItem = await this.compileItem( data, item.$shape.properties, {} );
|
|
206
208
|
|
|
207
209
|
Object.defineProperties( updatedItem, {
|
|
208
210
|
$segments: { value: item.$segments },
|
|
@@ -221,9 +223,9 @@ export class Shaper extends EventEmitter {
|
|
|
221
223
|
*
|
|
222
224
|
* @param {string?} term term expressing computation of variant ID
|
|
223
225
|
* @param {ModelItem} item an item to get variant ID for
|
|
224
|
-
* @returns {null|any} computed variant ID of provided item, nullish if item is not a variant
|
|
226
|
+
* @returns {Promise<null|any>} computed variant ID of provided item, nullish if item is not a variant
|
|
225
227
|
*/
|
|
226
|
-
computeVariantIdOfItem( term, item ) {
|
|
228
|
+
async computeVariantIdOfItem( term, item ) {
|
|
227
229
|
if ( term == null ) {
|
|
228
230
|
return null;
|
|
229
231
|
}
|
|
@@ -232,7 +234,7 @@ export class Shaper extends EventEmitter {
|
|
|
232
234
|
const data = augmentDataSpace( merge( {}, item.$original, item ), item );
|
|
233
235
|
|
|
234
236
|
try {
|
|
235
|
-
const value = Compiler.compile( String( term ), this.termFunctions, this.termsCache )( data );
|
|
237
|
+
const value = await Compiler.compile( String( term ), this.termFunctions, this.termsCache )( data );
|
|
236
238
|
|
|
237
239
|
// support processed term requesting to fail computation of variant ID
|
|
238
240
|
if ( value instanceof Error ) {
|
|
@@ -255,8 +257,15 @@ export class Shaper extends EventEmitter {
|
|
|
255
257
|
*/
|
|
256
258
|
getShapeOfModel( shapesPerModelCache, localShape, modelName ) {
|
|
257
259
|
if ( !shapesPerModelCache.hasOwnProperty( modelName ) ) {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
+
|
|
261
|
+
// NOTE: `{ defaults: {} }` must be a fresh target so the subsequent
|
|
262
|
+
// merges never write into the shared module-level DefaultShape.
|
|
263
|
+
shapesPerModelCache[modelName] = merge(
|
|
264
|
+
{ defaults: {} },
|
|
265
|
+
{ defaults: DefaultShape.common.defaults },
|
|
266
|
+
localShape?.common,
|
|
267
|
+
localShape?.models?.[modelName]
|
|
268
|
+
);
|
|
260
269
|
}
|
|
261
270
|
|
|
262
271
|
return shapesPerModelCache[modelName];
|
|
@@ -354,7 +363,7 @@ export class Shaper extends EventEmitter {
|
|
|
354
363
|
* @param {Shape} mergedLocalShapes local shapes of source folders merged in order of collecting records
|
|
355
364
|
* @returns {void}
|
|
356
365
|
*/
|
|
357
|
-
processContributions( itemsPerModel, contributedItems, mergedLocalShapes ) {
|
|
366
|
+
async processContributions( itemsPerModel, contributedItems, mergedLocalShapes ) {
|
|
358
367
|
const contributingModels = this.listContributingModels( itemsPerModel );
|
|
359
368
|
const shapePerModelCache = {};
|
|
360
369
|
|
|
@@ -427,7 +436,7 @@ export class Shaper extends EventEmitter {
|
|
|
427
436
|
};
|
|
428
437
|
|
|
429
438
|
const augmentedData = augmentDataSpace( item, contributionMeta );
|
|
430
|
-
const targetId = this.computeId( contributionTerms.$id, augmentedData );
|
|
439
|
+
const targetId = await this.computeId( contributionTerms.$id, augmentedData );
|
|
431
440
|
|
|
432
441
|
if ( !isOptionalContribution && ( !targetId || ( Array.isArray( targetId ) && !targetId.length ) ) ) {
|
|
433
442
|
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
|
|
@@ -446,13 +455,15 @@ export class Shaper extends EventEmitter {
|
|
|
446
455
|
|
|
447
456
|
|
|
448
457
|
// try computing variant ID based on contributing item
|
|
449
|
-
const variantId = this.computeVariantIdOfItem( contributionTerms.$variant, {
|
|
458
|
+
const variantId = await this.computeVariantIdOfItem( contributionTerms.$variant, {
|
|
450
459
|
...item,
|
|
451
460
|
$segments: item.$segments,
|
|
452
461
|
$original: item.$original,
|
|
453
462
|
$shape: targetModelShape,
|
|
454
463
|
$model: targetModelName,
|
|
455
|
-
$id:
|
|
464
|
+
$id: id,
|
|
465
|
+
$targetid: id,
|
|
466
|
+
$targetindex: index,
|
|
456
467
|
} );
|
|
457
468
|
|
|
458
469
|
// track contributions to selected target item or its variant
|
|
@@ -463,7 +474,7 @@ export class Shaper extends EventEmitter {
|
|
|
463
474
|
let target;
|
|
464
475
|
|
|
465
476
|
if ( !itemsPerModel.hasOwnProperty( targetModelName ) ) {
|
|
466
|
-
|
|
477
|
+
|
|
467
478
|
itemsPerModel[targetModelName] = {};
|
|
468
479
|
}
|
|
469
480
|
|
|
@@ -490,7 +501,15 @@ export class Shaper extends EventEmitter {
|
|
|
490
501
|
}
|
|
491
502
|
|
|
492
503
|
if ( !targetItem.$variants.hasOwnProperty( variantId ) ) {
|
|
493
|
-
|
|
504
|
+
// a variant target needs the same meta properties as a
|
|
505
|
+
// regular target item so it can be recompiled afterwards
|
|
506
|
+
targetItem.$variants[variantId] = Object.defineProperties( {}, {
|
|
507
|
+
$segments: { value: item.$segments },
|
|
508
|
+
$original: { value: {}, configurable: true },
|
|
509
|
+
$shape: { value: targetModelShape, configurable: true },
|
|
510
|
+
$model: { value: targetModelName },
|
|
511
|
+
$id: { value: id },
|
|
512
|
+
} );
|
|
494
513
|
}
|
|
495
514
|
|
|
496
515
|
target = targetItem.$variants[variantId];
|
|
@@ -499,7 +518,7 @@ export class Shaper extends EventEmitter {
|
|
|
499
518
|
|
|
500
519
|
// compile actual properties of contribution
|
|
501
520
|
const contributionDefaults = targetItemExists || variantId != null ? {} : targetModelShape.defaults;
|
|
502
|
-
const contribution = this.compileItem( augmentedData, contributionTerms, contributionDefaults );
|
|
521
|
+
const contribution = await this.compileItem( augmentedData, contributionTerms, contributionDefaults );
|
|
503
522
|
|
|
504
523
|
|
|
505
524
|
// merge this item's contribution with existing
|
|
@@ -523,7 +542,7 @@ export class Shaper extends EventEmitter {
|
|
|
523
542
|
if ( $isCollecting ) {
|
|
524
543
|
// contribution has been declared with one or more suffices, e.g. []
|
|
525
544
|
// -> collect value in target
|
|
526
|
-
if ( Array.isArray( target[name] ) ) { // eslint-disable-line
|
|
545
|
+
if ( Array.isArray( target[name] ) ) { // eslint-disable-line max-depth
|
|
527
546
|
target[name].push( value );
|
|
528
547
|
} else if ( target[name] == null ) {
|
|
529
548
|
target[name] = [value];
|
|
@@ -614,7 +633,7 @@ export class Shaper extends EventEmitter {
|
|
|
614
633
|
* @param {Set<string>} contributions lists IDs of items or their variants that have been contributed to
|
|
615
634
|
* @returns {Object<string,Object<string,ModelItem>>} pool of items per model as provided
|
|
616
635
|
*/
|
|
617
|
-
recompileContributionTargets( itemsPerModel, contributions ) {
|
|
636
|
+
async recompileContributionTargets( itemsPerModel, contributions ) {
|
|
618
637
|
for ( const track of contributions.values() ) {
|
|
619
638
|
const [ model, id, variantId ] = JSON.parse( track );
|
|
620
639
|
|
|
@@ -633,7 +652,7 @@ export class Shaper extends EventEmitter {
|
|
|
633
652
|
const item = modelPool[id];
|
|
634
653
|
|
|
635
654
|
if ( variantId == null ) {
|
|
636
|
-
modelPool[id] = this.recompileItem( item );
|
|
655
|
+
modelPool[id] = await this.recompileItem( item );
|
|
637
656
|
} else {
|
|
638
657
|
if ( !item.$variants ) {
|
|
639
658
|
console.warn( `inconsistency: missing variants of item ${id} of model ${model} though contributions have been tracked` );
|
|
@@ -647,7 +666,7 @@ export class Shaper extends EventEmitter {
|
|
|
647
666
|
continue;
|
|
648
667
|
}
|
|
649
668
|
|
|
650
|
-
variants[variantId] = this.recompileItem( variants[variantId] );
|
|
669
|
+
variants[variantId] = await this.recompileItem( variants[variantId] );
|
|
651
670
|
}
|
|
652
671
|
}
|
|
653
672
|
|
|
@@ -659,16 +678,16 @@ export class Shaper extends EventEmitter {
|
|
|
659
678
|
*
|
|
660
679
|
* @param {object} data source of record
|
|
661
680
|
* @param {Shape} shape shape of database affecting record
|
|
662
|
-
* @returns {string} name of model to use
|
|
681
|
+
* @returns {Promise<string>} name of model to use
|
|
663
682
|
*/
|
|
664
|
-
detectModel( data, shape ) {
|
|
683
|
+
async detectModel( data, shape ) {
|
|
665
684
|
const selector = shape?.common?.properties?.$model;
|
|
666
685
|
|
|
667
686
|
if ( !selector || typeof selector !== "string" ) {
|
|
668
687
|
throw new Error( "missing or invalid definition of common property $model in shape" );
|
|
669
688
|
}
|
|
670
689
|
|
|
671
|
-
const name = Compiler.compile( selector, this.termFunctions, this.termsCache )( data );
|
|
690
|
+
const name = await Compiler.compile( selector, this.termFunctions, this.termsCache )( data );
|
|
672
691
|
|
|
673
692
|
// support processed term requesting to fail model detection
|
|
674
693
|
if ( name instanceof Error ) {
|
|
@@ -687,10 +706,10 @@ export class Shaper extends EventEmitter {
|
|
|
687
706
|
*
|
|
688
707
|
* @param {object} data source of record
|
|
689
708
|
* @param {ShapeModel} modelShape shape of particular model
|
|
690
|
-
* @returns {string} ID of record
|
|
709
|
+
* @returns {Promise<string>} ID of record
|
|
691
710
|
*/
|
|
692
|
-
identifyRecord( data, modelShape ) {
|
|
693
|
-
const id = this.computeId( modelShape?.properties?.$id, data );
|
|
711
|
+
async identifyRecord( data, modelShape ) {
|
|
712
|
+
const id = await this.computeId( modelShape?.properties?.$id, data );
|
|
694
713
|
|
|
695
714
|
if ( !id ) {
|
|
696
715
|
throw new Error( `detecting ID of item related to record ${data.$segments.join( "/" )} failed` );
|
|
@@ -706,9 +725,9 @@ export class Shaper extends EventEmitter {
|
|
|
706
725
|
* @param {ShapeProperties} properties definition of resulting item's properties
|
|
707
726
|
* @param {ShapePropertyDefaults} defaults definition of default values for resulting item's properties
|
|
708
727
|
* @param {string[]} accept list names of properties to accept even when starting with a dollar sign
|
|
709
|
-
* @returns {
|
|
728
|
+
* @returns {Promise<Partial<ModelItem>>} compiled item
|
|
710
729
|
*/
|
|
711
|
-
compileItem( data, properties = {}, defaults = {}, accept = [] ) {
|
|
730
|
+
async compileItem( data, properties = {}, defaults = {}, accept = [] ) {
|
|
712
731
|
const item = {};
|
|
713
732
|
|
|
714
733
|
for ( const property of Object.keys( properties ) ) {
|
|
@@ -728,7 +747,7 @@ export class Shaper extends EventEmitter {
|
|
|
728
747
|
let value;
|
|
729
748
|
|
|
730
749
|
try {
|
|
731
|
-
value = Compiler.compile( term, this.termFunctions, this.termsCache )( data );
|
|
750
|
+
value = await Compiler.compile( term, this.termFunctions, this.termsCache )( data );
|
|
732
751
|
|
|
733
752
|
// support processed term requesting to fail computation of property's value
|
|
734
753
|
if ( value instanceof Error ) {
|
|
@@ -745,7 +764,7 @@ export class Shaper extends EventEmitter {
|
|
|
745
764
|
|
|
746
765
|
try {
|
|
747
766
|
const code = trimmed.startsWith( "=" ) ? trimmed.slice( 1 ) : undefined;
|
|
748
|
-
value = code ? Compiler.compile( code, this.termFunctions, this.termsCache )( data ) : defaultValue;
|
|
767
|
+
value = code ? await Compiler.compile( code, this.termFunctions, this.termsCache )( data ) : defaultValue;
|
|
749
768
|
|
|
750
769
|
// support processed term requesting to fail computation of default value
|
|
751
770
|
if ( value instanceof Error ) {
|
|
@@ -759,7 +778,7 @@ export class Shaper extends EventEmitter {
|
|
|
759
778
|
|
|
760
779
|
if ( value != null ) {
|
|
761
780
|
if ( isCollecting ) {
|
|
762
|
-
if ( Array.isArray( item[propertyName] ) ) {
|
|
781
|
+
if ( Array.isArray( item[propertyName] ) ) {
|
|
763
782
|
item[propertyName].push( value );
|
|
764
783
|
} else if ( item[propertyName] == null ) {
|
|
765
784
|
item[propertyName] = [value];
|
|
@@ -783,15 +802,15 @@ export class Shaper extends EventEmitter {
|
|
|
783
802
|
*
|
|
784
803
|
* @param {string} term term to process
|
|
785
804
|
* @param {object} data data space available to term processing
|
|
786
|
-
* @returns {any} processed term's result
|
|
805
|
+
* @returns {Promise<any>} processed term's result
|
|
787
806
|
*/
|
|
788
|
-
computeId( term, data ) {
|
|
807
|
+
async computeId( term, data ) {
|
|
789
808
|
if ( !term || typeof term !== "string" ) {
|
|
790
809
|
throw new Error( `missing or invalid term for computing ID of ${data.$segments.join( "/" )}` );
|
|
791
810
|
}
|
|
792
811
|
|
|
793
812
|
try {
|
|
794
|
-
const value = Compiler.compile( term, this.termFunctions, this.termsCache )( data );
|
|
813
|
+
const value = await Compiler.compile( term, this.termFunctions, this.termsCache )( data );
|
|
795
814
|
|
|
796
815
|
// support processed term requesting to fail computation of ID
|
|
797
816
|
if ( value instanceof Error ) {
|
|
@@ -812,7 +831,7 @@ export class Shaper extends EventEmitter {
|
|
|
812
831
|
* @param {Shape} mergedLocalShapes local shapes of source folders merged in order of collecting records
|
|
813
832
|
* @returns {DatabaseEndpoints} map of routes into data sets to expose eventually
|
|
814
833
|
*/
|
|
815
|
-
populateModels( endpoints, models, mergedLocalShapes ) {
|
|
834
|
+
async populateModels( endpoints, models, mergedLocalShapes ) {
|
|
816
835
|
const modelNames = Object.keys( models );
|
|
817
836
|
let index = 0;
|
|
818
837
|
|
|
@@ -834,10 +853,10 @@ export class Shaper extends EventEmitter {
|
|
|
834
853
|
throw new Error( `item ID "${itemId}" of model "${modelName}" results in invalid route for endpoint exposing it` );
|
|
835
854
|
}
|
|
836
855
|
|
|
837
|
-
endpoints[normalized] = model[itemId];
|
|
856
|
+
endpoints[normalized] = model[itemId];
|
|
838
857
|
}
|
|
839
858
|
|
|
840
|
-
this.populateModelCollections( endpoints, models, modelName, mergedLocalShapes );
|
|
859
|
+
await this.populateModelCollections( endpoints, models, modelName, mergedLocalShapes );
|
|
841
860
|
}
|
|
842
861
|
|
|
843
862
|
return endpoints;
|
|
@@ -853,26 +872,11 @@ export class Shaper extends EventEmitter {
|
|
|
853
872
|
* @param {Shape} mergedLocalShapes local shapes of source folders merged in order of collecting records
|
|
854
873
|
* @returns {DatabaseEndpoints} map of routes into data sets to expose eventually
|
|
855
874
|
*/
|
|
856
|
-
populateModelCollections( endpoints, models, modelName, mergedLocalShapes ) {
|
|
875
|
+
async populateModelCollections( endpoints, models, modelName, mergedLocalShapes ) {
|
|
857
876
|
const finalModelShape = merge( {}, mergedLocalShapes.common, mergedLocalShapes.models[modelName] );
|
|
858
877
|
const collections = finalModelShape.collections || {};
|
|
859
|
-
const cache = {};
|
|
860
|
-
|
|
861
|
-
// prepare to process collection instances prior to collection references
|
|
862
878
|
const keys = Object.keys( collections );
|
|
863
879
|
|
|
864
|
-
keys.sort( ( l, r ) => {
|
|
865
|
-
if ( l.reference ) {
|
|
866
|
-
cache[l.reference] = null;
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
if ( r.reference ) {
|
|
870
|
-
cache[r.reference] = null;
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
return l.reference ? r.reference ? 0 : 1 : r.reference ? -1 : 0;
|
|
874
|
-
} );
|
|
875
|
-
|
|
876
880
|
// process collection by collection
|
|
877
881
|
let index = 0;
|
|
878
882
|
|
|
@@ -889,28 +893,20 @@ export class Shaper extends EventEmitter {
|
|
|
889
893
|
|
|
890
894
|
const definition = collections[route];
|
|
891
895
|
if ( definition && typeof definition === "object" ) {
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
//
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
collection = this.compileCollection( models, modelName, definition, normalized );
|
|
901
|
-
isKeyed = definition.key && typeof definition.key === "string";
|
|
902
|
-
|
|
903
|
-
Object.defineProperties( collection, {
|
|
904
|
-
$isKeyed: { value: isKeyed },
|
|
905
|
-
} );
|
|
906
|
-
|
|
907
|
-
if ( cache.hasOwnProperty( normalized ) ) {
|
|
908
|
-
cache[route] = collection;
|
|
909
|
-
}
|
|
896
|
+
// The `reference` rule for re-using another collection's extracted
|
|
897
|
+
// items has been retired: it never worked correctly and is
|
|
898
|
+
// superseded by YAML anchors/aliases for sharing definitions.
|
|
899
|
+
// (Check for presence, not truthiness — the documented usage was
|
|
900
|
+
// `reference: ""` addressing the unnamed root collection.)
|
|
901
|
+
if ( Object.prototype.hasOwnProperty.call( definition, "reference" ) ) {
|
|
902
|
+
throw new Error( `collection "${normalized}" of model "${modelName}" uses the retired "reference" rule; `
|
|
903
|
+
+ `share collection definitions via YAML anchors/aliases instead` );
|
|
910
904
|
}
|
|
911
905
|
|
|
912
|
-
const
|
|
913
|
-
collection = this.
|
|
906
|
+
const isKeyed = definition.key && typeof definition.key === "string";
|
|
907
|
+
let collection = await this.compileCollection( models, modelName, definition, normalized );
|
|
908
|
+
|
|
909
|
+
collection = this.sortCollection( models, modelName, definition, normalized, collection, isKeyed );
|
|
914
910
|
|
|
915
911
|
// provide separate endpoints per key of collection?
|
|
916
912
|
ptnKey.lastIndex = 0;
|
|
@@ -968,12 +964,12 @@ export class Shaper extends EventEmitter {
|
|
|
968
964
|
const items = collection.items;
|
|
969
965
|
|
|
970
966
|
for ( let offset = 0; offset < items.length; offset += limit ) {
|
|
971
|
-
const normalized = normalizePathname( prefix + route.replace( ptnOffset, offset ) );
|
|
972
|
-
endpoints[normalized] = { count: items.length, items: items.slice( offset, offset + limit ) };
|
|
967
|
+
const normalized = normalizePathname( prefix + route.replace( ptnOffset, String( offset ) ) );
|
|
968
|
+
endpoints[normalized] = { count: items.length, items: items.slice( offset, offset + limit ) };
|
|
973
969
|
}
|
|
974
970
|
} else if ( isKeyed ) {
|
|
975
971
|
const normalized = normalizePathname( this.prefix + modelName + "/" + route );
|
|
976
|
-
const base = endpoints[normalized] = {};
|
|
972
|
+
const base = endpoints[normalized] = {};
|
|
977
973
|
|
|
978
974
|
for ( const key of Object.keys( collection ) ) {
|
|
979
975
|
const items = collection[key].items;
|
|
@@ -987,14 +983,14 @@ export class Shaper extends EventEmitter {
|
|
|
987
983
|
// expose separate endpoint per group of items
|
|
988
984
|
const subNormalized = normalizePathname( this.prefix + modelName + "/" + route + "/" + key );
|
|
989
985
|
|
|
990
|
-
endpoints[subNormalized] = { count: items.length, items };
|
|
986
|
+
endpoints[subNormalized] = { count: items.length, items };
|
|
991
987
|
}
|
|
992
988
|
}
|
|
993
989
|
} else {
|
|
994
990
|
const normalized = normalizePathname( this.prefix + modelName + "/" + route );
|
|
995
991
|
const items = collection.items;
|
|
996
992
|
|
|
997
|
-
endpoints[normalized] = { count: items.length, items };
|
|
993
|
+
endpoints[normalized] = { count: items.length, items };
|
|
998
994
|
}
|
|
999
995
|
}
|
|
1000
996
|
|
|
@@ -1036,7 +1032,7 @@ export class Shaper extends EventEmitter {
|
|
|
1036
1032
|
* @param {string} route route for resulting collection
|
|
1037
1033
|
* @returns {DatabaseCollection} compiled collection
|
|
1038
1034
|
*/
|
|
1039
|
-
compileCollection( models, modelName, definition, route ) {
|
|
1035
|
+
async compileCollection( models, modelName, definition, route ) {
|
|
1040
1036
|
const model = models[modelName];
|
|
1041
1037
|
const { key: $key, filter: $filter, properties: $properties } = definition;
|
|
1042
1038
|
const isKeyed = $key && typeof $key === "string";
|
|
@@ -1107,9 +1103,9 @@ export class Shaper extends EventEmitter {
|
|
|
1107
1103
|
context.$id = id;
|
|
1108
1104
|
|
|
1109
1105
|
// ignore this item or prepare its spot in resulting collection?
|
|
1110
|
-
if ( !filterFn || filterFn( data ) ) {
|
|
1106
|
+
if ( !filterFn || await filterFn( data ) ) {
|
|
1111
1107
|
if ( isKeyed ) {
|
|
1112
|
-
const key = keyFn( data );
|
|
1108
|
+
const key = await keyFn( data );
|
|
1113
1109
|
if ( key != null ) {
|
|
1114
1110
|
const _key = String( key );
|
|
1115
1111
|
|
|
@@ -1135,7 +1131,7 @@ export class Shaper extends EventEmitter {
|
|
|
1135
1131
|
if ( propertiesFn ) {
|
|
1136
1132
|
// compile custom representation of item in collection
|
|
1137
1133
|
for ( const fn of propertiesFn ) {
|
|
1138
|
-
extracted[fn.target] = fn( data );
|
|
1134
|
+
extracted[fn.target] = await fn( data );
|
|
1139
1135
|
}
|
|
1140
1136
|
} else {
|
|
1141
1137
|
// expose all properties of item in collection, too
|
|
@@ -1153,7 +1149,7 @@ export class Shaper extends EventEmitter {
|
|
|
1153
1149
|
}
|
|
1154
1150
|
|
|
1155
1151
|
/**
|
|
1156
|
-
* Sorts provided collection
|
|
1152
|
+
* Sorts provided collection in-place.
|
|
1157
1153
|
*
|
|
1158
1154
|
* @param {Object<string,Object<string,ModelItem>>} models pool of items per discovered model
|
|
1159
1155
|
* @param {string} modelName name of model to process
|
|
@@ -1161,10 +1157,9 @@ export class Shaper extends EventEmitter {
|
|
|
1161
1157
|
* @param {string} route route for resulting collection
|
|
1162
1158
|
* @param {DatabaseCollection} collection collection to sort
|
|
1163
1159
|
* @param {boolean} isKeyed true if collection is a keyed collection instead of regular one
|
|
1164
|
-
* @
|
|
1165
|
-
* @returns {DatabaseCollection} sorted collection, can be same as `collection` or some copy of it
|
|
1160
|
+
* @returns {DatabaseCollection} provided collection sorted in-place
|
|
1166
1161
|
*/
|
|
1167
|
-
sortCollection( models, modelName, collectionDefinition, route, collection, isKeyed
|
|
1162
|
+
sortCollection( models, modelName, collectionDefinition, route, collection, isKeyed ) {
|
|
1168
1163
|
let $sort = collectionDefinition.sort;
|
|
1169
1164
|
|
|
1170
1165
|
try {
|
|
@@ -1213,7 +1208,7 @@ export class Shaper extends EventEmitter {
|
|
|
1213
1208
|
|
|
1214
1209
|
if ( _tl === _tr ) {
|
|
1215
1210
|
if ( _tl === "number" ) {
|
|
1216
|
-
return dir * (
|
|
1211
|
+
return dir * ( _l - _r );
|
|
1217
1212
|
}
|
|
1218
1213
|
|
|
1219
1214
|
return dir * String( _l ).localeCompare( String( _r ) );
|
|
@@ -1231,30 +1226,17 @@ export class Shaper extends EventEmitter {
|
|
|
1231
1226
|
};
|
|
1232
1227
|
}
|
|
1233
1228
|
|
|
1234
|
-
const sorted = copy ? {} : collection;
|
|
1235
|
-
|
|
1236
1229
|
if ( isKeyed ) {
|
|
1237
1230
|
for ( const key in collection ) {
|
|
1238
1231
|
if ( collection.hasOwnProperty( key ) ) {
|
|
1239
|
-
|
|
1240
|
-
const items = collection[key].items.slice( 0 );
|
|
1241
|
-
items.sort( sorterFn );
|
|
1242
|
-
sorted[key] = { items };
|
|
1243
|
-
} else {
|
|
1244
|
-
collection[key].items.sort( sorterFn );
|
|
1245
|
-
}
|
|
1232
|
+
collection[key].items.sort( sorterFn );
|
|
1246
1233
|
}
|
|
1247
1234
|
}
|
|
1248
|
-
} else if ( copy ) {
|
|
1249
|
-
const items = collection.items.slice( 0 );
|
|
1250
|
-
items.sort( sorterFn );
|
|
1251
|
-
sorted.items = items;
|
|
1252
1235
|
} else {
|
|
1253
1236
|
collection.items.sort( sorterFn );
|
|
1254
1237
|
}
|
|
1255
1238
|
|
|
1256
|
-
|
|
1257
|
-
return sorted;
|
|
1239
|
+
return collection;
|
|
1258
1240
|
}
|
|
1259
1241
|
}
|
|
1260
1242
|
|
package/package.json
CHANGED
|
@@ -1,45 +1,53 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cepharum/concrete-db",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "a read-only web database generator",
|
|
5
|
-
"
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.mjs",
|
|
6
7
|
"types": "concrete-db.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./concrete-db.d.ts",
|
|
11
|
+
"import": "./lib/index.mjs"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
7
15
|
"bin": {
|
|
8
16
|
"cure": "cure.mjs"
|
|
9
17
|
},
|
|
10
18
|
"scripts": {
|
|
11
19
|
"cure": "node ./cure.mjs",
|
|
12
|
-
"lint": "eslint
|
|
13
|
-
"lint:fix": "eslint --fix
|
|
14
|
-
"test": "
|
|
20
|
+
"lint": "eslint .",
|
|
21
|
+
"lint:fix": "eslint --fix .",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"test:watch": "vitest",
|
|
24
|
+
"coverage": "vitest run --coverage",
|
|
15
25
|
"test:getting-started": "node ./test/curing.shell.mjs",
|
|
16
|
-
"docs:dev": "
|
|
17
|
-
"docs:build": "
|
|
26
|
+
"docs:dev": "vitepress dev docs",
|
|
27
|
+
"docs:build": "vitepress build docs"
|
|
18
28
|
},
|
|
19
29
|
"keywords": [],
|
|
20
30
|
"author": "cepharum GmbH",
|
|
21
31
|
"license": "MIT",
|
|
22
32
|
"dependencies": {
|
|
23
|
-
"ajv": "^8.11.0",
|
|
24
33
|
"file-essentials": "^0.1.2",
|
|
25
34
|
"lodash.merge": "^4.6.2",
|
|
26
|
-
"minimist": "^1.2.
|
|
35
|
+
"minimist": "^1.2.8",
|
|
27
36
|
"promise-essentials": "^0.2.0",
|
|
28
|
-
"simple-terms": "^0.
|
|
29
|
-
"yaml": "^2.
|
|
37
|
+
"simple-terms": "^0.5.0",
|
|
38
|
+
"yaml": "^2.9.0"
|
|
30
39
|
},
|
|
31
40
|
"devDependencies": {
|
|
32
|
-
"
|
|
33
|
-
"eslint": "^
|
|
34
|
-
"eslint-config-cepharum": "^
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"vuepress-plugin-mermaidjs": "^1.9.1"
|
|
41
|
+
"@vitest/coverage-v8": "^4.1.9",
|
|
42
|
+
"eslint": "^10.6.0",
|
|
43
|
+
"eslint-config-cepharum": "^2.0.2",
|
|
44
|
+
"mermaid": "^11.16.0",
|
|
45
|
+
"vitepress": "^1.6.4",
|
|
46
|
+
"vitepress-plugin-mermaid": "^2.0.17",
|
|
47
|
+
"vitest": "^4.1.9"
|
|
40
48
|
},
|
|
41
49
|
"engines": {
|
|
42
|
-
"node": ">=
|
|
50
|
+
"node": ">=20"
|
|
43
51
|
},
|
|
44
52
|
"files": [
|
|
45
53
|
"lib",
|