@reldens/storage 0.116.0 → 0.118.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/.claude/test-architecture.md +27 -17
- package/CLAUDE.md +67 -11
- package/README.md +204 -22
- package/bin/reldens-storage.js +43 -10
- package/index.js +53 -12
- package/lib/drizzle/drizzle-data-server.js +123 -0
- package/lib/drizzle/drizzle-driver.js +228 -0
- package/lib/drizzle/drizzle-models-generation.js +49 -0
- package/lib/drizzle/drizzle-modules-loader.js +32 -0
- package/lib/drizzle/drizzle-modules-validator.js +74 -0
- package/lib/entities-generator.js +36 -7
- package/lib/entity-templates/drizzle-model.template +28 -0
- package/lib/entity-templates/query-builder-model.template +17 -0
- package/lib/generators/base-generator.js +13 -0
- package/lib/generators/entities-generation.js +0 -13
- package/lib/generators/models-generation.js +179 -693
- package/lib/generators/relations-detection.js +171 -0
- package/lib/knex/knex-data-server.js +128 -0
- package/lib/knex/knex-driver.js +157 -0
- package/lib/knex/knex-modules-loader.js +28 -0
- package/lib/knex/knex-modules-validator.js +45 -0
- package/lib/kysely/kysely-data-server.js +128 -0
- package/lib/kysely/kysely-driver.js +176 -0
- package/lib/kysely/kysely-modules-loader.js +28 -0
- package/lib/kysely/kysely-modules-validator.js +53 -0
- package/lib/mikro-orm/mikro-orm-data-server.js +48 -29
- package/lib/mikro-orm/mikro-orm-driver.js +385 -121
- package/lib/mikro-orm/mikro-orm-models-generation.js +178 -0
- package/lib/mikro-orm/mikro-orm-modules-loader.js +50 -0
- package/lib/mikro-orm/mikro-orm-modules-validator.js +46 -0
- package/lib/mysql2-connection-config.js +38 -0
- package/lib/objection-js/objection-js-data-server.js +71 -125
- package/lib/objection-js/objection-js-driver.js +4 -1
- package/lib/objection-js/objection-js-models-generation.js +98 -0
- package/lib/objection-js/objection-modules-loader.js +28 -0
- package/lib/objection-js/objection-modules-validator.js +31 -0
- package/lib/package-resolver.js +45 -0
- package/lib/prisma/prisma-client-loader.js +11 -1
- package/lib/prisma/prisma-driver.js +16 -2
- package/lib/prisma/prisma-filter-processor.js +5 -6
- package/lib/prisma/prisma-models-generation.js +236 -0
- package/lib/prisma/prisma-schema-generator.js +5 -1
- package/lib/prisma/prisma-type-caster.js +14 -0
- package/lib/query-builder-driver.js +210 -0
- package/lib/query-builder-models-generation.js +80 -0
- package/lib/relations-loader.js +192 -0
- package/lib/type-mapper.js +38 -0
- package/package.json +7 -7
- package/tests/.env.test.example +6 -0
- package/tests/fixtures/categories-fixtures.js +8 -0
- package/tests/fixtures/expected-entities/entities/test-product-details-entity.js +72 -0
- package/tests/fixtures/expected-entities/entities-config.js +6 -4
- package/tests/fixtures/expected-entities/entities-translations.js +25 -14
- package/tests/fixtures/expected-entities-mikro-orm/models/mikro-orm/registered-models-mikro-orm.js +2 -0
- package/tests/fixtures/expected-entities-mikro-orm/models/mikro-orm/test-categories-model.js +13 -8
- package/tests/fixtures/expected-entities-mikro-orm/models/mikro-orm/test-product-details-model.js +80 -0
- package/tests/fixtures/expected-entities-mikro-orm/models/mikro-orm/test-products-model.js +20 -14
- package/tests/fixtures/expected-entities-mikro-orm/models/mikro-orm/test-reviews-model.js +13 -12
- package/tests/fixtures/expected-entities-objection-js/models/objection-js/registered-models-objection-js.js +2 -0
- package/tests/fixtures/expected-entities-objection-js/models/objection-js/test-product-details-model.js +33 -0
- package/tests/fixtures/expected-entities-objection-js/models/objection-js/test-products-model.js +9 -0
- package/tests/fixtures/expected-entities-prisma/models/prisma/registered-models-prisma.js +2 -0
- package/tests/fixtures/expected-entities-prisma/models/prisma/test-product-details-model.js +43 -0
- package/tests/fixtures/expected-entities-prisma/models/prisma/test-products-model.js +2 -0
- package/tests/fixtures/product-details-fixtures.js +22 -0
- package/tests/fixtures/reviews-fixtures.js +2 -1
- package/tests/fixtures/sql/test-schema.sql +23 -1
- package/tests/fixtures/table-columns-fixtures.js +97 -0
- package/tests/integration/test-cross-driver-equivalence.js +286 -0
- package/tests/integration/test-nested-filters.js +265 -408
- package/tests/integration/test-relations.js +199 -0
- package/tests/integration/test-reldens-sample-data.js +255 -0
- package/tests/integration/test-reldens-shape-relations.js +292 -0
- package/tests/run-tests.js +112 -60
- package/tests/unit/test-drivers.js +7 -1
- package/tests/unit/test-models-generation.js +174 -0
- package/tests/utils/column-value-assert.js +139 -0
- package/tests/utils/driver-registry.js +1 -1
- package/tests/utils/entity-projection.js +111 -0
- package/tests/utils/observed-value.js +41 -0
- package/tests/utils/projection-difference.js +151 -0
- package/tests/utils/relations-shape-support.js +104 -0
- package/tests/utils/reldens-schema-loader.js +274 -0
- package/tests/utils/test-helpers.js +152 -19
- package/tests/utils/test-runner.js +26 -5
- package/tests/utils/whole-result-assert.js +243 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Reldens - Cross Driver Equivalence Test
|
|
4
|
+
* Runs the exact same repository call on every active driver against the very same fixture rows and fails
|
|
5
|
+
* when the normalised results are not equivalent, naming the driver, the method, the entity and the key
|
|
6
|
+
* that diverged. The per driver suites prove that each driver works on its own, this suite proves that all
|
|
7
|
+
* of them answer the same thing for the same call.
|
|
8
|
+
*
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const assert = require('node:assert');
|
|
12
|
+
const { TestRunner } = require('../utils/test-runner');
|
|
13
|
+
const { TestHelpers } = require('../utils/test-helpers');
|
|
14
|
+
const { EntityProjection } = require('../utils/entity-projection');
|
|
15
|
+
const { ProjectionDifference } = require('../utils/projection-difference');
|
|
16
|
+
const { CategoriesFixtures } = require('../fixtures/categories-fixtures');
|
|
17
|
+
const { ProductsFixtures } = require('../fixtures/products-fixtures');
|
|
18
|
+
const { ReviewsFixtures } = require('../fixtures/reviews-fixtures');
|
|
19
|
+
const { ProductDetailsFixtures } = require('../fixtures/product-details-fixtures');
|
|
20
|
+
const { Logger } = require('@reldens/utils');
|
|
21
|
+
|
|
22
|
+
class CrossDriverEquivalenceTest
|
|
23
|
+
{
|
|
24
|
+
|
|
25
|
+
constructor(driverRegistry, driverNames)
|
|
26
|
+
{
|
|
27
|
+
this.driverRegistry = driverRegistry;
|
|
28
|
+
this.driverNames = driverNames;
|
|
29
|
+
this.runner = new TestRunner();
|
|
30
|
+
this.drivers = [];
|
|
31
|
+
this.referenceDriverName = '';
|
|
32
|
+
this.referenceDataServer = false;
|
|
33
|
+
this.minimumDrivers = 2;
|
|
34
|
+
this.volatileKeys = ['created_at', 'updated_at'];
|
|
35
|
+
this.categoryId = CategoriesFixtures.category_relations_1.id;
|
|
36
|
+
this.categorySlug = CategoriesFixtures.category_relations_1.slug;
|
|
37
|
+
this.secondCategoryId = CategoriesFixtures.category_relations_2.id;
|
|
38
|
+
this.productId = ProductsFixtures.product_relations_1.id;
|
|
39
|
+
this.productDetailsId = ProductDetailsFixtures.product_details_relations_1.id;
|
|
40
|
+
this.productsHint = ['related_test_products'];
|
|
41
|
+
this.categoriesHint = ['related_test_categories'];
|
|
42
|
+
this.productsReviewsHint = ['related_test_products.related_test_reviews'];
|
|
43
|
+
this.productsCategoriesHint = ['related_test_products.related_test_categories'];
|
|
44
|
+
this.commaSeparatedHint = 'related_test_categories,related_test_reviews';
|
|
45
|
+
this.updatePatch = {name: 'Cross Driver Updated'};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async run()
|
|
49
|
+
{
|
|
50
|
+
this.runner.suite('Cross Driver Equivalence');
|
|
51
|
+
if(!this.collectDrivers()){
|
|
52
|
+
return this.runner.fail(
|
|
53
|
+
'cross driver equivalence must have at least '+this.minimumDrivers+' active drivers',
|
|
54
|
+
'Only '+this.drivers.length+' driver(s) were available.'
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
await this.runAllGroups();
|
|
58
|
+
return this.runner.getResults();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
collectDrivers()
|
|
62
|
+
{
|
|
63
|
+
for(let driverName of this.driverNames){
|
|
64
|
+
let dataServer = this.driverRegistry.getDriver(driverName);
|
|
65
|
+
if(!dataServer){
|
|
66
|
+
Logger.warning('Cross driver equivalence: driver '+driverName+' is not available.');
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
this.drivers.push({driverName, dataServer, repos: this.driverRegistry.getRepos(driverName)});
|
|
70
|
+
}
|
|
71
|
+
if(this.minimumDrivers > this.drivers.length){
|
|
72
|
+
Logger.warning('Cross driver equivalence needs at least two active drivers, tests were skipped.');
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
let referenceDriver = [...this.drivers].shift();
|
|
76
|
+
this.referenceDriverName = referenceDriver.driverName;
|
|
77
|
+
this.referenceDataServer = referenceDriver.dataServer;
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async runAllGroups()
|
|
82
|
+
{
|
|
83
|
+
await this.insertSharedFixtures();
|
|
84
|
+
await this.testPlainReads();
|
|
85
|
+
await this.testRelationReads();
|
|
86
|
+
await this.testCounts();
|
|
87
|
+
await this.testWrites();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async insertSharedFixtures()
|
|
91
|
+
{
|
|
92
|
+
await TestHelpers.cleanDatabase(this.referenceDataServer);
|
|
93
|
+
this.clearIdentityMaps();
|
|
94
|
+
let fixturesByTable = {
|
|
95
|
+
test_categories: [CategoriesFixtures.category_relations_1, CategoriesFixtures.category_relations_2],
|
|
96
|
+
test_products: [ProductsFixtures.product_relations_1, ProductsFixtures.product_relations_2],
|
|
97
|
+
test_reviews: [ReviewsFixtures.review_relations_1, ReviewsFixtures.review_relations_2],
|
|
98
|
+
test_product_details: [ProductDetailsFixtures.product_details_relations_1]
|
|
99
|
+
};
|
|
100
|
+
for(let tableName of Object.keys(fixturesByTable)){
|
|
101
|
+
await TestHelpers.insertFixturesViaRawSQL(
|
|
102
|
+
this.referenceDataServer,
|
|
103
|
+
tableName,
|
|
104
|
+
fixturesByTable[tableName]
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
clearIdentityMaps()
|
|
110
|
+
{
|
|
111
|
+
for(let driverEntry of this.drivers){
|
|
112
|
+
if(!driverEntry.dataServer.orm){
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if(!driverEntry.dataServer.orm.em){
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
driverEntry.dataServer.orm.em.clear();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async collectFailures(entityName, methodLabel, driverCall, resetFixtures)
|
|
123
|
+
{
|
|
124
|
+
let referenceProjection = null;
|
|
125
|
+
let failures = [];
|
|
126
|
+
for(let driverEntry of this.drivers){
|
|
127
|
+
if(resetFixtures){
|
|
128
|
+
await this.insertSharedFixtures();
|
|
129
|
+
}
|
|
130
|
+
this.clearIdentityMaps();
|
|
131
|
+
let projection = EntityProjection.project(await driverCall(driverEntry.repos[entityName]));
|
|
132
|
+
if(driverEntry.driverName === this.referenceDriverName){
|
|
133
|
+
referenceProjection = projection;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
let differences = ProjectionDifference.collect(referenceProjection, projection, this.volatileKeys);
|
|
137
|
+
failures.push(...ProjectionDifference.describe(
|
|
138
|
+
driverEntry.driverName,
|
|
139
|
+
this.referenceDriverName,
|
|
140
|
+
entityName,
|
|
141
|
+
methodLabel,
|
|
142
|
+
differences
|
|
143
|
+
));
|
|
144
|
+
}
|
|
145
|
+
return failures;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async assertEquivalent(entityName, methodLabel, driverCall, resetFixtures)
|
|
149
|
+
{
|
|
150
|
+
let testName = methodLabel+' on '+entityName+' returns an equivalent result on every driver';
|
|
151
|
+
await this.runner.test(testName, async () => {
|
|
152
|
+
let failures = await this.collectFailures(entityName, methodLabel, driverCall, resetFixtures);
|
|
153
|
+
assert.strictEqual(failures.length, 0, this.buildReport(failures));
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
buildReport(failures)
|
|
158
|
+
{
|
|
159
|
+
if(0 === failures.length){
|
|
160
|
+
return '';
|
|
161
|
+
}
|
|
162
|
+
return failures.length+' difference(s) against the reference driver '+this.referenceDriverName
|
|
163
|
+
+':\n '+failures.join('\n ');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async runCases(cases, resetFixtures)
|
|
167
|
+
{
|
|
168
|
+
for(let testCase of cases){
|
|
169
|
+
await this.assertEquivalent(testCase.entity, testCase.label, testCase.call, resetFixtures);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async testPlainReads()
|
|
174
|
+
{
|
|
175
|
+
this.runner.group('Plain reads');
|
|
176
|
+
await this.runCases([
|
|
177
|
+
{entity: 'testCategories', label: 'loadById', call: (repo) => repo.loadById(this.categoryId)},
|
|
178
|
+
{entity: 'testCategories', label: 'loadBy', call: (repo) => repo.loadBy('is_active', 1)},
|
|
179
|
+
{entity: 'testCategories', label: 'loadAll', call: (repo) => repo.loadAll()},
|
|
180
|
+
{entity: 'testProducts', label: 'loadById', call: (repo) => repo.loadById(this.productId)},
|
|
181
|
+
{entity: 'testProducts', label: 'loadAll', call: (repo) => repo.loadAll()},
|
|
182
|
+
{entity: 'testReviews', label: 'loadAll', call: (repo) => repo.loadAll()},
|
|
183
|
+
{
|
|
184
|
+
entity: 'testCategories',
|
|
185
|
+
label: 'loadOneBy',
|
|
186
|
+
call: (repo) => repo.loadOneBy('slug', this.categorySlug)
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
entity: 'testProductDetails',
|
|
190
|
+
label: 'loadById',
|
|
191
|
+
call: (repo) => repo.loadById(this.productDetailsId)
|
|
192
|
+
}
|
|
193
|
+
], false);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async testRelationReads()
|
|
197
|
+
{
|
|
198
|
+
this.runner.group('Relation reads');
|
|
199
|
+
await this.runCases([
|
|
200
|
+
{
|
|
201
|
+
entity: 'testCategories',
|
|
202
|
+
label: 'loadByIdWithRelations one level hint',
|
|
203
|
+
call: (repo) => repo.loadByIdWithRelations(this.categoryId, this.productsHint)
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
entity: 'testCategories',
|
|
207
|
+
label: 'loadOneByWithRelations one level hint',
|
|
208
|
+
call: (repo) => repo.loadOneByWithRelations('slug', this.categorySlug, this.productsHint)
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
entity: 'testCategories',
|
|
212
|
+
label: 'loadAllWithRelations one level hint',
|
|
213
|
+
call: (repo) => repo.loadAllWithRelations(this.productsHint)
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
entity: 'testCategories',
|
|
217
|
+
label: 'loadWithRelations two levels hint',
|
|
218
|
+
call: (repo) => repo.loadWithRelations({id: this.categoryId}, this.productsReviewsHint)
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
entity: 'testProductDetails',
|
|
222
|
+
label: 'loadByIdWithRelations two levels hint',
|
|
223
|
+
call: (repo) => repo.loadByIdWithRelations(this.productDetailsId, this.productsCategoriesHint)
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
entity: 'testProducts',
|
|
227
|
+
label: 'loadWithRelations comma separated string hint',
|
|
228
|
+
call: (repo) => repo.loadWithRelations({id: this.productId}, this.commaSeparatedHint)
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
entity: 'testProducts',
|
|
232
|
+
label: 'loadAllWithRelations comma separated string hint',
|
|
233
|
+
call: (repo) => repo.loadAllWithRelations(this.commaSeparatedHint)
|
|
234
|
+
}
|
|
235
|
+
], false);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async testCounts()
|
|
239
|
+
{
|
|
240
|
+
this.runner.group('Counts');
|
|
241
|
+
await this.runCases([
|
|
242
|
+
{entity: 'testCategories', label: 'count empty filters', call: (repo) => repo.count({})},
|
|
243
|
+
{entity: 'testCategories', label: 'count filtered', call: (repo) => repo.count({is_active: 1})},
|
|
244
|
+
{
|
|
245
|
+
entity: 'testCategories',
|
|
246
|
+
label: 'countWithRelations one level hint',
|
|
247
|
+
call: (repo) => repo.countWithRelations({}, this.productsHint)
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
entity: 'testProducts',
|
|
251
|
+
label: 'countWithRelations one level hint',
|
|
252
|
+
call: (repo) => repo.countWithRelations({}, this.categoriesHint)
|
|
253
|
+
}
|
|
254
|
+
], false);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async testWrites()
|
|
258
|
+
{
|
|
259
|
+
this.runner.group('Writes');
|
|
260
|
+
await this.runCases([
|
|
261
|
+
{
|
|
262
|
+
entity: 'testCategories',
|
|
263
|
+
label: 'create',
|
|
264
|
+
call: (repo) => repo.create({...CategoriesFixtures.category_cross_driver_create})
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
entity: 'testCategories',
|
|
268
|
+
label: 'updateById',
|
|
269
|
+
call: (repo) => repo.updateById(this.categoryId, {...this.updatePatch})
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
entity: 'testCategories',
|
|
273
|
+
label: 'delete',
|
|
274
|
+
call: (repo) => repo.delete({id: this.secondCategoryId})
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
entity: 'testCategories',
|
|
278
|
+
label: 'deleteById',
|
|
279
|
+
call: (repo) => repo.deleteById(this.secondCategoryId)
|
|
280
|
+
}
|
|
281
|
+
], true);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
module.exports.CrossDriverEquivalenceTest = CrossDriverEquivalenceTest;
|