@reldens/storage 0.104.0 → 0.106.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.
@@ -9,6 +9,7 @@ const { Logger, sc } = require('@reldens/utils');
9
9
  const { ObjectionJsDataServer } = require('./objection-js/objection-js-data-server');
10
10
  const { MikroOrmDataServer } = require('./mikro-orm/mikro-orm-data-server');
11
11
  const { PrismaDataServer } = require('./prisma/prisma-data-server');
12
+ const { PrismaRelationsMetadataExtractor } = require('./prisma/prisma-relations-metadata-extractor');
12
13
  const { EntitiesGeneration } = require('./generators/entities-generation');
13
14
  const { ModelsGeneration } = require('./generators/models-generation');
14
15
  const { EntitiesConfigGeneration } = require('./generators/entities-config-generation');
@@ -165,125 +166,6 @@ class EntitiesGenerator
165
166
  return false;
166
167
  }
167
168
 
168
- extractPrismaRelationsMetadata()
169
- {
170
- if(!this.prismaClient){
171
- Logger.warning('Missing PrismaClient.');
172
- return;
173
- }
174
- if(!this.server || !(this.server instanceof PrismaDataServer)){
175
- Logger.warning('Server is not a PrismaDataServer instance.');
176
- return;
177
- }
178
- try {
179
- let dmmf = this.prismaClient._runtimeDataModel || this.prismaClient._dmmf?.datamodel;
180
- if(!dmmf || !dmmf.models){
181
- Logger.warning('Could not extract Prisma DMMF data for relations metadata.');
182
- return;
183
- }
184
- if(sc.isArray(dmmf.models)){
185
- this.processArrayModels(dmmf.models);
186
- return;
187
- }
188
- this.processObjectModels(dmmf.models);
189
- } catch(error) {
190
- Logger.warning('Failed to extract Prisma relations metadata: ' + error.message);
191
- }
192
- }
193
-
194
- processArrayModels(models)
195
- {
196
- for(let model of models){
197
- if(!model){
198
- Logger.error('Missing model.');
199
- continue;
200
- }
201
- if(!model.name){
202
- Logger.error('Missing model name.', model);
203
- continue;
204
- }
205
- if(!sc.isArray(model.fields)){
206
- Logger.error('Model fields not an array.', model);
207
- continue;
208
- }
209
- this.processModelForRelations(model.name, model, models);
210
- }
211
- Logger.info('Extracted relations metadata for '+Object.keys(this.prismaRelationsMetadata).length+' models.');
212
- }
213
-
214
- processObjectModels(models)
215
- {
216
- for(let modelName of Object.keys(models)){
217
- let model = models[modelName];
218
- if(!model){
219
- Logger.error('Missing model.');
220
- continue;
221
- }
222
- if(!sc.isArray(model.fields)){
223
- Logger.error('Model fields not an array.', model);
224
- continue;
225
- }
226
- this.processModelForRelations(modelName, model, models);
227
- }
228
- Logger.info('Extracted relations metadata for '+Object.keys(this.prismaRelationsMetadata).length+' models.');
229
- }
230
-
231
- processModelForRelations(modelName, model, allModels)
232
- {
233
- let tableName = modelName.toLowerCase();
234
- this.prismaRelationsMetadata[tableName] = {};
235
- for(let field of model.fields){
236
- if('object' !== field.kind){
237
- continue;
238
- }
239
- let relationType = 'many';
240
- if(!field.isList){
241
- relationType = this.inferRelationType(field, model, allModels, modelName);
242
- }
243
- this.prismaRelationsMetadata[tableName][field.name] = {
244
- type: relationType,
245
- model: field.type,
246
- isList: field.isList,
247
- isOptional: field.isOptional,
248
- relationName: field.relationName
249
- };
250
- }
251
- }
252
-
253
- inferRelationType(field, model, allModels, modelName)
254
- {
255
- if(field.isList){
256
- return 'many';
257
- }
258
- let relatedModel = this.findRelatedModel(allModels, field.type);
259
- if(!relatedModel){
260
- return 'one';
261
- }
262
- let backReference = relatedModel.fields.find(f =>
263
- 'object' === f.kind &&
264
- f.type === modelName &&
265
- f.relationName === field.relationName
266
- );
267
- if(!backReference){
268
- return 'one';
269
- }
270
- if(field.relationFromFields && 0 < field.relationFromFields.length){
271
- return 'one';
272
- }
273
- if(backReference.relationFromFields && 0 < backReference.relationFromFields.length){
274
- return 'one';
275
- }
276
- return 'one';
277
- }
278
-
279
- findRelatedModel(allModels, fieldType)
280
- {
281
- if(sc.isArray(allModels)){
282
- return allModels.find(m => m.name === fieldType);
283
- }
284
- return allModels[fieldType];
285
- }
286
-
287
169
  filterTablesToGenerate(tables, driverKey)
