@reldens/storage 0.117.0 → 0.119.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.
Files changed (78) hide show
  1. package/.claude/test-architecture.md +27 -17
  2. package/CLAUDE.md +67 -11
  3. package/README.md +204 -22
  4. package/bin/reldens-storage.js +43 -10
  5. package/index.js +53 -12
  6. package/lib/drizzle/drizzle-data-server.js +123 -0
  7. package/lib/drizzle/drizzle-driver.js +228 -0
  8. package/lib/drizzle/drizzle-models-generation.js +49 -0
  9. package/lib/drizzle/drizzle-modules-loader.js +32 -0
  10. package/lib/drizzle/drizzle-modules-validator.js +74 -0
  11. package/lib/entities-generator.js +36 -7
  12. package/lib/entity-templates/drizzle-model.template +28 -0
  13. package/lib/entity-templates/query-builder-model.template +17 -0
  14. package/lib/generators/base-generator.js +13 -0
  15. package/lib/generators/entities-generation.js +0 -13
  16. package/lib/generators/models-generation.js +179 -680
  17. package/lib/generators/relations-detection.js +171 -0
  18. package/lib/knex/knex-data-server.js +128 -0
  19. package/lib/knex/knex-driver.js +157 -0
  20. package/lib/knex/knex-modules-loader.js +28 -0
  21. package/lib/knex/knex-modules-validator.js +45 -0
  22. package/lib/kysely/kysely-data-server.js +128 -0
  23. package/lib/kysely/kysely-driver.js +176 -0
  24. package/lib/kysely/kysely-modules-loader.js +28 -0
  25. package/lib/kysely/kysely-modules-validator.js +53 -0
  26. package/lib/mikro-orm/mikro-orm-data-server.js +48 -29
  27. package/lib/mikro-orm/mikro-orm-driver.js +317 -108
  28. package/lib/mikro-orm/mikro-orm-models-generation.js +178 -0
  29. package/lib/mikro-orm/mikro-orm-modules-loader.js +50 -0
  30. package/lib/mikro-orm/mikro-orm-modules-validator.js +46 -0
  31. package/lib/mysql2-connection-config.js +38 -0
  32. package/lib/objection-js/objection-js-data-server.js +71 -125
  33. package/lib/objection-js/objection-js-driver.js +4 -1
  34. package/lib/objection-js/objection-js-models-generation.js +98 -0
  35. package/lib/objection-js/objection-modules-loader.js +28 -0
  36. package/lib/objection-js/objection-modules-validator.js +31 -0
  37. package/lib/package-resolver.js +45 -0
  38. package/lib/prisma/prisma-client-loader.js +11 -1
  39. package/lib/prisma/prisma-driver.js +16 -2
  40. package/lib/prisma/prisma-filter-processor.js +5 -6
  41. package/lib/prisma/prisma-models-generation.js +236 -0
  42. package/lib/prisma/prisma-schema-generator.js +5 -1
  43. package/lib/prisma/prisma-type-caster.js +14 -0
  44. package/lib/query-builder-driver.js +210 -0
  45. package/lib/query-builder-models-generation.js +80 -0
  46. package/lib/relations-loader.js +192 -0
  47. package/lib/type-mapper.js +38 -0
  48. package/package.json +8 -8
  49. package/tests/.env.test.example +6 -0
  50. package/tests/fixtures/categories-fixtures.js +8 -0
  51. package/tests/fixtures/expected-entities/entities/test-product-details-entity.js +10 -1
  52. package/tests/fixtures/expected-entities/entities-translations.js +2 -0
  53. package/tests/fixtures/expected-entities-mikro-orm/models/mikro-orm/test-categories-model.js +13 -8
  54. package/tests/fixtures/expected-entities-mikro-orm/models/mikro-orm/test-product-details-model.js +30 -10
  55. package/tests/fixtures/expected-entities-mikro-orm/models/mikro-orm/test-products-model.js +15 -14
  56. package/tests/fixtures/expected-entities-mikro-orm/models/mikro-orm/test-reviews-model.js +13 -12
  57. package/tests/fixtures/expected-entities-prisma/models/prisma/test-product-details-model.js +3 -1
  58. package/tests/fixtures/product-details-fixtures.js +4 -1
  59. package/tests/fixtures/reviews-fixtures.js +2 -1
  60. package/tests/fixtures/sql/test-schema.sql +11 -2
  61. package/tests/fixtures/table-columns-fixtures.js +97 -0
  62. package/tests/integration/test-cross-driver-equivalence.js +286 -0
  63. package/tests/integration/test-nested-filters.js +265 -408
  64. package/tests/integration/test-relations.js +135 -0
  65. package/tests/integration/test-reldens-sample-data.js +255 -0
  66. package/tests/integration/test-reldens-shape-relations.js +292 -0
  67. package/tests/run-tests.js +111 -65
  68. package/tests/unit/test-drivers.js +7 -1
  69. package/tests/unit/test-models-generation.js +28 -18
  70. package/tests/utils/column-value-assert.js +139 -0
  71. package/tests/utils/entity-projection.js +111 -0
  72. package/tests/utils/observed-value.js +41 -0
  73. package/tests/utils/projection-difference.js +151 -0
  74. package/tests/utils/relations-shape-support.js +104 -0
  75. package/tests/utils/reldens-schema-loader.js +274 -0
  76. package/tests/utils/test-helpers.js +143 -13
  77. package/tests/utils/test-runner.js +26 -5
  78. package/tests/utils/whole-result-assert.js +243 -0
