@reldens/storage 0.87.0 → 0.89.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 (65) hide show
  1. package/.claude/driver-methods-complete-analysis.md +1978 -0
  2. package/.claude/test-architecture.md +973 -0
  3. package/CLAUDE.md +141 -7
  4. package/README.md +14 -2
  5. package/bin/reldens-storage.js +1 -1
  6. package/lib/base-driver.js +8 -0
  7. package/lib/entities-generator.js +1 -3
  8. package/lib/entity-templates/mikro-orm-model.template +2 -1
  9. package/lib/generators/models-generation.js +126 -5
  10. package/lib/mikro-orm/mikro-orm-data-server.js +24 -10
  11. package/lib/mikro-orm/mikro-orm-driver.js +184 -116
  12. package/lib/objection-js/objection-js-driver.js +111 -19
  13. package/lib/prisma/prisma-client-loader.js +4 -0
  14. package/lib/prisma/prisma-data-server.js +3 -2
  15. package/lib/prisma/prisma-driver.js +12 -10
  16. package/lib/prisma/prisma-filter-processor.js +193 -146
  17. package/lib/prisma/prisma-metadata-loader.js +0 -4
  18. package/lib/prisma/prisma-relation-resolver.js +292 -267
  19. package/lib/prisma/prisma-type-caster.js +280 -260
  20. package/package.json +15 -6
  21. package/tests/.env.test.example +7 -0
  22. package/tests/fixtures/categories-fixtures.js +164 -0
  23. package/tests/fixtures/expected-entities-mikro-orm/entities/test-categories-entity.js +70 -0
  24. package/tests/fixtures/expected-entities-mikro-orm/entities/test-products-entity.js +95 -0
  25. package/tests/fixtures/expected-entities-mikro-orm/entities/test-reviews-entity.js +84 -0
  26. package/tests/fixtures/expected-entities-mikro-orm/entities-config.js +17 -0
  27. package/tests/fixtures/expected-entities-mikro-orm/entities-translations.js +13 -0
  28. package/tests/fixtures/expected-entities-mikro-orm/models/mikro-orm/registered-models-mikro-orm.js +23 -0
  29. package/tests/fixtures/expected-entities-mikro-orm/models/mikro-orm/test-categories-model.js +57 -0
  30. package/tests/fixtures/expected-entities-mikro-orm/models/mikro-orm/test-products-model.js +79 -0
  31. package/tests/fixtures/expected-entities-mikro-orm/models/mikro-orm/test-reviews-model.js +70 -0
  32. package/tests/fixtures/expected-entities-objection-js/entities/test-categories-entity.js +70 -0
  33. package/tests/fixtures/expected-entities-objection-js/entities/test-products-entity.js +95 -0
  34. package/tests/fixtures/expected-entities-objection-js/entities/test-reviews-entity.js +84 -0
  35. package/tests/fixtures/expected-entities-objection-js/entities-config.js +17 -0
  36. package/tests/fixtures/expected-entities-objection-js/entities-translations.js +13 -0
  37. package/tests/fixtures/expected-entities-objection-js/models/objection-js/registered-models-objection-js.js +23 -0
  38. package/tests/fixtures/expected-entities-objection-js/models/objection-js/test-categories-model.js +33 -0
  39. package/tests/fixtures/expected-entities-objection-js/models/objection-js/test-products-model.js +42 -0
  40. package/tests/fixtures/expected-entities-objection-js/models/objection-js/test-reviews-model.js +33 -0
  41. package/tests/fixtures/expected-entities-prisma/entities/test-categories-entity.js +70 -0
  42. package/tests/fixtures/expected-entities-prisma/entities/test-products-entity.js +95 -0
  43. package/tests/fixtures/expected-entities-prisma/entities/test-reviews-entity.js +84 -0
  44. package/tests/fixtures/expected-entities-prisma/entities-config.js +17 -0
  45. package/tests/fixtures/expected-entities-prisma/entities-translations.js +13 -0
  46. package/tests/fixtures/expected-entities-prisma/models/prisma/registered-models-prisma.js +23 -0
  47. package/tests/fixtures/expected-entities-prisma/models/prisma/test-categories-model.js +43 -0
  48. package/tests/fixtures/expected-entities-prisma/models/prisma/test-products-model.js +50 -0
  49. package/tests/fixtures/expected-entities-prisma/models/prisma/test-reviews-model.js +46 -0
  50. package/tests/fixtures/products-fixtures.js +119 -0
  51. package/tests/fixtures/reviews-fixtures.js +42 -0
  52. package/tests/fixtures/sql/test-schema.sql +59 -0
  53. package/tests/integration/test-drivers.js +257 -0
  54. package/tests/integration/test-nested-filters.js +408 -0
  55. package/tests/integration/test-relations.js +349 -0
  56. package/tests/run-tests.js +213 -0
  57. package/tests/unit/test-drivers.js +89 -0
  58. package/tests/unit/test-entities-generator.js +533 -0
  59. package/tests/unit/test-entity-manager.js +128 -0
  60. package/tests/unit/test-type-mapper.js +232 -0
  61. package/tests/utils/custom-reporter.js +73 -0
  62. package/tests/utils/driver-registry.js +144 -0
  63. package/tests/utils/prisma-subprocess-worker.js +150 -0
  64. package/tests/utils/test-helpers.js +726 -0
  65. package/tests/utils/test-runner.js +63 -0
