@reldens/storage 0.88.0 → 0.90.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 (56) hide show
  1. package/.claude/driver-methods-complete-analysis.md +1978 -0
  2. package/.claude/test-architecture.md +973 -0
  3. package/CLAUDE.md +132 -3
  4. package/bin/reldens-storage.js +1 -1
  5. package/lib/base-driver.js +8 -0
  6. package/lib/entities-generator.js +1 -3
  7. package/lib/entity-templates/entities-translations.template +3 -0
  8. package/lib/entity-templates/mikro-orm-model.template +2 -1
  9. package/lib/generators/entities-generation.js +12 -1
  10. package/lib/generators/entities-translations-generation.js +40 -4
  11. package/lib/generators/models-generation.js +126 -5
  12. package/lib/mikro-orm/mikro-orm-data-server.js +24 -10
  13. package/lib/mikro-orm/mikro-orm-driver.js +184 -116
  14. package/lib/objection-js/objection-js-driver.js +111 -19
  15. package/lib/prisma/prisma-data-server.js +3 -2
  16. package/lib/prisma/prisma-driver.js +12 -10
  17. package/lib/prisma/prisma-filter-processor.js +193 -146
  18. package/lib/prisma/prisma-metadata-loader.js +0 -4
  19. package/lib/prisma/prisma-relation-resolver.js +292 -267
  20. package/lib/prisma/prisma-type-caster.js +280 -260
  21. package/package.json +15 -6
  22. package/tests/.env.test.example +7 -0
  23. package/tests/fixtures/categories-fixtures.js +164 -0
  24. package/tests/fixtures/expected-entities/entities/test-categories-entity.js +70 -0
  25. package/tests/fixtures/expected-entities/entities/test-products-entity.js +95 -0
  26. package/tests/fixtures/expected-entities/entities/test-reviews-entity.js +84 -0
  27. package/tests/fixtures/expected-entities/entities-config.js +17 -0
  28. package/tests/fixtures/expected-entities/entities-translations.js +53 -0
  29. package/tests/fixtures/expected-entities-mikro-orm/models/mikro-orm/registered-models-mikro-orm.js +23 -0
  30. package/tests/fixtures/expected-entities-mikro-orm/models/mikro-orm/test-categories-model.js +57 -0
  31. package/tests/fixtures/expected-entities-mikro-orm/models/mikro-orm/test-products-model.js +79 -0
  32. package/tests/fixtures/expected-entities-mikro-orm/models/mikro-orm/test-reviews-model.js +70 -0
  33. package/tests/fixtures/expected-entities-objection-js/models/objection-js/registered-models-objection-js.js +23 -0
  34. package/tests/fixtures/expected-entities-objection-js/models/objection-js/test-categories-model.js +33 -0
  35. package/tests/fixtures/expected-entities-objection-js/models/objection-js/test-products-model.js +42 -0
  36. package/tests/fixtures/expected-entities-objection-js/models/objection-js/test-reviews-model.js +33 -0
  37. package/tests/fixtures/expected-entities-prisma/models/prisma/registered-models-prisma.js +23 -0
  38. package/tests/fixtures/expected-entities-prisma/models/prisma/test-categories-model.js +43 -0
  39. package/tests/fixtures/expected-entities-prisma/models/prisma/test-products-model.js +50 -0
  40. package/tests/fixtures/expected-entities-prisma/models/prisma/test-reviews-model.js +46 -0
  41. package/tests/fixtures/products-fixtures.js +119 -0
  42. package/tests/fixtures/reviews-fixtures.js +42 -0
  43. package/tests/fixtures/sql/test-schema.sql +59 -0
  44. package/tests/integration/test-drivers.js +257 -0
  45. package/tests/integration/test-nested-filters.js +408 -0
  46. package/tests/integration/test-relations.js +349 -0
  47. package/tests/run-tests.js +213 -0
  48. package/tests/unit/test-drivers.js +89 -0
  49. package/tests/unit/test-entities-generator.js +533 -0
  50. package/tests/unit/test-entity-manager.js +128 -0
  51. package/tests/unit/test-type-mapper.js +232 -0
  52. package/tests/utils/custom-reporter.js +73 -0
  53. package/tests/utils/driver-registry.js +144 -0
  54. package/tests/utils/prisma-subprocess-worker.js +150 -0
  55. package/tests/utils/test-helpers.js +726 -0
  56. package/tests/utils/test-runner.js +63 -0
