@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2022 cepharum GmbH
3
+ Copyright (c) 2026 cepharum GmbH
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
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 all existing files from output folder
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
- let pathname = Path.resolve( ".", library );
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
- if ( /^[a-z]:/i.test( pathname ) ) {
66
- pathname = "file://" + pathname.replace( /\//g, "%2F" );
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
- const name = fullPath.replace( /\.json$/, "" );
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 ( !database.hasOwnProperty( name ) ) {
60
- return stat.isDirectory() ? File.promises.rmdir( fullPath ) : File.promises.unlink( fullPath );
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
- // FIXME implement actually deep freezing of provided object
101
- return Object.freeze( object );
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 models = this.handleContributions( this.groupByModel( recordsCollection ), mergedLocalShapes );
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 {object} instance of model with hidden meta information injected
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
- // eslint-disable-next-line no-param-reassign
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
- // eslint-disable-next-line no-param-reassign
259
- shapesPerModelCache[modelName] = merge( { defaults: DefaultShape.common.defaults }, localShape?.common, localShape?.models?.[modelName] );
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: targetId,
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
- // eslint-disable-next-line no-param-reassign
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
- targetItem.$variants[variantId] = {};
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 no-lonely-if,max-depth
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 {object} compiled item
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] ) ) { // eslint-disable-line no-lonely-if
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]; // eslint-disable-line no-param-reassign
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
- let collection;
893
- let isKeyed;
894
-
895
- // compile (and cache) collection or re-use collection found in cache?
896
- if ( definition.reference ) {
897
- collection = cache[definition.reference];
898
- isKeyed = collection.$isKeyed;
899
- } else {
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 isCached = definition.reference || cache.hasOwnProperty( route );
913
- collection = this.sortCollection( models, modelName, definition, normalized, collection, isKeyed, isCached );
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 ) }; // eslint-disable-line no-param-reassign
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] = {}; // eslint-disable-line no-param-reassign
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 }; // eslint-disable-line no-param-reassign
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 }; // eslint-disable-line no-param-reassign
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 either in-place or as a copy.
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
- * @param {boolean} copy true if collection must not be sorted in-place
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, copy = false ) {
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 * ( _tl - _tr );
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
- if ( copy ) {
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.2.2",
3
+ "version": "0.4.0",
4
4
  "description": "a read-only web database generator",
5
- "main": "lib/collector.mjs",
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 lib/**/*.{js,mjs,cjs} test/**/*.{js,mjs,cjs} cure.mjs",
13
- "lint:fix": "eslint --fix lib/**/*.{js,mjs,cjs} test/**/*.{js,mjs,cjs} cure.mjs",
14
- "test": "c8 mocha --ui=tdd --recursive test/**/*.spec.*",
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": "vuepress dev docs",
17
- "docs:build": "vuepress build docs"
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.6",
35
+ "minimist": "^1.2.8",
27
36
  "promise-essentials": "^0.2.0",
28
- "simple-terms": "^0.4.0",
29
- "yaml": "^2.1.0"
37
+ "simple-terms": "^0.5.0",
38
+ "yaml": "^2.9.0"
30
39
  },
31
40
  "devDependencies": {
32
- "c8": "^7.11.3",
33
- "eslint": "^8.16.0",
34
- "eslint-config-cepharum": "^1.0.12",
35
- "eslint-plugin-promise": "^6.0.0",
36
- "mocha": "^10.0.0",
37
- "should": "^13.2.3",
38
- "vuepress": "^1.9.7",
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": ">=10"
50
+ "node": ">=20"
43
51
  },
44
52
  "files": [
45
53
  "lib",