288
170
  {
289
171
  let filteredTables = {};
@@ -386,7 +268,11 @@ class EntitiesGenerator
386
268
  this.prismaClient = this.server.prisma;
387
269
  }
388
270
  //Logger.debug('Extract Prisma relations metadata.');
389
- this.extractPrismaRelationsMetadata();
271
+ this.prismaRelationsMetadata = new PrismaRelationsMetadataExtractor({
272
+ prismaClient: this.prismaClient,
273
+ projectRoot: this.server ? this.server.projectRoot : false,
274
+ projectPath: this.projectPath
275
+ }).extract();
390
276
  }
391
277
  let tables = await this.server.fetchEntitiesFromDatabase();
392
278
  if(!tables){
@@ -215,6 +215,7 @@ class EntitiesGeneration extends BaseGenerator
215
215
  let type = column.type.toLowerCase();
216
216
  this.addTypeAttribute(props, column, type, columnName, referenceCounts);
217
217
  this.addRequiredAttribute(props, column);
218
+ this.addUniqueAttribute(props, column);
218
219
  props.push('dbType: \''+type+'\'');
219
220
  return props;
220
221
  }
@@ -295,6 +296,13 @@ class EntitiesGeneration extends BaseGenerator
295
296
  }
296
297
  }
297
298
 
299
+ addUniqueAttribute(props, column)
300
+ {
301
+ if('UNI' === column.key){
302
+ props.push('isUnique: true');
303
+ }
304
+ }
305
+
298
306
  getReferenceTable(column)