package/CLAUDE.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
4
 
5
+ ## ⚠️ CRITICAL: The Library Works - Fix The Tests
6
+
7
+ **This package is battle-tested in hundreds of production projects handling thousands of API calls successfully.**
8
+
9
+ **Default Assumption: Tests are failing because TEST CODE is incorrect, NOT because library code is broken.**
10
+
11
+ **Your Primary Task:**
12
+ - Fix test files in `tests/` directory to properly use the library
13
+ - Study working examples from reldens-cms and reldens main project
14
+ - Make all tests pass by correcting how they call the library APIs
15
+
16
+ **If You Suspect a Library Bug:**
17
+ - Present complete proof with production evidence
18
+ - Wait for user confirmation before proposing any library changes
19
+ - Do NOT modify `lib/` code without explicit approval
20
+
21
+ ---
22
+
5
23
  ## Package Overview
6
24
 
7
25
  **@reldens/storage** is the database abstraction layer for Reldens. It provides:
@@ -52,6 +70,7 @@ npx reldens-storage-prisma --host=<host> --database=<db> --user=<user> --passwor
52
70
  **BaseDriver** (`lib/base-driver.js`):
53
71
  - Abstract base class for ORM drivers
54
72
  - Provides common interface for all database operations
73
+ - Stores operator mapping for all drivers (`this.operatorsMap`)
55
74
  - Key methods (all must be implemented by drivers):
56
75
  - CRUD: `create()`, `update()`, `delete()`, `upsert()`
57
76
  - Read: `load()`, `loadById()`, `loadAll()`, `loadOne()`
@@ -59,6 +78,9 @@ npx reldens-storage-prisma --host=<host> --database=<db> --user=<user> --passwor
59
78
  - Count: `count()`, `countWithRelations()`
60
79
  - Query: `rawQuery()`, `executeCustomQuery()`
61
80
  - Helpers: `parseRelationsString()`, `isJsonField()`
81
+ - **Operator Mapping**: Maps string operators to SQL operators
82
+ - `GT` → `>`, `GTE` → `>=`, `LT` → `<`, `LTE` → `<=`, `NE` → `!=`, `EQ` → `=`
83
+ - Available to all drivers via `this.operatorsMap`
62
84
 
63
85
  **BaseDataServer** (`lib/base-data-server.js`):
64
86
  - Abstract base class for data servers
@@ -93,8 +115,11 @@ npx reldens-storage-prisma --host=<host> --database=<db> --user=<user> --passwor
93
115
  - Uses `withGraphFetched()` for eager loading
94
116
  - Supports relation modifiers (orderBy, limit)
95
117
  - JSON field handling with `castText()` for LIKE queries
96
- - Filter operators: OR, IN, NOT, LIKE
118
+ - **Filter operators** (case-insensitive): OR, IN, NOT, LIKE, GT, GTE, LT, LTE, NE, EQ
119
+ - **Operator conversion**: String operators converted to uppercase before processing
120
+ - **Nested filtering**: AND/OR operators support nested conditions with proper SQL grouping
97
121
  - Methods: `appendFilters()`, `appendRelationsToQuery()`
122
+ - **Upsert behavior**: Check if record exists → update if found, create if not
98
123
 
99
124
  **MikroORM Driver** (`lib/mikro-orm/`):
100
125
  - `mikro-orm-driver.js`: Driver implementation
@@ -122,6 +147,8 @@ npx reldens-storage-prisma --host=<host> --database=<db> --user=<user> --passwor
122
147
  - Binary targets configuration
123
148
  - Data proxy support
124
149
  - Windows permission error handling
150
+ - **Prisma.DbNull handling**: PrismaDataServer passes `Prisma.DbNull` to driver, which passes it to type caster
151
+ - **Type caster isolation**: PrismaTypeCaster never requires `@prisma/client` directly, receives `prismaDbNull` as prop
125
152
 
126
153
  ### Generators