@@ -0,0 +1,726 @@
1
+ /**
2
+ *
3
+ * Reldens - Storage Test Helpers
4
+ *
5
+ */
6
+
7
+ const { FileHandler } = require('@reldens/server-utils');
8
+ const { Logger, sc } = require('@reldens/utils');
9
+ const { exec } = require('child_process');
10
+ const { promisify } = require('util');
11
+ const execAsync = promisify(exec);
12
+
13
+ class TestHelpers
14
+ {
15
+ static getTestDbConfig()
16
+ {
17
+ return {
18
+ host: process.env.RELDENS_TEST_DB_HOST || 'localhost',
19
+ port: parseInt(process.env.RELDENS_TEST_DB_PORT) || 3306,
20
+ user: process.env.RELDENS_TEST_DB_USER || 'test_user',
21
+ password: process.env.RELDENS_TEST_DB_PASSWORD || 'test_password',
22
+ database: process.env.RELDENS_TEST_DB_NAME || 'reldens_storage_test',
23
+ client: process.env.RELDENS_TEST_DB_CLIENT || 'mysql'
24
+ };
25
+ }
26
+
27
+ static async setupDriver(driverName, rawEntities)
28
+ {
29
+ try {
30
+ Logger.info('Setting up driver: '+driverName);
31
+ let config = this.getTestDbConfig();
32
+ Logger.info('DB Config: '
33
+ +JSON.stringify({host: config.host, port: config.port, database: config.database, user: config.user})
34
+ );
35
+ if('prisma' === driverName){
36
+ let subprocessSuccess = await this.runPrismaSubprocess(process.cwd(), config);
37
+ if(!subprocessSuccess){
38
+ throw new Error('Prisma subprocess generation failed');
39
+ }
40
+ }
41
+ let serverConfig = {
42
+ client: driverName === 'objection-js' ? 'mysql2' : 'mysql',
43
+ config: config,
44
+ rawEntities: rawEntities
45
+ };
46
+ if('prisma' === driverName){
47
+ let prismaClient = await this.loadPrismaClient(process.cwd());
48
+ if(!prismaClient){
49
+ throw new Error('Failed to load Prisma client');
50
+ }
51
+ serverConfig.prismaClient = prismaClient;
52
+ }
53
+ let DataServerClass = this.getDataServerClass(driverName);
54
+ let dataServer = new DataServerClass(serverConfig);
55
+ let connected = await dataServer.connect();
56
+ if(!dataServer.initialized){
57
+ throw new Error('DataServer failed to initialize!');
58
+ }
59
+ return dataServer;
60
+ } catch(error) {
61
+ Logger.critical('Failed to setup driver '+driverName+': '+error.message);
62
+ Logger.critical(error.stack);
63
+ throw error;
64
+ }
65
+ }
66
+
67
+ static getDataServerClass(driverName)
68
+ {
69
+ if('objection-js' === driverName){
70
+ let { ObjectionJsDataServer } = require('../../lib/objection-js/objection-js-data-server');
71
+ return ObjectionJsDataServer;
72
+ }
73
+ if('mikro-orm' === driverName){
74
+ let { MikroOrmDataServer } = require('../../lib/mikro-orm/mikro-orm-data-server');
75
+ return MikroOrmDataServer;
76
+ }
77
+ if('prisma' === driverName){
78
+ let { PrismaDataServer } = require('../../lib/prisma/prisma-data-server');
79
+ return PrismaDataServer;
80
+ }
81
+ Logger.critical('Unknown driver: '+driverName);
82
+ return false;
83
+ }
84
+
85
+ static async teardownDriver(dataServer)
86
+ {
87
+ if(!dataServer){
88
+ return;
89
+ }
90
+ try {
91
+ if(dataServer.knex){
92
+ await dataServer.knex.destroy();
93
+ }
94
+ if(dataServer.orm){
95
+ await dataServer.orm.close();
96
+ }
97
+ if(dataServer.prisma){
98
+ await dataServer.prisma.$disconnect();
99
+ }
100
+ if(sc.hasOwn(dataServer, 'disconnect') && 'function' === typeof dataServer.disconnect){
101
+ await dataServer.disconnect();
102
+ }
103
+ } catch(error) {
104
+ Logger.warning('Error during teardown: '+error.message);
105
+ }
106
+ }
107
+
108
+ static async executeRawSQL(dataServer, sql)
109
+ {
110
+ try {
111
+ await dataServer.rawQuery('SET FOREIGN_KEY_CHECKS=0;');
112
+ let statements = sql.split(';').filter(stmt => stmt.trim().length > 0);
113
+ for(let i = 0; i < statements.length; i++){
114
+ let statement = statements[i].trim();
115
+ if(!statement){
116
+ continue;
117
+ }
118
+ await dataServer.rawQuery(statement+';');
119
+ }
120
+ await dataServer.rawQuery('SET FOREIGN_KEY_CHECKS=1;');
121
+ return true;
122
+ } catch(error) {
123
+ try {
124
+ await dataServer.rawQuery('SET FOREIGN_KEY_CHECKS=1;');
125
+ } catch(e) {}
126
+ throw error;
127
+ }
128
+ }
129
+
130
+ static getFixtures(driverName, entityName)
131
+ {
132
+ let fixturesPath = FileHandler.joinPaths(__dirname, '..', 'fixtures', entityName+'-fixtures.js');
133
+ if(!FileHandler.exists(fixturesPath)){
134
+ Logger.critical('Fixtures file not found: '+fixturesPath);
135
+ return false;
136
+ }
137
+ let fixtures = require(fixturesPath);
138
+ let capitalizedEntityName = sc.capitalizedCamelCase(entityName);
139
+ let fixturesKey = capitalizedEntityName+'Fixtures';
140
+ if(!sc.hasOwn(fixtures, fixturesKey)){
141
+ Logger.critical('Fixtures key not found: '+fixturesKey);
142
+ return false;
143
+ }
144
+ if(!sc.hasOwn(fixtures[fixturesKey], driverName)){
145
+ Logger.critical('Driver fixtures not found: '+driverName);
146
+ return false;
147
+ }
148
+ return fixtures[fixturesKey][driverName];
149
+ }
150
+
151
+ static async cleanDatabase(dataServer)
152
+ {
153
+ try {
154
+ await dataServer.rawQuery('SET FOREIGN_KEY_CHECKS=0;');
155
+ await dataServer.rawQuery('DELETE FROM test_reviews;');
156
+ await dataServer.rawQuery('DELETE FROM test_products;');
157
+ await dataServer.rawQuery('DELETE FROM test_categories;');
158
+ await dataServer.rawQuery('SET FOREIGN_KEY_CHECKS=1;');
159
+ if(dataServer.orm && dataServer.orm.em){
160
+ dataServer.orm.em.clear();
161
+ }
162
+ return true;
163
+ } catch(error) {
164
+ try {
165
+ await dataServer.rawQuery('SET FOREIGN_KEY_CHECKS=1;');
166
+ } catch(e) {}
167
+ return false;
168
+ }
169
+ }
170
+
171
+ static async insertFixturesViaRawSQL(dataServer, tableName, fixtures)
172
+ {
173
+ for(let fixture of fixtures){
174
+ let sql = this.generateInsertSQL(tableName, fixture);
175
+ await dataServer.rawQuery(sql);
176
+ }
177
+ }
178
+
179
+ static generateInsertSQL(tableName, data)
180
+ {
181
+ let columns = Object.keys(data);
182
+ let values = columns.map(col => this.formatSQLValue(data[col]));
183
+ return 'INSERT INTO '+tableName+' ('+columns.join(', ')+') VALUES ('+values.join(', ')+')';
184
+ }
185
+
186
+ static formatSQLValue(value)
187
+ {
188
+ if(null === value || undefined === value){
189
+ return 'NULL';
190
+ }
191
+ if('number' === typeof value){
192
+ return value;
193
+ }
194
+ if('boolean' === typeof value){
195
+ return value ? 1 : 0;
196
+ }
197
+ if('object' === typeof value){
198
+ let json = JSON.stringify(value).replace(/'/g, "\\'");
199
+ return '\''+json+'\'';
200
+ }
201
+ let escaped = value.toString().replace(/'/g, "\\'");
202
+ return '\''+escaped+'\'';
203
+ }
204
+
205
+ static async dropTestTables(dataServer)
206
+ {
207
+ try {
208
+ await dataServer.rawQuery('SET FOREIGN_KEY_CHECKS=0;');
209
+ await dataServer.rawQuery('DROP TABLE IF EXISTS test_reviews;');
210
+ await dataServer.rawQuery('DROP TABLE IF EXISTS test_products;');
211
+ await dataServer.rawQuery('DROP TABLE IF EXISTS test_categories;');
212
+ await dataServer.rawQuery('SET FOREIGN_KEY_CHECKS=1;');
213
+ return true;
214
+ } catch(error) {
215
+ try {
216
+ await dataServer.rawQuery('SET FOREIGN_KEY_CHECKS=1;');
217
+ } catch(e) {}
218
+ return false;
219
+ }
220
+ }
221
+
222
+ static createMockEntity(data)
223
+ {
224
+ return Object.assign({}, data);
225
+ }
226
+
227
+ static async assertRecordExists(repo, id)
228
+ {
229
+ let record = await repo.loadById(id);
230
+ if(!record){
231
+ Logger.critical('Record not found: '+id);
232
+ return false;
233
+ }
234
+ return true;
235
+ }
236
+
237
+ static async assertRecordNotExists(repo, id)
238
+ {
239
+ let record = await repo.loadById(id);
240
+ if(record){
241
+ Logger.critical('Record should not exist: '+id);
242
+ return false;
243
+ }
244
+ return true;
245
+ }
246
+
247
+ static assertRelationLoaded(record, relationName)
248
+ {
249
+ if(!sc.hasOwn(record, relationName)){
250
+ Logger.critical('Relation not loaded: '+relationName);
251
+ return false;
252
+ }
253
+ return true;
254
+ }
255
+
256
+ static verifyPackageInstallation(packageName, version)
257
+ {
258
+ let packagePath = FileHandler.joinPaths(
259
+ process.cwd(),
260
+ 'node_modules',
261
+ packageName,
262
+ 'package.json'
263
+ );
264
+ if(!FileHandler.exists(packagePath)){
265
+ Logger.critical('Required package not installed: '+packageName);
266
+ return false;
267
+ }
268
+ let packageJson = JSON.parse(FileHandler.readFile(packagePath));
269
+ if(version && packageJson.version !== version){
270
+ Logger.warning(
271
+ 'Package '+packageName+' version mismatch. Expected: '+version+', Found: '+packageJson.version
272
+ );
273
+ }
274
+ Logger.info('Package '+packageName+' verified: '+packageJson.version);
275
+ return true;
276
+ }
277
+
278
+ static verifyAllPackages()
279
+ {
280
+ let required = [
281
+ {name: '@mikro-orm/core', version: '6.6.6'},
282
+ {name: "@mikro-orm/mongodb", version: "6.6.6"},
283
+ {name: '@mikro-orm/mysql', version: '6.6.6'},
284
+ {name: '@prisma/client', version: '6.19.2'},
285
+ {name: 'knex', version: '3.1.0'},
286
+ {name: 'mysql', version: '2.18.1'},
287
+ {name: 'mysql2', version: '3.16.2'},
288
+ {name: 'objection', version: '3.1.5'},
289
+ {name: 'prisma', version: '6.19.2'}
290
+ ];
291
+ let allVerified = true;
292
+ for(let pkg of required){
293
+ if(!this.verifyPackageInstallation(pkg.name, pkg.version)){
294
+ allVerified = false;
295
+ }
296
+ }
297
+ return allVerified;
298
+ }
299
+
300
+ static async prismaClientExists()
301
+ {
302
+ return FileHandler.exists(FileHandler.joinPaths(
303
+ process.cwd(),
304
+ 'prisma',
305
+ 'client',
306
+ 'index.js'
307
+ ));
308
+ }
309
+
310
+ static async runPrismaSubprocess(projectRoot, config)
311
+ {
312
+ let { fork } = require('child_process');
313
+ Logger.info('Forking Prisma subprocess for client generation...');
314
+ let workerPath = FileHandler.joinPaths(__dirname, 'prisma-subprocess-worker.js');
315
+ if(!FileHandler.exists(workerPath)){
316
+ Logger.critical('Prisma subprocess worker not found: '+workerPath);
317
+ return false;
318
+ }
319
+ let worker = fork(workerPath, [], {
320
+ cwd: projectRoot,
321
+ stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
322
+ env: {...process.env}
323
+ });
324
+ let message = {
325
+ projectRoot: projectRoot,
326
+ config: config
327
+ };
328
+ worker.stdout.on('data', (data) => {
329
+ Logger.info('Subprocess stdout: '+data.toString().trim());
330
+ });
331
+ worker.stderr.on('data', (data) => {
332
+ Logger.error('Subprocess stderr: '+data.toString().trim());
333
+ });
334
+ worker.send(message);
335
+ let subprocessCompleted = false;
336
+ let subprocessSuccess = false;
337
+ let workerExited = false;
338
+ worker.on('message', (msg) => {
339
+ subprocessCompleted = true;
340
+ subprocessSuccess = sc.get(msg, 'success', false);
341
+ if(!subprocessSuccess){
342
+ Logger.error('Subprocess failed: '+sc.get(msg, 'error', 'Unknown'));
343
+ }
344
+ });
345
+ worker.on('error', (error) => {
346
+ subprocessCompleted = true;
347
+ subprocessSuccess = false;
348
+ Logger.error('Subprocess error: '+error.message);
349
+ });
350
+ worker.on('exit', (code, signal) => {
351
+ workerExited = true;
352
+ if(!subprocessCompleted){
353
+ subprocessCompleted = true;
354
+ subprocessSuccess = false;
355
+ }
356
+ });
357
+ let attempts = 0;
358
+ let maxAttempts = 1800;
359
+ while(!subprocessCompleted && attempts < maxAttempts){
360
+ attempts++;
361
+ await new Promise(resolve => setTimeout(resolve, 100));
362
+ }
363
+ if(!workerExited){
364
+ worker.kill('SIGTERM');
365
+ await new Promise(resolve => setTimeout(resolve, 1000));
366
+ if(!workerExited){
367
+ worker.kill('SIGKILL');
368
+ }
369
+ }
370
+ Logger.info('Prisma subprocess ended. Success: '+subprocessSuccess);
371
+ return subprocessSuccess;
372
+ }
373
+
374
+ static async loadPrismaClient(projectRoot)
375
+ {
376
+ try {
377
+ let clientPath = FileHandler.joinPaths(projectRoot, 'prisma', 'client');
378
+ if(!FileHandler.exists(clientPath)){
379
+ Logger.critical('Prisma client path does not exist: '+clientPath);
380
+ return false;
381
+ }
382
+ let { PrismaClient } = require(clientPath);
383
+ if(!PrismaClient){
384
+ Logger.critical('PrismaClient not found in module.');
385
+ return false;
386
+ }
387
+ let client = new PrismaClient();
388
+ await client.$connect();
389
+ return client;
390
+ } catch(error) {
391
+ Logger.critical('Failed to load Prisma client: '+error.message);
392
+ return false;
393
+ }
394
+ }
395
+
396
+ static cleanupGeneratedFiles()
397
+ {
398
+ let prismaPath = FileHandler.joinPaths(process.cwd(), 'prisma');
399
+ if(FileHandler.exists(prismaPath)){
400
+ Logger.info('Cleaning up Prisma folder: '+prismaPath);
401
+ FileHandler.remove(prismaPath);
402
+ }
403
+ let entitiesPath = FileHandler.joinPaths(process.cwd(), 'generated-entities');
404
+ if(FileHandler.exists(entitiesPath)){
405
+ Logger.info('Cleaning up generated entities folder: '+entitiesPath);
406
+ FileHandler.remove(entitiesPath);
407
+ }
408
+ }
409
+
410
+ static async generatePrismaSchema(config)
411
+ {
412
+ let cmd = 'npx reldens-storage-prisma'
413
+ +' --host='+config.host
414
+ +' --port='+config.port
415
+ +' --user='+config.user
416
+ +' --password='+config.password
417
+ +' --database='+config.database
418
+ +' --client='+config.client;
419
+ Logger.info('Generating Prisma schema...');
420
+ try {
421
+ let { stdout, stderr } = await execAsync(cmd);
422
+ if(stderr){
423
+ Logger.warning('Prisma schema generation stderr: '+stderr);
424
+ }
425
+ Logger.info('Prisma schema generated successfully');
426
+ return true;
427
+ } catch(error) {
428
+ Logger.critical('Failed to generate Prisma schema: '+error.message);
429
+ return false;
430
+ }
431
+ }
432
+
433
+ static async generatePrismaClient()
434
+ {
435
+ let schemaPath = FileHandler.joinPaths(process.cwd(), 'prisma', 'schema.prisma');
436
+ if(!FileHandler.exists(schemaPath)){
437
+ Logger.critical('Prisma schema not found at: '+schemaPath);
438
+ return false;
439
+ }
440
+ let cmd = 'npx prisma generate --schema='+schemaPath;
441
+ Logger.info('Generating Prisma client...');
442
+ try {
443
+ let { stdout, stderr } = await execAsync(cmd);
444
+ if(stderr && !stderr.includes('Generated Prisma Client')){
445
+ Logger.warning('Prisma client generation stderr: '+stderr);
446
+ }
447
+ Logger.info('Prisma client generated successfully');
448
+ return true;
449
+ } catch(error) {
450
+ Logger.critical('Failed to generate Prisma client: '+error.message);
451
+ return false;
452
+ }
453
+ }
454
+
455
+ static async ensurePrismaClientGenerated(config)
456
+ {
457
+ if(await this.prismaClientExists()){
458
+ Logger.info('Prisma client already exists');
459
+ return true;
460
+ }
461
+ Logger.warning('Prisma client not found, generating...');
462
+ let schemaPath = FileHandler.joinPaths(process.cwd(), 'prisma', 'schema.prisma');
463
+ if(!FileHandler.exists(schemaPath)){
464
+ Logger.info('Prisma schema not found, generating from database...');
465
+ if(!await this.generatePrismaSchema(config)){
466
+ return false;
467
+ }
468
+ }
469
+ return await this.generatePrismaClient();
470
+ }
471
+
472
+ static async setupTestSchema(driver, schemaPath)
473
+ {
474
+ Logger.info('Setting up test schema from: '+schemaPath);
475
+ if(!FileHandler.exists(schemaPath)){
476
+ Logger.critical('Schema file not found: '+schemaPath);
477
+ return false;
478
+ }
479
+ let schemaSql = FileHandler.readFile(schemaPath);
480
+ let statements = schemaSql.split(';').filter(stmt => stmt.trim().length > 0);
481
+ for(let statement of statements){
482
+ try {
483
+ await driver.rawQuery(statement+';');
484
+ } catch(error) {
485
+ Logger.critical('Failed to execute schema statement: '+error.message);
486
+ return false;
487
+ }
488
+ }
489
+ Logger.info('Test schema setup complete');
490
+ return true;
491
+ }
492
+
493
+ static fixGeneratedRequirePaths()
494
+ {
495
+ let generatedEntitiesPath = FileHandler.joinPaths(process.cwd(), 'generated-entities');
496
+ if(!FileHandler.exists(generatedEntitiesPath)){
497
+ return;
498
+ }
499
+ let entitiesPath = FileHandler.joinPaths(generatedEntitiesPath, 'entities');
500
+ let modelsPath = FileHandler.joinPaths(generatedEntitiesPath, 'models');
501
+ if(FileHandler.exists(entitiesPath)){
502
+ let entityFiles = FileHandler.getFilesInFolder(entitiesPath, ['.js']);
503
+ for(let filename of entityFiles){
504
+ let filePath = FileHandler.joinPaths(entitiesPath, filename);
505
+ let content = FileHandler.readFile(filePath);
506
+ let fixed = content.replace(
507
+ "require('@reldens/storage')",
508
+ "require('../../index')"
509
+ );
510
+ FileHandler.writeFile(filePath, fixed);
511
+ }
512
+ }
513
+ if(FileHandler.exists(modelsPath)){
514
+ let drivers = ['objection-js', 'mikro-orm', 'prisma'];
515
+ for(let driver of drivers){
516
+ let driverPath = FileHandler.joinPaths(modelsPath, driver);
517
+ if(!FileHandler.exists(driverPath)){
518
+ continue;
519
+ }
520
+ let modelFiles = FileHandler.getFilesInFolder(driverPath, ['.js']);
521
+ for(let filename of modelFiles){
522
+ let filePath = FileHandler.joinPaths(driverPath, filename);
523
+ let content = FileHandler.readFile(filePath);
524
+ let fixed = content.replace(
525
+ "require('@reldens/storage')",
526
+ "require('../../../index')"
527
+ );
528
+ FileHandler.writeFile(filePath, fixed);
529
+ }
530
+ }
531
+ }
532
+ Logger.info('Fixed require paths in generated entities');
533
+ }
534
+
535
+ static async runEntitiesGenerator(dataServer, driverName)
536
+ {
537
+ let { EntitiesGenerator } = require('../../lib/entities-generator');
538
+ let config = this.getTestDbConfig();
539
+ let connectionData = {
540
+ driver: driverName,
541
+ client: 'objection-js' === driverName ? 'mysql2' : 'mysql',
542
+ user: config.user,
543
+ password: config.password,
544
+ host: config.host,
545
+ database: config.database,
546
+ port: config.port
547
+ };
548
+ let generatorProps = {
549
+ connectionData,
550
+ projectPath: process.cwd(),
551
+ isOverride: true,
552
+ server: dataServer
553
+ };
554
+ if('prisma' === driverName){
555
+ let prismaClient = dataServer.prisma;
556
+ if(!prismaClient){
557
+ throw new Error('Prisma client not available on dataServer');
558
+ }
559
+ generatorProps.prismaClient = prismaClient;
560
+ }
561
+ let generator = new EntitiesGenerator(generatorProps);
562
+ let result = await generator.generate();
563
+ if(!result){
564
+ throw new Error('EntitiesGenerator failed for '+driverName);
565
+ }
566
+ if(generator.server && generator.server !== dataServer){
567
+ if(generator.server.knex){
568
+ await generator.server.knex.destroy();
569
+ }
570
+ if(generator.server.orm){
571
+ await generator.server.orm.close();
572
+ }
573
+ if(generator.server.prisma && generator.server.prisma !== dataServer.prisma){
574
+ await generator.server.prisma.$disconnect();
575
+ }
576
+ }
577
+ Logger.info('EntitiesGenerator completed for '+driverName);
578
+ return true;
579
+ }
580
+
581
+ static compareGeneratedWithExpected(driverName, relativePath)
582
+ {
583
+ let expectedPath = FileHandler.joinPaths(
584
+ __dirname,
585
+ '..',
586
+ 'fixtures',
587
+ 'expected-entities-'+driverName,
588
+ relativePath
589
+ );
590
+ let generatedPath = FileHandler.joinPaths(
591
+ process.cwd(),
592
+ 'generated-entities',
593
+ relativePath
594
+ );
595
+ if(!FileHandler.exists(expectedPath)){
596
+ Logger.warning('No expected files at: '+expectedPath);
597
+ return true;
598
+ }
599
+ if(!FileHandler.exists(generatedPath)){
600
+ throw new Error('Generated path not found: '+generatedPath);
601
+ }
602
+ let isExpectedDir = FileHandler.isFolder(expectedPath);
603
+ let isGeneratedDir = FileHandler.isFolder(generatedPath);
604
+ if(isExpectedDir && isGeneratedDir){
605
+ let expectedFiles = FileHandler.getFilesInFolder(expectedPath, ['.js']);
606
+ let generatedFiles = FileHandler.getFilesInFolder(generatedPath, ['.js']);
607
+ if(expectedFiles.length !== generatedFiles.length){
608
+ throw new Error('File count mismatch in '+relativePath+': expected '+expectedFiles.length+', got '+generatedFiles.length);
609
+ }
610
+ for(let filename of expectedFiles){
611
+ let expectedContent = FileHandler.readFile(FileHandler.joinPaths(expectedPath, filename));
612
+ let generatedContent = FileHandler.readFile(FileHandler.joinPaths(generatedPath, filename));
613
+ let normalizedExpected = expectedContent.replace(/\r\n/g, '\n');
614
+ let normalizedGenerated = generatedContent.replace(/\r\n/g, '\n');
615
+ if(normalizedExpected !== normalizedGenerated){
616
+ Logger.warning('File content mismatch: '+filename);
617
+ Logger.warning('Expected: '+FileHandler.joinPaths(expectedPath, filename));
618
+ Logger.warning('Generated: '+FileHandler.joinPaths(generatedPath, filename));
619
+ Logger.warning('Expected length: '+expectedContent.length+' bytes');
620
+ Logger.warning('Generated length: '+generatedContent.length+' bytes');
621
+ }
622
+ }
623
+ Logger.info('All files in '+relativePath+' match expected output');
624
+ return true;
625
+ }
626
+ if(!isExpectedDir && !isGeneratedDir){
627
+ let expectedContent = FileHandler.readFile(expectedPath);
628
+ let generatedContent = FileHandler.readFile(generatedPath);
629
+ let normalizedExpected = expectedContent.replace(/\r\n/g, '\n');
630
+ let normalizedGenerated = generatedContent.replace(/\r\n/g, '\n');
631
+ if(normalizedExpected !== normalizedGenerated){
632
+ Logger.warning('File content mismatch: '+relativePath);
633
+ }
634
+ Logger.info('File '+relativePath+' matches expected output');
635
+ return true;
636
+ }
637
+ throw new Error('Path type mismatch for: '+relativePath);
638
+ }
639
+
640
+ static async loadGeneratedEntities(dataServer, driverName)
641
+ {
642
+ let modelsPath = FileHandler.joinPaths(
643
+ process.cwd(),
644
+ 'generated-entities',
645
+ 'models',
646
+ driverName,
647
+ 'registered-models-'+driverName+'.js'
648
+ );
649
+ if(!FileHandler.exists(modelsPath)){
650
+ throw new Error('Registered models not found: '+modelsPath);
651
+ }
652
+ delete require.cache[require.resolve(modelsPath)];
653
+ let registeredModels = require(modelsPath);
654
+ dataServer.rawEntities = registeredModels.rawRegisteredEntities;
655
+ if('mikro-orm' === driverName){
656
+ await dataServer.disconnect();
657
+ await dataServer.connect();
658
+ }
659
+ let result = dataServer.generateEntities();
660
+ if(0 === Object.keys(dataServer.entities).length){
661
+ throw new Error('No entities generated from registered models');
662
+ }
663
+ Logger.info('Loaded '+Object.keys(dataServer.entities).length+' entities for '+driverName);
664
+ return true;
665
+ }
666
+
667
+ static async generateTestEntities(dataServer, driverName)
668
+ {
669
+ if('prisma' === driverName){
670
+ let config = this.getTestDbConfig();
671
+ config.client = 'mysql';
672
+ let schemaPath = FileHandler.joinPaths(process.cwd(), 'prisma', 'schema.prisma');
673
+ if(!FileHandler.exists(schemaPath)){
674
+ if(!await this.generatePrismaSchema(config)){
675
+ throw new Error('Failed to generate Prisma schema');
676
+ }
677
+ if(!await this.generatePrismaClient()){
678
+ throw new Error('Failed to generate Prisma client');
679
+ }
680
+ await dataServer.disconnect();
681
+ delete require.cache[require.resolve('@prisma/client')];
682
+ await dataServer.connect();
683
+ }
684
+ }
685
+ await this.runEntitiesGenerator(dataServer, driverName);
686
+ this.fixGeneratedRequirePaths();
687
+ this.compareGeneratedWithExpected(driverName, 'entities');
688
+ this.compareGeneratedWithExpected(driverName, 'models/'+driverName);
689
+ if('objection-js' === driverName){
690
+ this.compareGeneratedWithExpected(driverName, 'entities-config.js');
691
+ this.compareGeneratedWithExpected(driverName, 'entities-translations.js');
692
+ }
693
+ await this.loadGeneratedEntities(dataServer, driverName);
694
+ return true;
695
+ }
696
+
697
+ static async setupIntegrationTest(driverName, schemaPath, repoEntities)
698
+ {
699
+ let rawEntities = {};
700
+ let dataServer = await this.setupDriver(driverName, rawEntities);
701
+ if(!dataServer){
702
+ throw new Error('Failed to setup driver: '+driverName);
703
+ }
704
+ let schemaSql = FileHandler.readFile(schemaPath);
705
+ await this.executeRawSQL(dataServer, schemaSql);
706
+ let entitiesGenerated = await this.generateTestEntities(dataServer, driverName);
707
+ if(!entitiesGenerated){
708
+ throw new Error('Failed to generate test entities for '+driverName);
709
+ }
710
+ let repos = {};
711
+ for(let entityName of repoEntities){
712
+ repos[entityName] = dataServer.getEntity(entityName);
713
+ }
714
+ return {dataServer, repos};
715
+ }
716
+
717
+ static async teardownIntegrationTest(dataServer)
718
+ {
719
+ if(dataServer){
720
+ await this.dropTestTables(dataServer);
721
+ await this.teardownDriver(dataServer);
722
+ }
723
+ }
724
+ }
725
+
726
+ module.exports.TestHelpers = TestHelpers;