@@ -0,0 +1,139 @@
1
+ /**
2
+ *
3
+ * Reldens - Column Value Assert
4
+ * Value and type assertion of a single database column against the value the fixture row was inserted with.
5
+ * Both sides are reduced to the same canonical form, identical for every driver, so a driver returning
6
+ * another representation of the stored value fails stating what it actually returned.
7
+ *
8
+ */
9
+
10
+ const assert = require('node:assert');
11
+ const { ObservedValue } = require('./observed-value');
12
+ const { TableColumnsFixtures } = require('../fixtures/table-columns-fixtures');
13
+ const { sc } = require('@reldens/utils');
14
+
15
+ class ColumnValueAssert
16
+ {
17
+
18
+ static anyValue = {reldensAnyColumnValue: true};
19
+
20
+ static assertColumns(actualRecord, tableName, expectedRow, label)
21
+ {
22
+ let columns = TableColumnsFixtures[tableName].columns;
23
+ for(let columnName of Object.keys(columns)){
24
+ this.assertColumn(
25
+ actualRecord[columnName],
26
+ expectedRow[columnName],
27
+ columns[columnName],
28
+ label+'.'+columnName
29
+ );
30
+ }
31
+ return true;
32
+ }
33
+
34
+ static assertColumn(actualValue, expectedValue, columnType, label)
35
+ {
36
+ if(ColumnValueAssert.anyValue === expectedValue){
37
+ this.assertColumnType(actualValue, columnType, label);
38
+ return true;
39
+ }
40
+ if(null === expectedValue){
41
+ assert.strictEqual(
42
+ actualValue,
43
+ null,
44
+ label+' expected null, observed: '+ObservedValue.describe(actualValue)
45
+ );
46
+ return true;
47
+ }
48
+ assert.strictEqual(
49
+ this.canonicalValue(actualValue, columnType),
50
+ this.canonicalValue(expectedValue, columnType),
51
+ label+' expected '+ObservedValue.describe(expectedValue)
52
+ +' but observed: '+ObservedValue.describe(actualValue)
53
+ );
54
+ this.assertColumnType(actualValue, columnType, label);
55
+ return true;
56
+ }
57
+
58
+ static assertColumnType(actualValue, columnType, label)
59
+ {
60
+ assert.ok(
61
+ null !== actualValue,
62
+ label+' is null but the column holds a value, observed: '+ObservedValue.describe(actualValue)
63
+ );
64
+ if('date' === columnType){
65
+ assert.ok(
66
+ actualValue instanceof Date,
67
+ label+' is not a Date instance, observed: '+ObservedValue.describe(actualValue)
68
+ );
69
+ return true;
70
+ }
71
+ if('json' === columnType){
72
+ assert.strictEqual(
73
+ typeof actualValue,
74
+ 'object',
75
+ label+' is not a parsed json value, observed: '+ObservedValue.describe(actualValue)
76
+ );
77
+ return true;
78
+ }
79
+ assert.strictEqual(
80
+ typeof actualValue,
81
+ columnType,
82
+ label+' must be a '+columnType+', observed: '+ObservedValue.describe(actualValue)
83
+ );
84
+ return true;
85
+ }
86
+
87
+ static canonicalValue(value, columnType)
88
+ {
89
+ if('json' === columnType){
90
+ return this.canonicalJson(value);
91
+ }
92
+ if(value instanceof Date){
93
+ return value.toISOString();
94
+ }
95
+ if(sc.isObject(value) || sc.isArray(value)){
96
+ return sc.toJsonString(this.sortedJsonValue(value));
97
+ }
98
+ if('number' === columnType && null !== value){
99
+ return String(Number(value));
100
+ }
101
+ return String(value);
102
+ }
103
+
104
+ static canonicalJson(value)
105
+ {
106
+ let parsedValue = value;
107
+ if(sc.isString(value)){
108
+ parsedValue = sc.parseJson(value, value);
109
+ }
110
+ return sc.toJsonString(this.sortedJsonValue(parsedValue));
111
+ }
112
+
113
+ static sortedJsonValue(value)
114
+ {
115
+ if(sc.isArray(value)){
116
+ return this.sortedJsonList(value);
117
+ }
118
+ if(!sc.isObject(value)){
119
+ return value;
120
+ }
121
+ let sortedObject = {};
122
+ for(let key of Object.keys(value).sort()){
123
+ sortedObject[key] = this.sortedJsonValue(value[key]);
124
+ }
125
+ return sortedObject;
126
+ }
127
+
128
+ static sortedJsonList(value)
129
+ {
130
+ let sortedList = [];
131
+ for(let item of value){
132
+ sortedList.push(this.sortedJsonValue(item));
133
+ }
134
+ return sortedList;
135
+ }
136
+
137
+ }
138
+
139
+ module.exports.ColumnValueAssert = ColumnValueAssert;
@@ -0,0 +1,111 @@
1
+ /**
2
+ *
3
+ * Reldens - Entity Projection
4
+ * Turns any driver result (plain row, ORM managed entity, ORM collection) into a normalised plain object
5
+ * projection: own enumerable keys sorted, dates as tagged strings, collections as arrays, nested entities
6
+ * projected recursively. Two projections built from the same database rows must be strictly equal on every
7
+ * driver, so any divergence found between them is a driver contract divergence.
8
+ *
9
+ */
10
+
11
+ const { sc } = require('@reldens/utils');
12
+
13
+ class EntityProjection
14
+ {
15
+
16
+ static maxDepth = 12;
17
+ static functionLabel = 'function';
18
+ static circularReferenceLabel = 'circular-reference';
19
+ static maxDepthLabel = 'max-depth-reached';
20
+ static uninitializedCollectionLabel = 'uninitialized-collection';
21
+
22
+ static project(value, parentVisited = null, depth = 0)
23
+ {
24
+ let visited = null === parentVisited ? new Set() : parentVisited;
25
+ if(null === value){
26
+ return null;
27
+ }
28
+ if('undefined' === typeof value){
29
+ return null;
30
+ }
31
+ if('function' === typeof value){
32
+ return EntityProjection.functionLabel;
33
+ }
34
+ if('bigint' === typeof value){
35
+ return 'bigint:'+String(value);
36
+ }
37
+ if('object' !== typeof value){
38
+ return value;
39
+ }
40
+ if(value instanceof Date){
41
+ return 'date:'+value.toISOString();
42
+ }
43
+ if(Buffer.isBuffer(value)){
44
+ return 'buffer:'+value.length;
45
+ }
46
+ if(EntityProjection.maxDepth < depth){
47
+ return EntityProjection.maxDepthLabel;
48
+ }
49
+ if(visited.has(value)){
50
+ return EntityProjection.circularReferenceLabel;
51
+ }
52
+ return this.projectContainer(value, visited, depth);
53
+ }
54
+
55
+ static projectContainer(value, visited, depth)
56
+ {
57
+ visited.add(value);
58
+ let projected = this.projectByShape(value, visited, depth);
59
+ visited.delete(value);
60
+ return projected;
61
+ }
62
+
63
+ static projectByShape(value, visited, depth)
64
+ {
65
+ if(sc.isArray(value)){
66
+ return this.projectArray(value, visited, depth);
67
+ }
68
+ if(this.isCollection(value)){
69
+ return this.projectCollection(value, visited, depth);
70
+ }
71
+ return this.projectObject(value, visited, depth);
72
+ }
73
+
74
+ static isCollection(value)
75
+ {
76
+ if(!sc.isFunction(value.getItems)){
77
+ return false;
78
+ }
79
+ return sc.isFunction(value.isInitialized);
80
+ }
81
+
82
+ static projectCollection(value, visited, depth)
83
+ {
84
+ if(!value.isInitialized()){
85
+ return EntityProjection.uninitializedCollectionLabel;
86
+ }
87
+ return this.projectArray(value.getItems(), visited, depth);
88
+ }
89
+
90
+ static projectArray(value, visited, depth)
91
+ {
92
+ let projected = [];
93
+ for(let item of value){
94
+ projected.push(this.project(item, visited, depth + 1));
95
+ }
96
+ return projected;
97
+ }
98
+
99
+ static projectObject(value, visited, depth)
100
+ {
101
+ let projected = {};
102
+ let keys = Object.keys(value).sort();
103
+ for(let key of keys){
104
+ projected[key] = this.project(value[key], visited, depth + 1);
105
+ }
106
+ return projected;
107
+ }
108
+
109
+ }
110
+
111
+ module.exports.EntityProjection = EntityProjection;
@@ -0,0 +1,41 @@
1
+ /**
2
+ *
3
+ * Reldens - Observed Value
4
+ * Shared diagnostics helper: turns any runtime value into a readable shape description and logs it, so a
5
+ * failing assertion message states what was actually observed instead of only what was expected.
6
+ *
7
+ */
8
+
9
+ const { Logger } = require('@reldens/utils');
10
+
11
+ class ObservedValue
12
+ {
13
+
14
+ static describe(value)
15
+ {
16
+ if(null === value){
17
+ return 'null';
18
+ }
19
+ let valueType = typeof value;
20
+ if('object' !== valueType){
21
+ return valueType+'('+String(value)+')';
22
+ }
23
+ if(Array.isArray(value)){
24
+ return 'Array[length='+value.length+']('+value.join(',')+')';
25
+ }
26
+ if(!value.constructor){
27
+ return 'NoConstructor{'+Object.keys(value).join(',')+'}';
28
+ }
29
+ return value.constructor.name+'{'+Object.keys(value).join(',')+'}';
30
+ }
31
+
32
+ static log(label, value)
33
+ {
34
+ let described = this.describe(value);
35
+ Logger.debug(' OBSERVED '+label+' => '+described);
36
+ return described;
37
+ }
38
+
39
+ }
40
+
41
+ module.exports.ObservedValue = ObservedValue;
@@ -0,0 +1,151 @@
1
+ /**
2
+ *
3
+ * Reldens - Projection Difference
4
+ * Compares two normalised entity projections and returns every path where they diverge, so a cross driver
5
+ * assertion can name the exact key that broke the contract instead of only stating that the results differ.
6
+ * Keys listed as volatile (database generated timestamps) are compared by type only, since every driver
7
+ * writes them at a different moment.
8
+ *
9
+ */
10
+
11
+ const { sc } = require('@reldens/utils');
12
+
13
+ class ProjectionDifference
14
+ {
15
+
16
+ static missingKeyLabel = 'key-not-present';
17
+
18
+ static collect(expected, actual, volatileKeys)
19
+ {
20
+ let differences = [];
21
+ this.compareValues(expected, actual, 'result', differences, volatileKeys);
22
+ return differences;
23
+ }
24
+
25
+ static compareValues(expected, actual, path, differences, volatileKeys)
26
+ {
27
+ if(this.isVolatilePath(path, volatileKeys)){
28
+ this.compareTypes(expected, actual, path, differences);
29
+ return;
30
+ }
31
+ if(this.typeName(expected) !== this.typeName(actual)){
32
+ differences.push({path, expected: this.render(expected), actual: this.render(actual)});
33
+ return;
34
+ }
35
+ if(sc.isArray(expected)){
36
+ this.compareArrays(expected, actual, path, differences, volatileKeys);
37
+ return;
38
+ }
39
+ if(sc.isObject(expected)){
40
+ this.compareObjects(expected, actual, path, differences, volatileKeys);
41
+ return;
42
+ }
43
+ if(expected === actual){
44
+ return;
45
+ }
46
+ differences.push({path, expected: this.render(expected), actual: this.render(actual)});
47
+ }
48
+
49
+ static compareArrays(expected, actual, path, differences, volatileKeys)
50
+ {
51
+ if(expected.length !== actual.length){
52
+ differences.push({
53
+ path: path+'.length',
54
+ expected: this.render(expected.length),
55
+ actual: this.render(actual.length)
56
+ });
57
+ return;
58
+ }
59
+ for(let index = 0; index < expected.length; index++){
60
+ this.compareValues(expected[index], actual[index], path+'['+index+']', differences, volatileKeys);
61
+ }
62
+ }
63
+
64
+ static compareObjects(expected, actual, path, differences, volatileKeys)
65
+ {
66
+ for(let key of Object.keys(expected)){
67
+ if(!sc.hasOwn(actual, key)){
68
+ differences.push({
69
+ path: path+'.'+key,
70
+ expected: this.render(expected[key]),
71
+ actual: ProjectionDifference.missingKeyLabel
72
+ });
73
+ continue;
74
+ }
75
+ this.compareValues(expected[key], actual[key], path+'.'+key, differences, volatileKeys);
76
+ }
77
+ for(let key of Object.keys(actual)){
78
+ if(sc.hasOwn(expected, key)){
79
+ continue;
80
+ }
81
+ differences.push({
82
+ path: path+'.'+key,
83
+ expected: ProjectionDifference.missingKeyLabel,
84
+ actual: this.render(actual[key])
85
+ });
86
+ }
87
+ }
88
+
89
+ static compareTypes(expected, actual, path, differences)
90
+ {
91
+ if(this.typeName(expected) === this.typeName(actual)){
92
+ return;
93
+ }
94
+ differences.push({path, expected: this.render(expected), actual: this.render(actual)});
95
+ }
96
+
97
+ static isVolatilePath(path, volatileKeys)
98
+ {
99
+ if(!sc.isArray(volatileKeys)){
100
+ return false;
101
+ }
102
+ return -1 !== volatileKeys.indexOf(path.split('.').pop());
103
+ }
104
+
105
+ static typeName(value)
106
+ {
107
+ if(null === value){
108
+ return 'null';
109
+ }
110
+ if(sc.isArray(value)){
111
+ return 'array';
112
+ }
113
+ if(sc.isObject(value)){
114
+ return 'object';
115
+ }
116
+ return typeof value;
117
+ }
118
+
119
+ static render(value)
120
+ {
121
+ if(null === value){
122
+ return 'null';
123
+ }
124
+ if(sc.isArray(value)){
125
+ return 'array(length='+value.length+')';
126
+ }
127
+ if(sc.isObject(value)){
128
+ return 'object(keys='+Object.keys(value).join('|')+')';
129
+ }
130
+ return typeof value+'('+String(value)+')';
131
+ }
132
+
133
+ static describe(driverName, referenceDriverName, entityName, methodLabel, differences)
134
+ {
135
+ let lines = [];
136
+ for(let difference of differences){
137
+ lines.push(
138
+ 'driver: '+driverName
139
+ +' | method: '+methodLabel
140
+ +' | entity: '+entityName
141
+ +' | key: '+difference.path
142
+ +' | '+referenceDriverName+' returned: '+difference.expected
143
+ +' | '+driverName+' returned: '+difference.actual
144
+ );
145
+ }
146
+ return lines;
147
+ }
148
+
149
+ }
150
+
151
+ module.exports.ProjectionDifference = ProjectionDifference;
@@ -0,0 +1,104 @@
1
+ /**
2
+ *
3
+ * Reldens - Relations Shape Support
4
+ * Reusable fixtures setup and relation/foreign-key assertions for the child -> parent -> grandparent chain
5
+ * (test_reviews -> test_products -> test_categories), both links being NOT NULL m:1 foreign keys.
6
+ * Every assertion reports the observed value so a failure states what the driver actually returned.
7
+ *
8
+ */
9
+
10
+ const { TestHelpers } = require('./test-helpers');
11
+ const { ObservedValue } = require('./observed-value');
12
+ const { assert } = require('./test-runner');
13
+ const { CategoriesFixtures } = require('../fixtures/categories-fixtures');
14
+ const { ProductsFixtures } = require('../fixtures/products-fixtures');
15
+ const { ReviewsFixtures } = require('../fixtures/reviews-fixtures');
16
+
17
+ class RelationsShapeSupport
18
+ {
19
+
20
+ constructor(dataServer, repos)
21
+ {
22
+ this.dataServer = dataServer;
23
+ this.repos = {
24
+ child: repos.testReviews,
25
+ parent: repos.testProducts,
26
+ grandParent: repos.testCategories
27
+ };
28
+ }
29
+
30
+ async insertChainFixtures()
31
+ {
32
+ await TestHelpers.cleanDatabase(this.dataServer);
33
+ let fixturesByTable = {
34
+ test_categories: [CategoriesFixtures.category_relations_1, CategoriesFixtures.category_relations_2],
35
+ test_products: [ProductsFixtures.product_relations_1, ProductsFixtures.product_relations_2],
36
+ test_reviews: [ReviewsFixtures.review_relations_1, ReviewsFixtures.review_relations_2]
37
+ };
38
+ for(let tableName of Object.keys(fixturesByTable)){
39
+ await TestHelpers.insertFixturesViaRawSQL(this.dataServer, tableName, fixturesByTable[tableName]);
40
+ }
41
+ return true;
42
+ }
43
+
44
+ cleared(repoKey)
45
+ {
46
+ if(this.dataServer.orm && this.dataServer.orm.em){
47
+ this.dataServer.orm.em.clear();
48
+ }
49
+ return this.repos[repoKey];
50
+ }
51
+
52
+ async loadChildWithHint(relationsHint)
53
+ {
54
+ return await this.cleared('child').loadOneByWithRelations(
55
+ 'product_id',
56
+ ProductsFixtures.product_relations_1.id,
57
+ relationsHint
58
+ );
59
+ }
60
+
61
+ assertEntityColumns(entityValue, expectedColumns, label)
62
+ {
63
+ let described = ObservedValue.log(label, entityValue);
64
+ assert.ok(entityValue, label+' is falsy, the relation was not populated, observed: '+described);
65
+ assert.notStrictEqual('number', typeof entityValue, label+' is a RAW NUMBER, observed: '+described);
66
+ assert.strictEqual('object', typeof entityValue, label+' is not an entity, observed: '+described);
67
+ for(let columnName of Object.keys(expectedColumns)){
68
+ assert.strictEqual(
69
+ entityValue[columnName],
70
+ expectedColumns[columnName],
71
+ label+'.'+columnName+' observed: '+ObservedValue.describe(entityValue[columnName])
72
+ +' - expected: '+String(expectedColumns[columnName])
73
+ );
74
+ }
75
+ return true;
76
+ }
77
+
78
+ assertNumericColumn(actualValue, expectedValue, label)
79
+ {
80
+ let described = ObservedValue.log(label, actualValue);
81
+ assert.ok(null !== actualValue, label+' is null, observed: '+described);
82
+ assert.strictEqual(Number(actualValue), expectedValue, label+' observed: '+described);
83
+ return true;
84
+ }
85
+
86
+ async assertStoredColumn(repoKey, id, columnName, expectedValue, label)
87
+ {
88
+ let record = await this.cleared(repoKey).loadOneBy('id', id);
89
+ assert.ok(record, label+' row was not found in the database, id: '+id);
90
+ this.assertNumericColumn(record[columnName], expectedValue, label+'.'+columnName+' re-read from database');
91
+ let filtered = await this.cleared(repoKey).loadBy(columnName, expectedValue);
92
+ let filteredIds = filtered.map((row) => Number(row.id));
93
+ ObservedValue.log(label+' ids where '+columnName+' = '+expectedValue, filteredIds);
94
+ assert.ok(
95
+ filteredIds.includes(Number(id)),
96
+ label+' id '+id+' was not returned when filtering by '+columnName+' = '+expectedValue
97
+ +', so that column value is not stored in the database; observed ids: '+filteredIds.join(',')
98
+ );
99
+ return true;
100
+ }
101
+
102
+ }
103
+
104
+ module.exports.RelationsShapeSupport = RelationsShapeSupport;