127
154
 
@@ -164,7 +191,14 @@ npx reldens-storage-prisma --host=<host> --database=<db> --user=<user> --passwor
164
191
 
165
192
  **EntitiesTranslationsGeneration** (`lib/generators/entities-translations-generation.js`):
166
193
  - Generates `entities-translations.js` file
167
- - Creates i18n translation keys for entities
194
+ - Creates i18n translation keys for entities and fields
195
+ - Generates two types of translations:
196
+ - Table labels: Human-readable entity names
197
+ - Entity-specific field translations: Field labels per entity
198
+ - Methods:
199
+ - `getTranslationLabels()`: Generates table name translations
200
+ - `getFieldTranslations()`: Generates entity-specific field translations
201
+ - `formatFieldName()`: Converts field names to human-readable labels (e.g., `owner_id` → `Owner ID`)
168
202
 
169
203
  **BaseGenerator** (`lib/generators/base-generator.js`):
170
204
  - Base class for all generators
@@ -228,7 +262,9 @@ All generated files are created in the **generated-entities/** directory:
228
262
 
229
263
  **Configuration Files:**
230
264
  - entities-config.js (entity relation configuration)
231
- - entities-translations.js (translation keys)
265
+ - entities-translations.js (translation keys with two sections):
266
+ - `labels`: Table name translations (e.g., `'skills_class_path': 'Class Paths'`)
267
+ - `fields`: Entity-specific field translations (e.g., `'skills_class_path': {'id': 'ID', 'key': 'Key'}`)
232
268
 
233
269
  ## Relation Keys Pattern
234
270
 
@@ -472,3 +508,96 @@ if(!prismaClient){
472
508
  **Used By:**
473
509
  - `bin/reldens-storage.js`: CLI entity generator
474
510
  - External packages: `@reldens/cms` CLI tools (update-password, generate-entities, generate-sitemap)
511
+
512
+ ## Test Suite Architecture
513
+
514
+ **CRITICAL:** For detailed test suite architecture, execution flow, and lifecycle hooks, see: `.claude/test-architecture.md`
515
+
516
+ ### The Golden Rule: Connect Once, Test Many
517
+
518
+ **DO NOT** create database connections, tables, or entities in `beforeEach()` hooks. These operations happen **ONCE per driver** in `before()` hooks.
519
+
520
+ **Correct test lifecycle:**
521
+ 1. `before()` hook - RUNS ONCE per driver:
522
+ - Connect to database
523
+ - Create tables via SQL
524
+ - Generate entities (introspects database, creates models)
525
+ - For Prisma: Generate schema → Generate client → Reconnect
526
+ - Get repository references
527
+
528
+ 2. `beforeEach()` hook - RUNS BEFORE EACH TEST:
529
+ - DELETE data from tables (fast cleanup)
530
+ - Never drop/recreate tables
531
+ - Never reconnect to database
532
+
533
+ 3. `after()` hook - RUNS ONCE per driver:
534
+ - DROP all tables
535
+ - Disconnect from database
536
+
537
+ ### Test Database Operations
538
+
539
+ - **cleanDatabase()**: Uses DELETE statements to clear data between tests (fast)
540
+ - **dropTestTables()**: Uses DROP TABLE statements for final cleanup (slow, runs once)
541
+ - **executeRawSQL()**: Creates tables from SQL schema file (slow, runs once)
542
+
543
+ ### DataServer Flow
544
+
545
+ All three drivers follow this pattern:
546
+
547
+ ```
548
+ 1. new DataServer({config, rawEntities})
549
+
550
+ 2. await dataServer.connect()
551
+
552
+ 3. await executeRawSQL(dataServer, schemaSql) ← Tables must exist before next step
553
+
554
+ 4. await generateTestEntities(dataServer, driverName)
555
+ ├─ Introspects database schema
556
+ ├─ Creates models from tables
557
+ ├─ [Prisma only]: Generate schema + client + reconnect
558
+ └─ Calls dataServer.generateEntities()
559
+
560
+ 5. dataServer.getEntity('entityName') ← Returns repository
561
+ ```
562
+
563
+ ### Why Tables Must Exist Before Entity Generation
564
+
565
+ - `generateTestEntities()` calls `fetchEntitiesFromDatabase()`
566
+ - This queries MySQL `information_schema` for actual table structures
567
+ - Without tables, introspection returns empty/null
568
+ - Entity generation fails without table metadata
569
+
570
+ ### Prisma Special Handling
571
+
572
+ Prisma requires additional subprocess steps during entity generation:
573
+
574
+ 1. Generate `schema.prisma` file from database (subprocess: `npx reldens-storage-prisma`)
575
+ 2. Generate PrismaClient code (subprocess: `npx prisma generate`)
576
+ 3. Disconnect old client
577
+ 4. Clear Node.js module cache
578
+ 5. Reconnect with newly generated client
579
+ 6. Now PrismaClient has models for the current database schema
580
+
581
+ This happens ONCE during the `before()` hook inside `generateTestEntities()`.
582
+
583
+ **For critical patterns including:**
584
+ - Why tests use dynamic models (not generated models)
585
+ - Prisma client loading pattern
586
+ - Common pitfalls and solutions
587
+
588
+ **See:** `.claude/test-architecture.md` - Critical Patterns section
589
+
590
+ ### Test Files
591
+
592
+ **Integration tests:**
593
+ - `tests/integration/test-cross-driver-compatibility.js`: Full CRUD cycle for all 3 drivers
594
+ - `tests/integration/test-nested-filters.js`: Complex filter syntax (AND/OR/NOT/IN/LIKE)
595
+ - `tests/integration/test-relations.js`: Relation loading and nested relations
596
+
597
+ **Test utilities:**
598
+ - `tests/utils/test-helpers.js`: Database setup, entity generation, cleanup utilities
599
+ - `tests/utils/test-runner.js`: Test framework with suite/group/test methods, uses Logger for output
600
+
601
+ **All integration test files use the correct lifecycle pattern as of 2026-01-18.**
602
+
603
+ For complete details on test architecture, common issues, performance comparison, and troubleshooting, see `.claude/test-architecture.md`.
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  /**
4
4
  *
@@ -21,6 +21,14 @@ class BaseDriver
21
21
  this.sortBy = sc.get(props, 'sortBy', false);
22
22
  this.sortDirection = sc.get(props, 'sortDirection', 'ASC');
23
23
  this.entityConfig = sc.get(props, 'entityConfig', {});
24
+ this.operatorsMap = {
25
+ 'GT': '>',
26
+ 'GTE': '>=',
27
+ 'LT': '<',
28
+ 'LTE': '<=',
29
+ 'NE': '!=',
30
+ 'EQ': '='
31
+ };
24
32
  }
25
33
 
26
34
  databaseName()
@@ -402,9 +402,7 @@ class EntitiesGenerator
402
402
  Logger.critical('Failed to fetch driver key.');
403
403
  return false;
404
404
  }
405
- if('objection-js' === driverKey){
406
- this.modelsGeneration.setAllTablesData(tables);
407
- }
405
+ this.modelsGeneration.setAllTablesData(tables);
408
406
  let tablesToGenerate = this.filterTablesToGenerate(tables, driverKey);
409
407
  if(0 === Object.keys(tablesToGenerate).length && !this.isOverride){
410
408
  return true;
@@ -7,5 +7,8 @@
7
7
  module.exports.entitiesTranslations = {
8
8
  labels: {
9
9
  {{labels}}
10
+ },
11
+ fields: {
12
+ {{fields}}
10
13
  }
11
14
  };
@@ -25,11 +25,12 @@ class {{modelClassName}}
25
25
 
26
26
  const schema = new EntitySchema({
27
27
  class: {{modelClassName}},
28
+ tableName: '{{tableName}}',
28
29
  properties: {
29
30
  {{entityPropertiesDefinition}}
30
31
  },
31
32
  });
32
-
33
+ {{fkMappingsAttachment}}
33
34
  module.exports = {
34
35
  {{modelClassName}},
35
36
  entity: {{modelClassName}},
@@ -75,15 +75,26 @@ class EntitiesGeneration extends BaseGenerator
75
75
  return false;
76
76
  }
77
77
  Logger.info('Generated entity file: '+fileName);
78
+ let properties = this.buildPropertiesObject(tableData.columns);
78
79
  this.generatedEntities[tableName] = {
79
80
  entityClassName,
80
81
  entityFileName: fileName,
81
82
  tableName,
82
- titleProperty
83
+ titleProperty,
84
+ properties
83
85
  };
84
86
  return true;
85
87
  }
86
88
 
89
+ buildPropertiesObject(columns)
90
+ {
91
+ let properties = {};
92
+ for(let columnName of Object.keys(columns)){
93
+ properties[columnName] = true;
94
+ }
95
+ return properties;
96
+ }
97
+
87
98
  detectPrimaryKeyColumn(columns)
88
99
  {
89
100
  for(let columnName of Object.keys(columns)){
@@ -37,10 +37,10 @@ class EntitiesTranslationsGeneration extends BaseGenerator
37
37
  Logger.critical('Failed to read entities translations template file: '+this.templatePath);
38
38
  return false;
39
39
  }
40
- let translationsContent = translationsTemplateContent.replace(
41
- /{{labels}}/g,
42
- this.getTranslationLabels(Object.assign({}, existingEntities, generatedEntities))
43
- );
40
+ let allEntities = Object.assign({}, existingEntities, generatedEntities);
41
+ let translationsContent = translationsTemplateContent
42
+ .replace(/{{labels}}/g, this.getTranslationLabels(allEntities))
43
+ .replace(/{{fields}}/g, this.getFieldTranslations(allEntities));
44
44
  if(!FileHandler.writeFile(this.entitiesTranslationsPath, translationsContent)){
45
45
  Logger.critical('Failed to write entities translations file: '+this.entitiesTranslationsPath);
46
46
  return false;
@@ -130,6 +130,42 @@ class EntitiesTranslationsGeneration extends BaseGenerator
130
130
  return labels.join(',\n ');
131
131
  }
132
132
 
133
+ getFieldTranslations(allEntities)
134
+ {
135
+ let fieldsTranslations = [];
136
+ for(let tableName of Object.keys(allEntities)){
137
+ let entity = allEntities[tableName];
138
+ if(!sc.hasOwn(entity, 'properties')){
139
+ continue;
140
+ }
141
+ let entityFields = [];
142
+ for(let fieldName of Object.keys(entity.properties)){
143
+ let formattedName = this.formatFieldName(fieldName);
144
+ entityFields.push('\''+fieldName+'\': \''+formattedName+'\'');
145
+ }
146
+ if(0 === entityFields.length){
147
+ continue;
148
+ }
149
+ fieldsTranslations.push(
150
+ '\''+tableName+'\': {\n '+entityFields.join(',\n ')+'\n }'
151
+ );
152
+ }
153
+ return fieldsTranslations.join(',\n ');
154
+ }
155
+
156
+ formatFieldName(fieldName)
157
+ {
158
+ let formatted = fieldName
159
+ .split('_')
160
+ .map(word => word.charAt(0).toUpperCase()+word.slice(1))
161
+ .join(' ');
162
+ formatted = formatted.replace(/\sId(\s|$)/g, ' ID$1');
163
+ formatted = formatted.replace(/\sAt(\s|$)/g, ' At$1');
164
+ formatted = formatted.replace(/\sKey(\s|$)/g, ' Key$1');
165
+ formatted = formatted.replace(/^Id(\s|$)/g, 'ID$1');
166
+ return formatted;
167
+ }
168
+
133
169
  }
134
170
 
135
171
  module.exports.EntitiesTranslationsGeneration = EntitiesTranslationsGeneration;
@@ -20,6 +20,12 @@ class ModelsGeneration extends BaseGenerator
20
20
  this.generatedModels = {};
21
21
  this.allTablesData = {};
22
22
  this.removeIdFromMultipleRelations = sc.get(props, 'removeIdFromMultipleRelations', true);
23
+ this.mikroOrmRelationKindMap = {
24
+ 'BelongsToOneRelation': 'm:1',
25
+ 'HasOneRelation': '1:1',
26
+ 'HasManyRelation': '1:m',
27
+ 'ManyToManyRelation': 'm:n'
28
+ };
23
29
  }
24
30
 
25
31
  async generateModelFile(tableName, tableData, driverKey, entityInfo, relationMetadata = {})
@@ -40,7 +46,12 @@ class ModelsGeneration extends BaseGenerator
40
46
  .map(columnName => 'this.'+columnName+' = '+columnName+';')
41
47
  .join('\n ');
42
48
  let modelRelations = this.generateModelRelations(tableName, tableData, relationMetadata, driverKey);
43
- let entityPropertiesDefinition = this.getEntityPropertiesDefinition(tableData.columns, driverKey);
49
+ let entityPropertiesDefinition = this.getEntityPropertiesDefinition(tableData.columns, driverKey, tableName, tableData);
50
+ let fkMappingsJson = this.generateFkMappings(tableName, tableData, driverKey);
51
+ let fkMappingsAttachment = '';
52
+ if('mikro-orm' === driverKey && fkMappingsJson){
53
+ fkMappingsAttachment = 'schema._fkMappings = '+fkMappingsJson+';';
54
+ }
44
55
  let idColumn = this.generateIdColumn(tableData.columns, driverKey);
45
56
  let replacements = {
46
57
  modelClassName,
@@ -49,6 +60,7 @@ class ModelsGeneration extends BaseGenerator
49
60
  modelPropertiesConstructor,
50
61
  modelRelations,
51
62
  entityPropertiesDefinition,
63
+ fkMappingsAttachment,
52
64
  idColumn
53
65
  };
54
66
  let modelContent = this.applyReplacements(modelTemplateContent, replacements);
@@ -461,24 +473,85 @@ class ModelsGeneration extends BaseGenerator
461
473
  return 'HasManyRelation';
462
474
  }
463
475
 
464
- getEntityPropertiesDefinition(columns, driverKey)
476
+ getEntityPropertiesDefinition(columns, driverKey, tableName = null, tableData = null)
465
477
  {
466
478
  if('mikro-orm' === driverKey){
467
479
  let entityProps = [];
480
+ if(!tableName || !tableData){
481
+ for(let columnName of Object.keys(columns)){
482
+ let column = columns[columnName];
483
+ let isPrimary = 'PRI' === column.key;
484
+ let type = TypeMapper.mapDbTypeToJsType(column.type);
485
+ let hasDefault = null !== column.default && undefined !== column.default;
486
+ let propDef = columnName+': { type: \''+type+'\'';
487
+ if(isPrimary){
488
+ propDef += ', primary: true';
489
+ }
490
+ if(column.nullable || hasDefault){
491
+ propDef += ', nullable: true';
492
+ }
493
+ propDef += ' }';
494
+ entityProps.push(propDef);
495
+ }
496
+ return entityProps.join(',\n ');
497
+ }
498
+ let relations = this.detectObjectionJsRelations(tableName, tableData);
499
+ let fkColumnsUsedByRelations = {};
500
+ for(let relationKey of Object.keys(relations)){
501
+ let relation = relations[relationKey];
502
+ if(relation.fromColumn){
503
+ fkColumnsUsedByRelations[relation.fromColumn] = true;
504
+ }
505
+ }
468
506
  for(let columnName of Object.keys(columns)){
469
507
  let column = columns[columnName];
508
+ if(fkColumnsUsedByRelations[columnName]){
509
+ let type = TypeMapper.mapDbTypeToJsType(column.type);
510
+ let propDef = columnName+': { type: \''+type+'\', persist: false }';
511
+ entityProps.push(propDef);
512
+ continue;
513
+ }
470
514
  let isPrimary = 'PRI' === column.key;
471
515
  let type = TypeMapper.mapDbTypeToJsType(column.type);
516
+ let hasDefault = null !== column.default && undefined !== column.default;
472
517
  let propDef = columnName+': { type: \''+type+'\'';
473
518
  if(isPrimary){
474
519
  propDef += ', primary: true';
475
520
  }
476
- if(column.nullable){
521
+ if(column.nullable || hasDefault){
477
522
  propDef += ', nullable: true';
478
523
  }
479
524
  propDef += ' }';
480
525
  entityProps.push(propDef);
481
526
  }
527
+ for(let relationKey of Object.keys(relations)){
528
+ let relation = relations[relationKey];
529
+ let relatedModelClass = sc.capitalizedCamelCase(relation.referencedTable)+'Model';
530
+ let kind = sc.get(this.mikroOrmRelationKindMap, relation.relationType, 'm:1');
531
+ let propDef = relationKey+': {\n';
532
+ propDef += ' kind: \''+kind+'\',\n';
533
+ propDef += ' entity: \''+relatedModelClass+'\'';
534
+ if(relation.fromColumn){
535
+ propDef += ',\n joinColumn: \''+relation.fromColumn+'\'';
536
+ }
537
+ propDef += '\n }';
538
+ entityProps.push(propDef);
539
+ }
540
+ let reverseRelations = this.detectReverseObjectionJsRelations(tableName);
541
+ for(let relationKey of Object.keys(reverseRelations)){
542
+ let relation = reverseRelations[relationKey];
543
+ let relatedModelClass = sc.capitalizedCamelCase(relation.referencingTable)+'Model';
544
+ let kind = sc.get(this.mikroOrmRelationKindMap, relation.relationType, '1:m');
545
+ let mappedBy = this.findMappedByKey(tableName, relation.referencingTable, relation);
546
+ let propDef = relationKey+': {\n';
547
+ propDef += ' kind: \''+kind+'\',\n';
548
+ propDef += ' entity: \''+relatedModelClass+'\'';
549
+ if(mappedBy){
550
+ propDef += ',\n mappedBy: \''+mappedBy+'\'';
551
+ }
552
+ propDef += '\n }';
553
+ entityProps.push(propDef);
554
+ }
482
555
  return entityProps.join(',\n ');
483
556
  }
484
557
  if('prisma' === driverKey){
@@ -502,6 +575,52 @@ class ModelsGeneration extends BaseGenerator
502
575
  return '';
503
576
  }
504
577
 
578
+ findMappedByKey(currentTable, referencingTable, reverseRelation)
579
+ {
580
+ if(!this.allTablesData[referencingTable]){
581
+ return 'related_'+currentTable;
582
+ }
583
+ let referencingTableData = this.allTablesData[referencingTable];
584
+ let forwardRelations = this.detectObjectionJsRelations(referencingTable, referencingTableData);
585
+ for(let forwardKey of Object.keys(forwardRelations)){
586
+ let forward = forwardRelations[forwardKey];
587
+ if(forward.referencedTable === currentTable && forward.toColumn === reverseRelation.fromColumn){
588
+ return forwardKey;
589
+ }
590
+ }
591
+ return 'related_'+currentTable;
592
+ }
593
+
594
+ generateFkMappings(tableName, tableData, driverKey)
595
+ {
596
+ if('mikro-orm' !== driverKey){
597
+ return '';
598
+ }
599
+ if(!tableName || !tableData){
600
+ return '';
601
+ }
602
+ let fkMappings = {};
603
+ let referenceCounts = this.countReferencesPerTable(tableData);
604
+ for(let columnName of Object.keys(tableData.columns)){
605
+ let column = tableData.columns[columnName];
606
+ if(!sc.hasOwn(column, 'referencedTable')){
607
+ continue;
608
+ }
609
+ let relationKey = this.generateForwardRelationKey(column.referencedTable, columnName, referenceCounts);
610
+ let relatedModelClass = sc.capitalizedCamelCase(column.referencedTable)+'Model';
611
+ fkMappings[columnName] = {
612
+ relationKey: relationKey,
613
+ entityName: relatedModelClass,
614
+ referencedColumn: column.referencedColumn,
615
+ nullable: column.nullable || false
616
+ };
617
+ }
618
+ if(0 === Object.keys(fkMappings).length){
619
+ return '';
620
+ }
621
+ return JSON.stringify(fkMappings, null, 4);
622
+ }
623
+
505
624
  generateRegisteredModelsFile(entitiesInfo, existingModels, driverKey, templatePath)
506
625
  {
507
626
  if(!FileHandler.exists(templatePath)){
@@ -541,8 +660,9 @@ class ModelsGeneration extends BaseGenerator
541
660
  }
542
661
  let modelClassName = entity.modelClassName || sc.capitalizedCamelCase(tableName)+'Model';
543
662
  let modelFileName = entity.modelFileName || sc.kebabCase(tableName)+'-model.js';
663
+ let variableName = 'mikro-orm' !== driverKey ? '{ '+modelClassName+' }' : sc.camelCase(tableName)+'Model';
544
664
  registeredModels.push(
545
- 'const { '+modelClassName+' } = require(\'./'+modelFileName.replace('.js', '')+'\');'
665
+ 'const '+variableName+' = require(\'./'+modelFileName.replace('.js', '')+'\');'
546
666
  );
547
667
  }
548
668
  return registeredModels.join('\n');
@@ -559,7 +679,8 @@ class ModelsGeneration extends BaseGenerator
559
679
  }
560
680
  }
561
681
  let modelClassName = entity.modelClassName || sc.capitalizedCamelCase(tableName)+'Model';
562
- registeredEntitiesObject.push(sc.camelCase(tableName)+': '+modelClassName);
682
+ let variableName = 'mikro-orm' === driverKey ? sc.camelCase(tableName)+'Model' : modelClassName;
683
+ registeredEntitiesObject.push(sc.camelCase(tableName)+': '+variableName);
563
684
  }
564
685
  return registeredEntitiesObject.join(',\n ');
565
686
  }
@@ -10,7 +10,7 @@ const { MySQLTablesProvider } = require('../mysql-tables-provider');
10
10
  const { MikroORM } = require('@mikro-orm/core');
11
11
  const { MySqlDriver } = require('@mikro-orm/mysql');
12
12
  const { MongoDriver } = require('@mikro-orm/mongodb');
13
- const { ErrorManager, Logger, sc } = require('@reldens/utils');
13
+ const { Logger, sc } = require('@reldens/utils');
14
14
 
15
15
  class MikroOrmDataServer extends BaseDataServer
16
16
  {
@@ -28,6 +28,27 @@ class MikroOrmDataServer extends BaseDataServer
28
28
  this.warnWhenNoEntities = Boolean(props.warnWhenNoEntities);
29
29
  }
30
30
 
31
+ getEntitiesToRegister()
32
+ {
33
+ if(this.entitiesPath){
34
+ //Logger.debug('MikroORM using entitiesPath');
35
+ return this.entitiesPath;
36
+ }
37
+ if(0 < Object.keys(this.entities).length){
38
+ //Logger.debug('MikroORM using this.entities: '+Object.keys(this.entities).join(', '));
39
+ return this.entities;
40
+ }
41
+ if(!this.rawEntities){
42
+ //Logger.debug('MikroORM has no entities!');
43
+ return [];
44
+ }
45
+ let entities = Object.values(this.rawEntities);
46
+ //Logger.debug('MikroORM extracting from rawEntities, count: '+entities.length);
47
+ return entities.map(rawEntity => {
48
+ return (rawEntity && rawEntity.schema) ? rawEntity.schema : rawEntity;
49
+ });
50
+ }
51
+
31
52
  async connect()
32
53
  {
33
54
  if(this.initialized){
@@ -39,15 +60,7 @@ class MikroOrmDataServer extends BaseDataServer
39
60
  clientUrl: this.connectString,
40
61
  allowGlobalContext: true,
41
62
  };
42
- if(this.rawEntities){
43
- providedSetup.entities = Object.values(this.rawEntities);
44
- }
45
- if(0 < Object.keys(this.entities).length){
46
- providedSetup.entities = this.entities;
47
- }
48
- if(this.entitiesPath){
49
- providedSetup.entities = this.entitiesPath;
50
- }
63
+ providedSetup.entities = this.getEntitiesToRegister();
51
64
  providedSetup.discovery = {warnWhenNoEntities: this.warnWhenNoEntities};
52
65
  this.orm = await MikroORM.init(providedSetup);
53
66
  this.initialized = Date.now();
@@ -60,6 +73,7 @@ class MikroOrmDataServer extends BaseDataServer
60
73
  return false;
61
74
  }
62
75
  await this.orm.close();
76
+ this.initialized = null;
63
77
  return true;
64
78
  }
65
79