299
307
  {
300
308
  if(column.referencedTable){
@@ -17,21 +17,10 @@ class PrismaMetadataLoader
17
17
 
18
18
  getDmmf()
19
19
  {
20
- try {
21
- if(this.prisma._dmmf){
22
- return this.prisma._dmmf.datamodel;
23
- }
24
- if(this.prisma._getDmmf && sc.isFunction(this.prisma._getDmmf)){
25
- return this.prisma._getDmmf().datamodel;
26
- }
27
- if(this.prisma._runtimeDataModel){
28
- return this.prisma._runtimeDataModel;
29
- }
30
- return null;
31
- } catch(error) {
32
- Logger.warning('Failed to access DMMF: '+error.message);
33
- return null;
20
+ if(this.prisma && this.prisma._runtimeDataModel){
21
+ return this.prisma._runtimeDataModel;
34
22
  }
23
+ return null;
35
24
  }
36
25
 
37
26
  loadModelMetadata(tableName)
@@ -50,6 +39,7 @@ class PrismaMetadataLoader
50
39
  let metadata = {
51
40
  requiredFields: [],
52
41
  optionalFields: [],
42
+ uniqueFields: [],
53
43
  fieldTypes: {},
54
44
  fieldDefaults: {},
55
45
  idFieldType: 'String',
@@ -92,6 +82,9 @@ class PrismaMetadataLoader
92
82
  if(field.isOptional || field.hasDefaultValue){
93
83
  metadata.optionalFields.push(field.name);
94
84
  }
85
+ if(field.isUnique){
86
+ metadata.uniqueFields.push(field.name);
87
+ }
95
88
  if(field.kind === 'scalar' && field.isList === false){
96
89
  let isNumericType = this.isNumericFieldType(field.type);
97
90
  let isReferenceField = field.relationFromFields && 0 < field.relationFromFields.length;
@@ -0,0 +1,137 @@
1
+ /**
2
+ *
3
+ * Reldens - PrismaRelationsMetadataExtractor
4
+ *
5
+ * Builds the per-model relation metadata ({type: 'many'|'one', model, isList, isOptional, relationName})
6
+ * consumed by ModelsGeneration to emit each Prisma model's relationTypes. The DMMF access is reused from
7
+ * PrismaMetadataLoader.getDmmf(). Cardinality (list vs single) is read from schema.prisma's list markers
8
+ * ("[]") because Prisma 7's pruned _runtimeDataModel no longer exposes field.isList.
9
+ *
10
+ */
11
+
12
+ const { PrismaMetadataLoader } = require('./prisma-metadata-loader');
13
+ const { FileHandler } = require('@reldens/server-utils');
14
+ const { Logger, sc } = require('@reldens/utils');
15
+
16
+ class PrismaRelationsMetadataExtractor
17
+ {
18
+
19
+ constructor(props)
20
+ {
21
+ this.prismaClient = sc.get(props, 'prismaClient', false);
22
+ this.projectRoot = sc.get(props, 'projectRoot', false);
23
+ this.projectPath = sc.get(props, 'projectPath', false);
24
+ this.metadataLoader = new PrismaMetadataLoader({prisma: this.prismaClient});
25
+ this.relationsMetadata = {};
26
+ this.schemaListRelations = {};
27
+ }
28
+
29
+ extract()
30
+ {
31
+ if(!this.prismaClient){
32
+ Logger.warning('Missing PrismaClient for relations metadata extraction.');
33
+ return this.relationsMetadata;
34
+ }
35
+ let dmmf = this.metadataLoader.getDmmf();
36
+ if(!dmmf || !dmmf.models){
37
+ Logger.warning('Could not access Prisma DMMF for relations metadata.');
38
+ return this.relationsMetadata;
39
+ }
40
+ this.schemaListRelations = this.resolveSchemaListRelations();
41
+ this.processModels(dmmf.models);
42
+ Logger.info('Extracted relations metadata for '+Object.keys(this.relationsMetadata).length+' models.');
43
+ return this.relationsMetadata;
44
+ }
45
+
46
+ processModels(models)
47
+ {
48
+ if(sc.isArray(models)){
49
+ for(let model of models){
50
+ this.processModel(sc.get(model, 'name', ''), model);
51
+ }
52
+ return;
53
+ }
54
+ for(let modelName of Object.keys(models)){
55
+ this.processModel(modelName, models[modelName]);
56
+ }
57
+ }
58
+
59
+ processModel(modelName, model)
60
+ {
61
+ if('' === modelName || !sc.isArray(sc.get(model, 'fields', false))){
62
+ return;
63
+ }
64
+ let tableName = modelName.toLowerCase();
65
+ this.relationsMetadata[tableName] = {};
66
+ for(let field of model.fields){
67
+ if('object' !== field.kind){
68
+ continue;
69
+ }
70
+ let isListRelation = true === sc.get(sc.get(this.schemaListRelations, tableName, {}), field.name, false);
71
+ this.relationsMetadata[tableName][field.name] = {
72
+ type: isListRelation ? 'many' : 'one',
73
+ model: field.type,
74
+ isList: isListRelation,
75
+ isOptional: field.isOptional,
76
+ relationName: field.relationName
77
+ };
78
+ }
79
+ }
80
+
81
+ resolveSchemaPath()
82
+ {
83
+ let candidatePaths = [];
84
+ if(this.projectRoot){
85
+ candidatePaths.push(FileHandler.joinPaths(this.projectRoot, 'prisma', 'schema.prisma'));
86
+ }
87
+ candidatePaths.push(FileHandler.joinPaths(process.cwd(), 'prisma', 'schema.prisma'));
88
+ if(this.projectPath){
89
+ candidatePaths.push(FileHandler.joinPaths(this.projectPath, 'prisma', 'schema.prisma'));
90
+ }
91
+ for(let candidatePath of candidatePaths){
92
+ if(FileHandler.exists(candidatePath)){
93
+ return candidatePath;
94
+ }
95
+ }
96
+ return false;
97
+ }
98
+
99
+ resolveSchemaListRelations()
100
+ {
101
+ let listRelations = {};
102
+ let schemaPath = this.resolveSchemaPath();
103
+ if(!schemaPath){
104
+ Logger.warning('Prisma schema.prisma not found; relation cardinality may fall back to one.');
105
+ return listRelations;
106
+ }
107
+ let schemaContent = FileHandler.readFile(schemaPath);
108
+ if(!schemaContent){
109
+ return listRelations;
110
+ }
111
+ let currentModel = '';
112
+ for(let schemaLine of schemaContent.toString().split('\n')){
113
+ let line = schemaLine.trim();
114
+ let modelMatch = line.match(/^model\s+(\w+)\s*\{/);
115
+ if(modelMatch){
116
+ currentModel = modelMatch[1].toLowerCase();
117
+ listRelations[currentModel] = {};
118
+ continue;
119
+ }
120
+ if('' === currentModel){
121
+ continue;
122
+ }
123
+ if('}' === line){
124
+ currentModel = '';
125
+ continue;
126
+ }
127
+ let listFieldMatch = line.match(/^(\w+)\s+\w+\[\]/);
128
+ if(listFieldMatch){
129
+ listRelations[currentModel][listFieldMatch[1]] = true;
130
+ }
131
+ }
132
+ return listRelations;
133
+ }
134
+
135
+ }
136
+
137
+ module.exports.PrismaRelationsMetadataExtractor = PrismaRelationsMetadataExtractor;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@reldens/storage",
3
3
  "scope": "@reldens",
4
- "version": "0.104.0",
4
+ "version": "0.106.0",
5
5
  "description": "Reldens - Storage",
6
6
  "author": "Damian A. Pastorini",
7
7
  "license": "MIT",
@@ -51,16 +51,16 @@
51
51
  "test:driver": "node tests/run-tests.js --driver="
52
52
  },
53
53
  "dependencies": {
54
- "@mikro-orm/core": "7.1.5",
55
- "@mikro-orm/mongodb": "7.1.5",
56
- "@mikro-orm/mysql": "7.1.5",
54
+ "@mikro-orm/core": "7.1.6",
55
+ "@mikro-orm/mongodb": "7.1.6",
56
+ "@mikro-orm/mysql": "7.1.6",
57
57
  "@prisma/adapter-mariadb": "7.8.0",
58
58
  "@prisma/client": "7.8.0",
59
59
  "@reldens/server-utils": "^0.53.0",
60
60
  "@reldens/utils": "^0.57.0",
61
61
  "knex": "3.3.0",
62
62
  "mysql": "2.18.1",
63
- "mysql2": "3.22.6",
63
+ "mysql2": "3.23.0",
64
64
  "objection": "3.1.5",
65
65
  "prisma": "7.8.0"
66
66
  }
@@ -26,6 +26,7 @@ class TestCategoriesEntity extends EntityProperties
26
26
  },
27
27
  slug: {
28
28
  isRequired: true,
29
+ isUnique: true,
29
30
  dbType: 'varchar'
30
31
  },
31
32
  description: {
@@ -33,6 +33,7 @@ class TestProductsEntity extends EntityProperties
33
33
  },
34
34
  sku: {
35
35
  isRequired: true,
36
+ isUnique: true,
36
37
  dbType: 'varchar'
37
38
  },
38
39
  description: {
@@ -15,6 +15,7 @@ const RawQueriesTest = require('./integration/test-raw-queries');
15
15
  const EntityManagerTest = require('./unit/test-entity-manager');
16
16
  const TypeMapperTest = require('./unit/test-type-mapper');
17
17
  const DriversUnitTest = require('./unit/test-drivers');
18
+ const EntitiesGenerationTest = require('./unit/test-entities-generation');
18
19
 
19
20
  if(!process.env.RELDENS_TEST_DB_HOST){
20
21
  let envPath = FileHandler.joinPaths(__dirname, '.env.test');
@@ -187,6 +188,11 @@ class RunTests
187
188
  this.allCounts.total += driversUnitResult.total;
188
189
  this.allCounts.passed += driversUnitResult.passed;
189
190
  this.allCounts.failed += driversUnitResult.failed;
191
+ let entitiesGenerationTest = new EntitiesGenerationTest();
192
+ let entitiesGenerationResult = await entitiesGenerationTest.run();
193
+ this.allCounts.total += entitiesGenerationResult.total;
194
+ this.allCounts.passed += entitiesGenerationResult.passed;
195
+ this.allCounts.failed += entitiesGenerationResult.failed;
190
196
  }
191
197
 
192
198
  async runPreFlightChecks(config)
@@ -0,0 +1,152 @@
1
+ /**
2
+ *
3
+ * Reldens - EntitiesGeneration Test
4
+ *
5
+ */
6
+
7
+ const { TestRunner, assert } = require('../utils/test-runner');
8
+ const { EntitiesGeneration } = require('../../lib/generators/entities-generation');
9
+
10
+ class EntitiesGenerationTest
11
+ {
12
+
13
+ constructor()
14
+ {
15
+ this.runner = new TestRunner();
16
+ }
17
+
18
+ createColumn(props)
19
+ {
20
+ return Object.assign(
21
+ {
22
+ name: 'test_column',
23
+ type: 'varchar',
24
+ columnType: 'varchar(100)',
25
+ length: '100',
26
+ nullable: true,
27
+ key: '',
28
+ extra: '',
29
+ default: null
30
+ },
31
+ props
32
+ );
33
+ }
34
+
35
+ async run()
36
+ {
37
+ this.runner.suite('EntitiesGeneration');
38
+ await this.testAddUniqueAttribute();
39
+ await this.testGetPropertyAttributesUnique();
40
+ await this.testGeneratePropertiesConfigUnique();
41
+ return this.runner.getResults();
42
+ }
43
+
44
+ async testAddUniqueAttribute()
45
+ {
46
+ this.runner.group('addUniqueAttribute');
47
+ let generation = new EntitiesGeneration({});
48
+ await this.runner.test('should push isUnique for a UNI column', async () => {
49
+ let props = [];
50
+ generation.addUniqueAttribute(props, this.createColumn({key: 'UNI'}));
51
+ assert.deepStrictEqual(props, ['isUnique: true']);
52
+ });
53
+ await this.runner.test('should not push isUnique for a non indexed column', async () => {
54
+ let props = [];
55
+ generation.addUniqueAttribute(props, this.createColumn({key: ''}));
56
+ assert.deepStrictEqual(props, []);
57
+ });
58
+ await this.runner.test('should not push isUnique for a MUL column', async () => {
59
+ let props = [];
60
+ generation.addUniqueAttribute(props, this.createColumn({key: 'MUL'}));
61
+ assert.deepStrictEqual(props, []);
62
+ });
63
+ await this.runner.test('should not push isUnique for a PRI column', async () => {
64
+ let props = [];
65
+ generation.addUniqueAttribute(props, this.createColumn({key: 'PRI'}));
66
+ assert.deepStrictEqual(props, []);
67
+ });
68
+ }
69
+
70
+ async testGetPropertyAttributesUnique()
71
+ {
72
+ this.runner.group('getPropertyAttributes - isUnique');
73
+ let generation = new EntitiesGeneration({});
74
+ await this.runner.test('should include isUnique for a UNI column', async () => {
75
+ let column = this.createColumn({name: 'slug', key: 'UNI'});
76
+ let props = generation.getPropertyAttributes(column, 'slug', {});
77
+ assert.ok(props.includes('isUnique: true'));
78
+ });
79
+ await this.runner.test('should not include isUnique for a non unique column', async () => {
80
+ let column = this.createColumn({name: 'tags', key: ''});
81
+ let props = generation.getPropertyAttributes(column, 'tags', {});
82
+ assert.deepStrictEqual(props, ['dbType: \'varchar\'']);
83
+ });
84
+ await this.runner.test('should include both isRequired and isUnique for a required unique column', async () => {
85
+ let column = this.createColumn({name: 'sku', key: 'UNI', nullable: false});
86
+ let props = generation.getPropertyAttributes(column, 'sku', {});
87
+ assert.deepStrictEqual(props, ['isRequired: true', 'isUnique: true', 'dbType: \'varchar\'']);
88
+ });
89
+ await this.runner.test('should include isUnique only for a nullable unique column', async () => {
90
+ let column = this.createColumn({name: 'sku', key: 'UNI', nullable: true});
91
+ let props = generation.getPropertyAttributes(column, 'sku', {});
92
+ assert.deepStrictEqual(props, ['isUnique: true', 'dbType: \'varchar\'']);
93
+ });
94
+ await this.runner.test('should not include isUnique for a composite unique member column', async () => {
95
+ let column = this.createColumn({name: 'owner_id', type: 'int', key: 'MUL', nullable: false});
96
+ let props = generation.getPropertyAttributes(column, 'owner_id', {});
97
+ assert.deepStrictEqual(props, ['type: \'number\'', 'isRequired: true', 'dbType: \'int\'']);
98
+ });
99
+ await this.runner.test('should not include isUnique for the primary key column', async () => {
100
+ let column = this.createColumn({name: 'id', type: 'int', key: 'PRI', nullable: false, extra: 'auto_increment'});
101
+ let props = generation.getPropertyAttributes(column, 'id', {});
102
+ assert.deepStrictEqual(props, ['type: \'number\'', 'isRequired: true', 'dbType: \'int\'']);
103
+ });
104
+ await this.runner.test('should include isUnique for a unique reference column', async () => {
105
+ let column = this.createColumn({
106
+ name: 'profile_id',
107
+ type: 'int',
108
+ key: 'UNI',
109
+ nullable: false,
110
+ referencedTable: 'profiles',
111
+ referencedColumn: 'id'
112
+ });
113
+ let props = generation.getPropertyAttributes(column, 'profile_id', {profiles: 1});
114
+ assert.deepStrictEqual(props, [
115
+ 'type: \'reference\'',
116
+ 'reference: \'profiles\'',
117
+ 'alias: \'related_profiles\'',
118
+ 'isRequired: true',
119
+ 'isUnique: true',
120
+ 'dbType: \'int\''
121
+ ]);
122
+ });
123
+ }
124
+
125
+ async testGeneratePropertiesConfigUnique()
126
+ {
127
+ this.runner.group('generatePropertiesConfig - isUnique');
128
+ let generation = new EntitiesGeneration({});
129
+ await this.runner.test('should render isUnique in the generated properties config', async () => {
130
+ let columns = {
131
+ id: this.createColumn({name: 'id', type: 'int', key: 'PRI', nullable: false}),
132
+ slug: this.createColumn({name: 'slug', key: 'UNI', nullable: false}),
133
+ tags: this.createColumn({name: 'tags', key: ''})
134
+ };
135
+ let propertiesConfig = generation.generatePropertiesConfig(columns, false, 'id');
136
+ assert.ok(propertiesConfig.includes('slug: {'));
137
+ assert.ok(propertiesConfig.includes('isUnique: true'));
138
+ assert.strictEqual(propertiesConfig.split('isUnique: true').length - 1, 1);
139
+ });
140
+ await this.runner.test('should not render isUnique when no unique columns exist', async () => {
141
+ let columns = {
142
+ id: this.createColumn({name: 'id', type: 'int', key: 'PRI', nullable: false}),
143
+ tags: this.createColumn({name: 'tags', key: ''})
144
+ };
145
+ let propertiesConfig = generation.generatePropertiesConfig(columns, false, 'id');
146
+ assert.ok(!propertiesConfig.includes('isUnique'));
147
+ });
148
+ }
149
+
150
+ }
151
+
152
+ module.exports = EntitiesGenerationTest;