@cepharum/concrete-db 0.3.0 → 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 +48 -68
- package/package.json +27 -19
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
|
@@ -172,7 +172,7 @@ export class Shaper extends EventEmitter {
|
|
|
172
172
|
metaProperties.$id = await this.identifyRecord( augmentedData, metaProperties.$shape );
|
|
173
173
|
|
|
174
174
|
if ( !itemsPerModel.hasOwnProperty( metaProperties.$model ) ) {
|
|
175
|
-
|
|
175
|
+
|
|
176
176
|
itemsPerModel[metaProperties.$model] = {};
|
|
177
177
|
}
|
|
178
178
|
|
|
@@ -257,8 +257,15 @@ export class Shaper extends EventEmitter {
|
|
|
257
257
|
*/
|
|
258
258
|
getShapeOfModel( shapesPerModelCache, localShape, modelName ) {
|
|
259
259
|
if ( !shapesPerModelCache.hasOwnProperty( modelName ) ) {
|
|
260
|
-
|
|
261
|
-
|
|
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
|
+
);
|
|
262
269
|
}
|
|
263
270
|
|
|
264
271
|
return shapesPerModelCache[modelName];
|
|
@@ -454,7 +461,9 @@ export class Shaper extends EventEmitter {
|
|
|
454
461
|
$original: item.$original,
|
|
455
462
|
$shape: targetModelShape,
|
|
456
463
|
$model: targetModelName,
|
|
457
|
-
$id:
|
|
464
|
+
$id: id,
|
|
465
|
+
$targetid: id,
|
|
466
|
+
$targetindex: index,
|
|
458
467
|
} );
|
|
459
468
|
|
|
460
469
|
// track contributions to selected target item or its variant
|
|
@@ -465,7 +474,7 @@ export class Shaper extends EventEmitter {
|
|
|
465
474
|
let target;
|
|
466
475
|
|
|
467
476
|
if ( !itemsPerModel.hasOwnProperty( targetModelName ) ) {
|
|
468
|
-
|
|
477
|
+
|
|
469
478
|
itemsPerModel[targetModelName] = {};
|
|
470
479
|
}
|
|
471
480
|
|
|
@@ -492,7 +501,15 @@ export class Shaper extends EventEmitter {
|
|
|
492
501
|
}
|
|
493
502
|
|
|
494
503
|
if ( !targetItem.$variants.hasOwnProperty( variantId ) ) {
|
|
495
|
-
|
|
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
|
+
} );
|
|
496
513
|
}
|
|
497
514
|
|
|
498
515
|
target = targetItem.$variants[variantId];
|
|
@@ -525,7 +542,7 @@ export class Shaper extends EventEmitter {
|
|
|
525
542
|
if ( $isCollecting ) {
|
|
526
543
|
// contribution has been declared with one or more suffices, e.g. []
|
|
527
544
|
// -> collect value in target
|
|
528
|
-
if ( Array.isArray( target[name] ) ) { // eslint-disable-line
|
|
545
|
+
if ( Array.isArray( target[name] ) ) { // eslint-disable-line max-depth
|
|
529
546
|
target[name].push( value );
|
|
530
547
|
} else if ( target[name] == null ) {
|
|
531
548
|
target[name] = [value];
|
|
@@ -761,7 +778,7 @@ export class Shaper extends EventEmitter {
|
|
|
761
778
|
|
|
762
779
|
if ( value != null ) {
|
|
763
780
|
if ( isCollecting ) {
|
|
764
|
-
if ( Array.isArray( item[propertyName] ) ) {
|
|
781
|
+
if ( Array.isArray( item[propertyName] ) ) {
|
|
765
782
|
item[propertyName].push( value );
|
|
766
783
|
} else if ( item[propertyName] == null ) {
|
|
767
784
|
item[propertyName] = [value];
|
|
@@ -836,7 +853,7 @@ export class Shaper extends EventEmitter {
|
|
|
836
853
|
throw new Error( `item ID "${itemId}" of model "${modelName}" results in invalid route for endpoint exposing it` );
|
|
837
854
|
}
|
|
838
855
|
|
|
839
|
-
endpoints[normalized] = model[itemId];
|
|
856
|
+
endpoints[normalized] = model[itemId];
|
|
840
857
|
}
|
|
841
858
|
|
|
842
859
|
await this.populateModelCollections( endpoints, models, modelName, mergedLocalShapes );
|
|
@@ -858,23 +875,8 @@ export class Shaper extends EventEmitter {
|
|
|
858
875
|
async populateModelCollections( endpoints, models, modelName, mergedLocalShapes ) {
|
|
859
876
|
const finalModelShape = merge( {}, mergedLocalShapes.common, mergedLocalShapes.models[modelName] );
|
|
860
877
|
const collections = finalModelShape.collections || {};
|
|
861
|
-
const cache = {};
|
|
862
|
-
|
|
863
|
-
// prepare to process collection instances prior to collection references
|
|
864
878
|
const keys = Object.keys( collections );
|
|
865
879
|
|
|
866
|
-
keys.sort( ( l, r ) => {
|
|
867
|
-
if ( l.reference ) {
|
|
868
|
-
cache[l.reference] = null;
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
if ( r.reference ) {
|
|
872
|
-
cache[r.reference] = null;
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
return l.reference ? r.reference ? 0 : 1 : r.reference ? -1 : 0;
|
|
876
|
-
} );
|
|
877
|
-
|
|
878
880
|
// process collection by collection
|
|
879
881
|
let index = 0;
|
|
880
882
|
|
|
@@ -891,28 +893,20 @@ export class Shaper extends EventEmitter {
|
|
|
891
893
|
|
|
892
894
|
const definition = collections[route];
|
|
893
895
|
if ( definition && typeof definition === "object" ) {
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
//
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
collection = await this.compileCollection( models, modelName, definition, normalized );
|
|
903
|
-
isKeyed = definition.key && typeof definition.key === "string";
|
|
904
|
-
|
|
905
|
-
Object.defineProperties( collection, {
|
|
906
|
-
$isKeyed: { value: isKeyed },
|
|
907
|
-
} );
|
|
908
|
-
|
|
909
|
-
if ( cache.hasOwnProperty( normalized ) ) {
|
|
910
|
-
cache[route] = collection;
|
|
911
|
-
}
|
|
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` );
|
|
912
904
|
}
|
|
913
905
|
|
|
914
|
-
const
|
|
915
|
-
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 );
|
|
916
910
|
|
|
917
911
|
// provide separate endpoints per key of collection?
|
|
918
912
|
ptnKey.lastIndex = 0;
|
|
@@ -971,11 +965,11 @@ export class Shaper extends EventEmitter {
|
|
|
971
965
|
|
|
972
966
|
for ( let offset = 0; offset < items.length; offset += limit ) {
|
|
973
967
|
const normalized = normalizePathname( prefix + route.replace( ptnOffset, String( offset ) ) );
|
|
974
|
-
endpoints[normalized] = { count: items.length, items: items.slice( offset, offset + limit ) };
|
|
968
|
+
endpoints[normalized] = { count: items.length, items: items.slice( offset, offset + limit ) };
|
|
975
969
|
}
|
|
976
970
|
} else if ( isKeyed ) {
|
|
977
971
|
const normalized = normalizePathname( this.prefix + modelName + "/" + route );
|
|
978
|
-
const base = endpoints[normalized] = {};
|
|
972
|
+
const base = endpoints[normalized] = {};
|
|
979
973
|
|
|
980
974
|
for ( const key of Object.keys( collection ) ) {
|
|
981
975
|
const items = collection[key].items;
|
|
@@ -989,14 +983,14 @@ export class Shaper extends EventEmitter {
|
|
|
989
983
|
// expose separate endpoint per group of items
|
|
990
984
|
const subNormalized = normalizePathname( this.prefix + modelName + "/" + route + "/" + key );
|
|
991
985
|
|
|
992
|
-
endpoints[subNormalized] = { count: items.length, items };
|
|
986
|
+
endpoints[subNormalized] = { count: items.length, items };
|
|
993
987
|
}
|
|
994
988
|
}
|
|
995
989
|
} else {
|
|
996
990
|
const normalized = normalizePathname( this.prefix + modelName + "/" + route );
|
|
997
991
|
const items = collection.items;
|
|
998
992
|
|
|
999
|
-
endpoints[normalized] = { count: items.length, items };
|
|
993
|
+
endpoints[normalized] = { count: items.length, items };
|
|
1000
994
|
}
|
|
1001
995
|
}
|
|
1002
996
|
|
|
@@ -1155,7 +1149,7 @@ export class Shaper extends EventEmitter {
|
|
|
1155
1149
|
}
|
|
1156
1150
|
|
|
1157
1151
|
/**
|
|
1158
|
-
* Sorts provided collection
|
|
1152
|
+
* Sorts provided collection in-place.
|
|
1159
1153
|
*
|
|
1160
1154
|
* @param {Object<string,Object<string,ModelItem>>} models pool of items per discovered model
|
|
1161
1155
|
* @param {string} modelName name of model to process
|
|
@@ -1163,10 +1157,9 @@ export class Shaper extends EventEmitter {
|
|
|
1163
1157
|
* @param {string} route route for resulting collection
|
|
1164
1158
|
* @param {DatabaseCollection} collection collection to sort
|
|
1165
1159
|
* @param {boolean} isKeyed true if collection is a keyed collection instead of regular one
|
|
1166
|
-
* @
|
|
1167
|
-
* @returns {DatabaseCollection} sorted collection, can be same as `collection` or some copy of it
|
|
1160
|
+
* @returns {DatabaseCollection} provided collection sorted in-place
|
|
1168
1161
|
*/
|
|
1169
|
-
sortCollection( models, modelName, collectionDefinition, route, collection, isKeyed
|
|
1162
|
+
sortCollection( models, modelName, collectionDefinition, route, collection, isKeyed ) {
|
|
1170
1163
|
let $sort = collectionDefinition.sort;
|
|
1171
1164
|
|
|
1172
1165
|
try {
|
|
@@ -1215,7 +1208,7 @@ export class Shaper extends EventEmitter {
|
|
|
1215
1208
|
|
|
1216
1209
|
if ( _tl === _tr ) {
|
|
1217
1210
|
if ( _tl === "number" ) {
|
|
1218
|
-
return dir * (
|
|
1211
|
+
return dir * ( _l - _r );
|
|
1219
1212
|
}
|
|
1220
1213
|
|
|
1221
1214
|
return dir * String( _l ).localeCompare( String( _r ) );
|
|
@@ -1233,30 +1226,17 @@ export class Shaper extends EventEmitter {
|
|
|
1233
1226
|
};
|
|
1234
1227
|
}
|
|
1235
1228
|
|
|
1236
|
-
const sorted = copy ? {} : collection;
|
|
1237
|
-
|
|
1238
1229
|
if ( isKeyed ) {
|
|
1239
1230
|
for ( const key in collection ) {
|
|
1240
1231
|
if ( collection.hasOwnProperty( key ) ) {
|
|
1241
|
-
|
|
1242
|
-
const items = collection[key].items.slice( 0 );
|
|
1243
|
-
items.sort( sorterFn );
|
|
1244
|
-
sorted[key] = { items };
|
|
1245
|
-
} else {
|
|
1246
|
-
collection[key].items.sort( sorterFn );
|
|
1247
|
-
}
|
|
1232
|
+
collection[key].items.sort( sorterFn );
|
|
1248
1233
|
}
|
|
1249
1234
|
}
|
|
1250
|
-
} else if ( copy ) {
|
|
1251
|
-
const items = collection.items.slice( 0 );
|
|
1252
|
-
items.sort( sorterFn );
|
|
1253
|
-
sorted.items = items;
|
|
1254
1235
|
} else {
|
|
1255
1236
|
collection.items.sort( sorterFn );
|
|
1256
1237
|
}
|
|
1257
1238
|
|
|
1258
|
-
|
|
1259
|
-
return sorted;
|
|
1239
|
+
return collection;
|
|
1260
1240
|
}
|
|
1261
1241
|
}
|
|
1262
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.12.0",
|
|
24
33
|
"file-essentials": "^0.1.2",
|
|
25
34
|
"lodash.merge": "^4.6.2",
|
|
26
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",
|