@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.
- package/.claude/driver-methods-complete-analysis.md +1978 -0
- package/.claude/test-architecture.md +973 -0
- package/CLAUDE.md +132 -3
- package/bin/reldens-storage.js +1 -1
- package/lib/base-driver.js +8 -0
- package/lib/entities-generator.js +1 -3
- package/lib/entity-templates/entities-translations.template +3 -0
- package/lib/entity-templates/mikro-orm-model.template +2 -1
- package/lib/generators/entities-generation.js +12 -1
- package/lib/generators/entities-translations-generation.js +40 -4
- package/lib/generators/models-generation.js +126 -5
- package/lib/mikro-orm/mikro-orm-data-server.js +24 -10
- package/lib/mikro-orm/mikro-orm-driver.js +184 -116
- package/lib/objection-js/objection-js-driver.js +111 -19
- package/lib/prisma/prisma-data-server.js +3 -2
- package/lib/prisma/prisma-driver.js +12 -10
- package/lib/prisma/prisma-filter-processor.js +193 -146
- package/lib/prisma/prisma-metadata-loader.js +0 -4
- package/lib/prisma/prisma-relation-resolver.js +292 -267
- package/lib/prisma/prisma-type-caster.js +280 -260
- package/package.json +15 -6
- package/tests/.env.test.example +7 -0
- package/tests/fixtures/categories-fixtures.js +164 -0
- package/tests/fixtures/expected-entities/entities/test-categories-entity.js +70 -0
- package/tests/fixtures/expected-entities/entities/test-products-entity.js +95 -0
- package/tests/fixtures/expected-entities/entities/test-reviews-entity.js +84 -0
- package/tests/fixtures/expected-entities/entities-config.js +17 -0
- package/tests/fixtures/expected-entities/entities-translations.js +53 -0
- package/tests/fixtures/expected-entities-mikro-orm/models/mikro-orm/registered-models-mikro-orm.js +23 -0
- package/tests/fixtures/expected-entities-mikro-orm/models/mikro-orm/test-categories-model.js +57 -0
- package/tests/fixtures/expected-entities-mikro-orm/models/mikro-orm/test-products-model.js +79 -0
- package/tests/fixtures/expected-entities-mikro-orm/models/mikro-orm/test-reviews-model.js +70 -0
- package/tests/fixtures/expected-entities-objection-js/models/objection-js/registered-models-objection-js.js +23 -0
- package/tests/fixtures/expected-entities-objection-js/models/objection-js/test-categories-model.js +33 -0
- package/tests/fixtures/expected-entities-objection-js/models/objection-js/test-products-model.js +42 -0
- package/tests/fixtures/expected-entities-objection-js/models/objection-js/test-reviews-model.js +33 -0
- package/tests/fixtures/expected-entities-prisma/models/prisma/registered-models-prisma.js +23 -0
- package/tests/fixtures/expected-entities-prisma/models/prisma/test-categories-model.js +43 -0
- package/tests/fixtures/expected-entities-prisma/models/prisma/test-products-model.js +50 -0
- package/tests/fixtures/expected-entities-prisma/models/prisma/test-reviews-model.js +46 -0
- package/tests/fixtures/products-fixtures.js +119 -0
- package/tests/fixtures/reviews-fixtures.js +42 -0
- package/tests/fixtures/sql/test-schema.sql +59 -0
- package/tests/integration/test-drivers.js +257 -0
- package/tests/integration/test-nested-filters.js +408 -0
- package/tests/integration/test-relations.js +349 -0
- package/tests/run-tests.js +213 -0
- package/tests/unit/test-drivers.js +89 -0
- package/tests/unit/test-entities-generator.js +533 -0
- package/tests/unit/test-entity-manager.js +128 -0
- package/tests/unit/test-type-mapper.js +232 -0
- package/tests/utils/custom-reporter.js +73 -0
- package/tests/utils/driver-registry.js +144 -0
- package/tests/utils/prisma-subprocess-worker.js +150 -0
- package/tests/utils/test-helpers.js +726 -0
- 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`.
|
package/bin/reldens-storage.js
CHANGED
package/lib/base-driver.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
|
@@ -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
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|