@reldens/storage 0.52.0 → 0.54.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/README.md CHANGED
@@ -23,32 +23,39 @@ It ensures consistent data access methods across different database types and OR
23
23
  ### CLI Tools
24
24
  Generate entity files directly from your database structure:
25
25
  ```bash
26
- npx reldens-storage generateEntities --user=dbuser --pass=dbpass --database=dbname --driver=objection-js
26
+ npx reldens-storage generateEntities --user=[dbuser] --pass=[dbpass] --database=[dbname] --driver=[objection-js]
27
27
  ```
28
28
 
29
29
  Options:
30
- - `--user` - Database username
31
- - `--pass` - Database password
32
- - `--host` - Database host (default: localhost)
33
- - `--port` - Database port (default: 3306)
34
- - `--database` - Database name
35
- - `--driver` - ORM driver (objection-js|mikro-orm)
36
- - `--client` - Database client (mysql|mysql2|mongodb)
37
- - `--path` - Project path for output files
38
-
39
- Generate Prisma schema for Prisma database:
30
+ - `--user=[username]` - Database username
31
+ - `--pass=[password]` - Database password
32
+ - `--host=[host]` - Database host (default: localhost)
33
+ - `--port=[port]` - Database port (default: 3306)
34
+ - `--database=[name]` - Database name
35
+ - `--driver=[driver]` - ORM driver (objection-js|mikro-orm|prisma)
36
+ - `--client=[client]` - Database client (mysql|mysql2|mongodb)
37
+ - `--path=[path]` - Project path for output files
38
+ - `--override` - Regenerate all files even if they exist
39
+
40
+ Generate Prisma schema:
40
41
  ```bash
41
- npx reldens-storage-prisma --user=dbuser --pass=dbpass --database=dbname
42
+ npx reldens-generate-prisma-schema --host=[host] --port=[port] --user=[dbuser] --password=[dbpass] --database=[dbname]
42
43
  ```
43
44
 
44
45
  Options:
45
- - `--user` - Database username
46
- - `--pass` - Database password
47
- - `--host` - Database host (default: localhost)
48
- - `--port` - Database port (default: 3306)
49
- - `--database` - Database name
50
- - `--client` - Database client (mysql|mysql2|postgresql|mongodb)
51
- - `--path` - Project path for output files
46
+ - `--host=[host]` - Database host (required)
47
+ - `--port=[port]` - Database port (required)
48
+ - `--user=[username]` - Database username (required)
49
+ - `--password=[password]` - Database password (required)
50
+ - `--database=[name]` - Database name (required)
51
+ - `--client=[client]` - Database client (default: mysql)
52
+ - `--debug` - Enable debug mode
53
+ - `--dataProxy` - Enable data proxy
54
+ - `--checkInterval=[ms]` - Check interval in milliseconds (default: 1000)
55
+ - `--maxWaitTime=[ms]` - Max wait time in milliseconds (default: 30000)
56
+ - `--prismaSchemaPath=[path]` - Path to Prisma schema directory
57
+ - `--clientOutputPath=[path]` - Client output path (if not set, uses Prisma default)
58
+ - `--generateBinaryTargets=[targets]` - Comma-separated binary targets (default: native)
52
59
 
53
60
  ## Usage Examples
54
61
 
@@ -96,7 +103,7 @@ const entities = server.generateEntities();
96
103
 
97
104
  First, generate your Prisma schema:
98
105
  ```bash
99
- npx reldens-storage-prisma --user=dbuser --pass=dbpass --database=dbname
106
+ npx reldens-generate-prisma-schema --host=localhost --port=3306 --user=dbuser --password=dbpass --database=dbname
100
107
  ```
101
108
 
102
109
  Then, use the PrismaDataServer in your code:
@@ -119,7 +126,7 @@ await server.connect();
119
126
  const entities = server.generateEntities();
120
127
  ```
121
128
 
122
- Note: The PrismaDataServer requires the Prisma schema to be generated first. Make sure to run the `reldens-storage-prisma` command before using PrismaDataServer.
129
+ Note: The PrismaDataServer requires the Prisma schema to be generated first. Make sure to run the `reldens-generate-prisma-schema` command before using PrismaDataServer.
123
130
 
124
131
  ## Custom Drivers
125
132
 
@@ -7,79 +7,152 @@
7
7
  */
8
8
 
9
9
  const { PrismaSchemaGenerator } = require('../lib/prisma/prisma-schema-generator');
10
- const { Logger } = require('@reldens/utils');
10
+ const { Logger, sc } = require('@reldens/utils');
11
11
 
12
- let args = process.argv.slice(2);
12
+ class PrismaSchemaGeneratorCLI
13
+ {
13
14
 
14
- let connectionData = {
15
- client: 'mysql',
16
- config: {
17
- user: '',
18
- password: '',
19
- host: 'localhost',
20
- database: '',
21
- port: 3306
15
+ constructor()
16
+ {
17
+ this.args = process.argv.slice(2);
18
+ this.config = {};
19
+ this.parseArguments();
22
20
  }
23
- };
24
21
 
25
- let projectPath = process.cwd();
26
- for(let i = 0; i < args.length; i++){
27
- let arg = args[i];
28
- if(!arg.startsWith('--')){
29
- continue;
22
+ parseArguments()
23
+ {
24
+ for(let i = 0; i < this.args.length; i++){
25
+ let arg = this.args[i];
26
+ if(!arg.startsWith('--')){
27
+ continue;
28
+ }
29
+ let equalIndex = arg.indexOf('=');
30
+ if(-1 === equalIndex){
31
+ let flag = arg.substring(2);
32
+ if('debug' === flag || 'dataProxy' === flag || 'help' === flag || 'h' === flag){
33
+ this.config[flag] = true;
34
+ }
35
+ continue;
36
+ }
37
+ let key = arg.substring(2, equalIndex);
38
+ let value = arg.substring(equalIndex + 1);
39
+ if('port' === key || 'checkInterval' === key || 'maxWaitTime' === key){
40
+ this.config[key] = parseInt(value);
41
+ continue;
42
+ }
43
+ if('generateBinaryTargets' === key){
44
+ this.config[key] = value.split(',').map(target => target.trim());
45
+ continue;
46
+ }
47
+ this.config[key] = value;
48
+ }
30
49
  }
31
- let [key, value] = arg.substring(2).split('=');
32
- if(!key || !value){
33
- continue;
34
- }
35
- if('pass' === key){
36
- connectionData.config.password = value;
37
- continue;
38
- }
39
- if('path' === key){
40
- projectPath = value;
41
- continue;
42
- }
43
- if('user' === key){
44
- connectionData.config.user = value;
45
- continue;
50
+
51
+ shouldShowHelp()
52
+ {
53
+ return 0 === this.args.length || sc.get(this.config, 'help', false) || sc.get(this.config, 'h', false);
46
54
  }
47
- if('database' === key){
48
- connectionData.config.database = value;
49
- continue;
55
+
56
+ showHelp()
57
+ {
58
+ Logger.info('');
59
+ Logger.info('Reldens Prisma Schema Generator');
60
+ Logger.info('================================');
61
+ Logger.info('');
62
+ Logger.info('Usage: npx reldens-generate-prisma-schema [options]');
63
+ Logger.info('');
64
+ Logger.info('Required Options:');
65
+ Logger.info(' --host=[host] Database host');
66
+ Logger.info(' --port=[port] Database port');
67
+ Logger.info(' --user=[user] Database user');
68
+ Logger.info(' --password=[password] Database password');
69
+ Logger.info(' --database=[database] Database name');
70
+ Logger.info('');
71
+ Logger.info('Optional Options:');
72
+ Logger.info(' --client=[client] Database client (default: mysql)');
73
+ Logger.info(' --debug Enable debug mode (default: false)');
74
+ Logger.info(' --dataProxy Enable data proxy (default: false)');
75
+ Logger.info(' --checkInterval=[ms] Check interval in milliseconds (default: 1000)');
76
+ Logger.info(' --maxWaitTime=[ms] Max wait time in milliseconds (default: 30000)');
77
+ Logger.info(' --prismaSchemaPath=[path] Path to Prisma schema directory');
78
+ Logger.info(' --clientOutputPath=[path] Client output path (if not set, uses Prisma default)');
79
+ Logger.info(' --generateBinaryTargets=[targets] Comma-separated binary targets (default: native)');
80
+ Logger.info('');
81
+ Logger.info('Example:');
82
+ Logger.info(' npx reldens-generate-prisma-schema --host=localhost --port=3306 --user=root --password=secret --database=mydb');
83
+ Logger.info('');
84
+ Logger.info('Advanced Example:');
85
+ Logger.info(' npx reldens-generate-prisma-schema --host=localhost --port=3306 --user=root --password=secret --database=mydb --client=postgresql --debug --dataProxy --checkInterval=2000 --maxWaitTime=60000 --prismaSchemaPath=./custom/prisma --clientOutputPath=./custom/client --generateBinaryTargets=native,debian-openssl-1.1.x');
86
+ Logger.info('');
50
87
  }
51
- if('host' === key){
52
- connectionData.config.host = value;
53
- continue;
88
+
89
+ validateRequiredArgs()
90
+ {
91
+ let requiredArgs = ['host', 'port', 'user', 'password', 'database'];
92
+ let missing = [];
93
+ for(let arg of requiredArgs){
94
+ if(!this.config[arg]){
95
+ missing.push(arg);
96
+ }
97
+ }
98
+ if(0 < missing.length){
99
+ Logger.error('Missing required arguments: ' + missing.join(', '));
100
+ this.showHelp();
101
+ return false;
102
+ }
103
+ return true;
54
104
  }
55
- if('port' === key){
56
- connectionData.config.port = value;
57
- continue;
105
+
106
+ getSchemaConfig()
107
+ {
108
+ return {
109
+ config: {
110
+ host: this.config.host,
111
+ port: this.config.port,
112
+ user: this.config.user,
113
+ password: this.config.password,
114
+ database: this.config.database
115
+ },
116
+ client: sc.get(this.config, 'client', 'mysql'),
117
+ debug: sc.get(this.config, 'debug', false),
118
+ dataProxy: sc.get(this.config, 'dataProxy', false),
119
+ checkInterval: sc.get(this.config, 'checkInterval', 1000),
120
+ maxWaitTime: sc.get(this.config, 'maxWaitTime', 30000),
121
+ prismaSchemaPath: sc.get(this.config, 'prismaSchemaPath'),
122
+ clientOutputPath: sc.get(this.config, 'clientOutputPath'),
123
+ generateBinaryTargets: sc.get(this.config, 'generateBinaryTargets')
124
+ };
58
125
  }
59
- if('client' === key){
60
- connectionData.client = value;
126
+
127
+ async run()
128
+ {
129
+ if(this.shouldShowHelp()){
130
+ this.showHelp();
131
+ return true;
132
+ }
133
+ if(!this.validateRequiredArgs()){
134
+ return false;
135
+ }
136
+ let schemaConfig = this.getSchemaConfig();
137
+ let generator = new PrismaSchemaGenerator(schemaConfig);
138
+ let success = await generator.generate();
139
+ if(!success){
140
+ Logger.critical('Failed to generate Prisma schema');
141
+ return false;
142
+ }
143
+ Logger.info('Prisma schema generation completed successfully');
144
+ return true;
61
145
  }
62
- }
63
146
 
64
- if(!connectionData.config.user || !connectionData.config.database){
65
- Logger.error('Required parameters missing.');
66
- Logger.error(
67
- 'Usage:',
68
- 'npx reldens-storage generatePrismaSchema --user=[db-username] --pass=[db-password] --host=[db-host] --database=[db-name] --client=[db-client] --path=[project-path]'
69
- );
70
- process.exit();
71
147
  }
72
148
 
73
- let generator = new PrismaSchemaGenerator({...connectionData, prismaSchemaPath: projectPath+'/prisma'});
74
-
75
- generator.generate().then((success) => {
149
+ let generator = new PrismaSchemaGeneratorCLI();
150
+ generator.run().then((success) => {
76
151
  if(!success){
77
- Logger.error('Prisma schema generation failed.');
78
- process.exit();
152
+ process.exit(1);
79
153
  }
80
- Logger.info('Prisma schema generation completed successfully!');
81
- process.exit();
154
+ process.exit(0);
82
155
  }).catch((error) => {
83
- Logger.error('Error during Prisma schema generation: '+error.message);
84
- process.exit();
156
+ Logger.critical('Unexpected error: ' + error.message);
157
+ process.exit(1);
85
158
  });
@@ -7,69 +7,124 @@
7
7
  */
8
8
 
9
9
  const { EntitiesGenerator } = require('../lib/entities-generator');
10
- const { Logger } = require('@reldens/utils');
10
+ const { Logger, sc } = require('@reldens/utils');
11
11
 
12
- let args = process.argv.slice(2);
13
- let command = args[0];
12
+ class StorageEntitiesGenerator
13
+ {
14
14
 
15
- if('generateEntities' !== command){
16
- Logger.error('Unknown command. Available command "generateEntities".');
17
- process.exit();
18
- }
19
-
20
- let connectionData = {
21
- driver: 'objection-js',
22
- client: 'mysql2',
23
- user: '',
24
- password: '',
25
- host: 'localhost',
26
- database: '',
27
- port: 3306
28
- };
29
-
30
- let projectPath = process.cwd();
31
- for(let i = 1; i < args.length; i++){
32
- let arg = args[i];
33
- if(!arg.startsWith('--')){
34
- continue;
15
+ constructor()
16
+ {
17
+ this.args = process.argv.slice(2);
18
+ this.command = this.args[0];
19
+ this.config = {};
20
+ this.projectPath = process.cwd();
21
+ this.isOverride = false;
22
+ this.parseArguments();
35
23
  }
36
- let [key, value] = arg.substring(2).split('=');
37
- if(!key || !value){
38
- continue;
24
+
25
+ parseArguments()
26
+ {
27
+ for(let i = 1; i < this.args.length; i++){
28
+ let arg = this.args[i];
29
+ if(!arg.startsWith('--')){
30
+ continue;
31
+ }
32
+ let equalIndex = arg.indexOf('=');
33
+ if(-1 === equalIndex){
34
+ let flag = arg.substring(2);
35
+ if('override' === flag){
36
+ this.isOverride = true;
37
+ }
38
+ continue;
39
+ }
40
+ let key = arg.substring(2, equalIndex);
41
+ let value = arg.substring(equalIndex + 1);
42
+ if('path' === key){
43
+ this.projectPath = value;
44
+ continue;
45
+ }
46
+ if('pass' === key){
47
+ this.config['password'] = value;
48
+ continue;
49
+ }
50
+ if('port' === key){
51
+ this.config[key] = parseInt(value);
52
+ continue;
53
+ }
54
+ this.config[key] = value;
55
+ }
39
56
  }
40
- if('pass' === key){
41
- connectionData.password = value;
42
- continue;
57
+
58
+ getConnectionData()
59
+ {
60
+ let connectionData = {
61
+ driver: sc.get(this.config, 'driver', 'objection-js'),
62
+ client: sc.get(this.config, 'client', 'mysql2'),
63
+ user: sc.get(this.config, 'user', ''),
64
+ password: sc.get(this.config, 'password', ''),
65
+ host: sc.get(this.config, 'host', 'localhost'),
66
+ database: sc.get(this.config, 'database', ''),
67
+ port: sc.get(this.config, 'port', 3306)
68
+ };
69
+ if('mikro-orm' === connectionData.driver && !this.config.client){
70
+ connectionData.client = 'mysql';
71
+ }
72
+ return connectionData;
43
73
  }
44
- if('path' === key){
45
- projectPath = value;
46
- continue;
74
+
75
+ validateCommand()
76
+ {
77
+ if('generateEntities' !== this.command){
78
+ Logger.error('Unknown command. Available command "generateEntities".');
79
+ return false;
80
+ }
81
+ return true;
47
82
  }
48
- connectionData[key] = value;
49
- }
50
83
 
51
- if('mikro-orm' === connectionData.driver && !args.find(arg => arg.startsWith('--client='))){
52
- connectionData.client = 'mysql';
53
- }
84
+ validateRequiredArgs(connectionData)
85
+ {
86
+ if(!connectionData.user || !connectionData.database){
87
+ Logger.error('Required parameters missing.');
88
+ Logger.error('Usage: npx reldens-storage generateEntities --user=[db-username] --pass=[db-password] --host=[db-host] --database=[db-name] --driver=[driver-map-key] --client=[db-client] --path=[project-path] --override');
89
+ Logger.error('');
90
+ Logger.error('Optional flags:');
91
+ Logger.error(' --override Regenerate all files even if they exist');
92
+ return false;
93
+ }
94
+ return true;
95
+ }
54
96
 
55
- if(!connectionData.user || !connectionData.database){
56
- Logger.error('Required parameters missing.');
57
- Logger.error(
58
- 'Usage:',
59
- 'npx reldens-storage generateEntities --user=[db-username] --pass=[db-password] --host=[db-host] --database=[db-name] --driver=[driver-map-key] --client=[db-client] --path=[project-path]'
60
- );
61
- process.exit();
97
+ async run()
98
+ {
99
+ if(!this.validateCommand()){
100
+ return false;
101
+ }
102
+ let connectionData = this.getConnectionData();
103
+ if(!this.validateRequiredArgs(connectionData)){
104
+ return false;
105
+ }
106
+ let generator = new EntitiesGenerator({
107
+ connectionData,
108
+ projectPath: this.projectPath,
109
+ isOverride: this.isOverride
110
+ });
111
+ let success = await generator.generate();
112
+ if(!success){
113
+ Logger.error('Entity generation failed.');
114
+ return false;
115
+ }
116
+ Logger.info('Entity generation completed successfully!');
117
+ return true;
118
+ }
62
119
  }
63
120
 
64
- let generator = new EntitiesGenerator({connectionData, projectPath});
65
- generator.generate().then((success) => {
121
+ let generator = new StorageEntitiesGenerator();
122
+ generator.run().then((success) => {
66
123
  if(!success){
67
- Logger.error('Entity generation failed.');
68
- process.exit();
124
+ process.exit(1);
69
125
  }
70
- Logger.info('Entity generation completed successfully!');
71
- process.exit();
126
+ process.exit(0);
72
127
  }).catch((error) => {
73
128
  Logger.error('Error during entity generation: '+error.message);
74
- process.exit();
129
+ process.exit(1);
75
130
  });
@@ -47,6 +47,8 @@ class EntitiesGenerator
47
47
  this.isOverride = sc.get(props, 'isOverride', false);
48
48
  this.generatedEntities = {};
49
49
  this.existingEntities = {};
50
+ this.existingEntityFields = {};
51
+ this.existingModels = {};
50
52
  }
51
53
 
52
54
  async generateEntityFile(tableName, tableData)
@@ -57,7 +59,7 @@ class EntitiesGenerator
57
59
  return false;
58
60
  }
59
61
  let entityTemplateContent = FileHandler.fetchFileContents(entityTemplatePath);
60
- if(!entityTemplateContent) {
62
+ if(!entityTemplateContent){
61
63
  Logger.critical('Failed to read entity template file: '+entityTemplatePath);
62
64
  return false;
63
65
  }
@@ -158,7 +160,7 @@ class EntitiesGenerator
158
160
  let propertiesConfig = [];
159
161
  for(let columnName of Object.keys(columns)){
160
162
  let column = columns[columnName];
161
- let propertyKey = titleProperty && columnName === titleProperty ? '[titleProperty]' : columnName;
163
+ let propertyKey = titleProperty && titleProperty === columnName ? '[titleProperty]' : columnName;
162
164
  if('id' === columnName){
163
165
  propertiesConfig.push(columnName+': {}');
164
166
  continue;
@@ -195,7 +197,7 @@ class EntitiesGenerator
195
197
  props.push('type: \'datetime\'');
196
198
  return;
197
199
  }
198
- if('text' === type || 'longtext' === type || 'mediumtext' === type || 'tinytext' === type){
200
+ if('text' === type || 'longtext' === type || 'mediumtext' === type || 'tinytext' === type || 'json' === type){
199
201
  props.push('type: \'textarea\'');
200
202
  return;
201
203
  }
@@ -332,7 +334,7 @@ class EntitiesGenerator
332
334
  let entityPropertiesDefinition = this.getEntityPropertiesDefinition(tableData.columns, driverKey);
333
335
  let replacements = {
334
336
  modelClassName,
335
- tableName: driverKey === 'prisma' ? tableName.toLowerCase() : tableName,
337
+ tableName: 'prisma' === driverKey ? tableName.toLowerCase() : tableName,
336
338
  modelPropertiesList,
337
339
  modelPropertiesConstructor,
338
340
  modelRelations,
@@ -341,10 +343,7 @@ class EntitiesGenerator
341
343
  let modelContent = this.applyReplacements(modelTemplateContent, replacements);
342
344
  let fileName = sc.kebabCase(tableName)+'-model.js';
343
345
  let driverFolder = FileHandler.joinPaths(this.modelsFolder, driverKey);
344
- if(!FileHandler.createFolder(driverFolder)){
345
- Logger.critical('Failed to create driver folder.');
346
- return false;
347
- }
346
+ FileHandler.createFolder(driverFolder);
348
347
  let filePath = FileHandler.joinPaths(driverFolder, fileName);
349
348
  if(!FileHandler.writeFile(filePath, modelContent)){
350
349
  Logger.critical('Failed to write model file: '+fileName);
@@ -410,32 +409,166 @@ class EntitiesGenerator
410
409
  }
411
410
  let tableName = file.replace('-entity.js', '').replace(/-/g, '_');
412
411
  this.existingEntities[tableName] = {entityFileName: file, tableName};
412
+ this.detectExistingEntityFields(tableName, file);
413
413
  }
414
+ this.detectExistingModels();
414
415
  Logger.info('Detected '+Object.keys(this.existingEntities).length+' existing entities.');
415
416
  }
416
417
 
417
- filterTablesToGenerate(tables)
418
+ detectExistingModels()
418
419
  {
419
- if(this.isOverride){
420
- return tables;
420
+ if(!FileHandler.exists(this.modelsFolder)){
421
+ return;
422
+ }
423
+ let driverFolders = FileHandler.fetchSubFoldersList(this.modelsFolder);
424
+ for(let driverKey of driverFolders){
425
+ let driverFolder = FileHandler.joinPaths(this.modelsFolder, driverKey);
426
+ let modelFiles = FileHandler.getFilesInFolder(driverFolder, ['.js']);
427
+ for(let file of modelFiles){
428
+ if(!file.endsWith('-model.js')){
429
+ continue;
430
+ }
431
+ let tableName = file.replace('-model.js', '').replace(/-/g, '_');
432
+ if(!this.existingModels[tableName]){
433
+ this.existingModels[tableName] = {};
434
+ }
435
+ this.existingModels[tableName][driverKey] = {
436
+ modelFileName: file,
437
+ driverKey: driverKey
438
+ };
439
+ }
440
+ }
441
+ }
442
+
443
+ detectExistingEntityFields(tableName, fileName)
444
+ {
445
+ let filePath = FileHandler.joinPaths(this.entitiesFolder, fileName);
446
+ let fileContent = FileHandler.readFile(filePath);
447
+ if(!fileContent){
448
+ return;
449
+ }
450
+ let fieldsMatch = fileContent.match(/properties\s*=\s*\{([^}]+)\}/s);
451
+ if(!fieldsMatch){
452
+ return;
453
+ }
454
+ let propertiesContent = fieldsMatch[1];
455
+ let fieldMatches = propertiesContent.match(/(\w+):\s*\{[^}]*\}/g);
456
+ if(!fieldMatches){
457
+ return;
458
+ }
459
+ this.existingEntityFields[tableName] = [];
460
+ for(let fieldMatch of fieldMatches){
461
+ let fieldName = fieldMatch.split(':')[0].trim();
462
+ this.existingEntityFields[tableName].push(fieldName);
463
+ }
464
+ }
465
+
466
+ entityNeedsUpdate(tableName, tableData)
467
+ {
468
+ if(!this.existingEntityFields[tableName]){
469
+ return true;
470
+ }
471
+ let existingFields = this.existingEntityFields[tableName];
472
+ let databaseFields = Object.keys(tableData.columns);
473
+ for(let dbField of databaseFields){
474
+ if(!existingFields.includes(dbField)){
475
+ return true;
476
+ }
421
477
  }
478
+ for(let existingField of existingFields){
479
+ if('id' === existingField){
480
+ continue;
481
+ }
482
+ if(!databaseFields.includes(existingField)){
483
+ return true;
484
+ }
485
+ }
486
+ return false;
487
+ }
488
+
489
+ filterTablesToGenerate(tables, driverKey)
490
+ {
422
491
  let filteredTables = {};
423
492
  let newTablesCount = 0;
493
+ let updateTablesCount = 0;
494
+ let missingConfigCount = 0;
495
+ let missingModelCount = 0;
424
496
  for(let tableName of Object.keys(tables)){
497
+ let needsGeneration = false;
498
+ let reasons = [];
425
499
  if(!this.existingEntities[tableName]){
426
- filteredTables[tableName] = tables[tableName];
500
+ needsGeneration = true;
427
501
  newTablesCount++;
502
+ reasons.push('new entity');
503
+ }
504
+ if(this.existingEntities[tableName]){
505
+ if(this.entityNeedsUpdate(tableName, tables[tableName])){
506
+ needsGeneration = true;
507
+ updateTablesCount++;
508
+ reasons.push('fields mismatch');
509
+ }
510
+ if(!this.entityExistsInConfig(tableName)){
511
+ needsGeneration = true;
512
+ missingConfigCount++;
513
+ reasons.push('missing from config');
514
+ }
515
+ }
516
+ if(!this.existingModels[tableName] || !this.existingModels[tableName][driverKey]){
517
+ needsGeneration = true;
518
+ missingModelCount++;
519
+ reasons.push('missing model');
520
+ }
521
+ if(this.existingModels[tableName] && this.existingModels[tableName][driverKey]){
522
+ if(!this.modelExistsInRegistered(tableName, driverKey)){
523
+ needsGeneration = true;
524
+ reasons.push('missing from registered models');
525
+ }
526
+ }
527
+ if(needsGeneration || this.isOverride){
528
+ filteredTables[tableName] = tables[tableName];
529
+ if(0 < reasons.length){
530
+ Logger.info('Entity '+tableName+' needs generation: '+reasons.join(', '));
531
+ }
428
532
  }
429
533
  }
430
- if(0 === newTablesCount){
431
- Logger.info('No new tables found. All entities are up to date.');
534
+ if(0 === newTablesCount && 0 === updateTablesCount && 0 === missingConfigCount && 0 === missingModelCount){
535
+ Logger.info('No new tables found and all entities are properly configured.');
536
+ return filteredTables;
432
537
  }
433
538
  if(0 < newTablesCount){
434
539
  Logger.info('Found '+newTablesCount+' new tables to generate entities for.');
435
540
  }
541
+ if(0 < updateTablesCount){
542
+ Logger.info('Found '+updateTablesCount+' existing entities that need updates.');
543
+ }
544
+ if(0 < missingConfigCount){
545
+ Logger.info('Found '+missingConfigCount+' entities missing from config.');
546
+ }
547
+ if(0 < missingModelCount){
548
+ Logger.info('Found '+missingModelCount+' entities missing models.');
549
+ }
436
550
  return filteredTables;
437
551
  }
438
552
 
553
+ entityExistsInConfig(tableName)
554
+ {
555
+ if(!FileHandler.exists(this.entitiesConfigPath)){
556
+ return false;
557
+ }
558
+ let configContent = FileHandler.readFile(this.entitiesConfigPath);
559
+ return configContent && configContent.includes(sc.camelCase(tableName)+':');
560
+ }
561
+
562
+ modelExistsInRegistered(tableName, driverKey)
563
+ {
564
+ let registeredPath = FileHandler.joinPaths(this.modelsFolder, driverKey, 'registered-models-'+driverKey+'.js');
565
+ if(!FileHandler.exists(registeredPath)){
566
+ return false;
567
+ }
568
+ let registeredContent = FileHandler.readFile(registeredPath);
569
+ return registeredContent && registeredContent.includes(sc.camelCase(tableName)+':');
570
+ }
571
+
439
572
  generateEntitiesConfigFile()
440
573
  {
441
574
  if(this.isOverride){
@@ -447,7 +580,7 @@ class EntitiesGenerator
447
580
  regenerateEntitiesConfigFile()
448
581
  {
449
582
  let configTemplateContent = FileHandler.fetchFileContents(this.templates['entities-config']);
450
- if(!configTemplateContent) {
583
+ if(!configTemplateContent){
451
584
  Logger.critical('Failed to read entities config template file: '+this.templates['entities-config']);
452
585
  return false;
453
586
  }
@@ -473,8 +606,8 @@ class EntitiesGenerator
473
606
  return true;
474
607
  }
475
608
  if(!FileHandler.exists(this.entitiesConfigPath)){
476
- Logger.error('Entities config file does not exist, cannot append new entities.');
477
- return false;
609
+ Logger.info('Entities config file does not exist, creating new file.');
610
+ return this.regenerateEntitiesConfigFile();
478
611
  }
479
612
  let existingContent = FileHandler.readFile(this.entitiesConfigPath);
480
613
  if(!existingContent){
@@ -492,37 +625,66 @@ class EntitiesGenerator
492
625
  if(!existingContent.includes(requireStatement)){
493
626
  newRequires.push(requireStatement);
494
627
  }
495
- if(!existingContent.includes(sc.camelCase(tableName)+':')){
628
+ if(!this.entityExistsInConfig(tableName)){
496
629
  newConfigs.push(configEntry);
497
630
  }
498
631
  }
499
632
  if(0 === newRequires.length && 0 === newConfigs.length){
500
633
  return true;
501
634
  }
635
+ let normalizedContent = existingContent.replace(/\s+/g, ' ');
636
+ let entitiesConfigPattern = /let\s+entitiesConfig\s*=\s*\{/;
637
+ let entitiesConfigMatch = normalizedContent.match(entitiesConfigPattern);
638
+ if(!entitiesConfigMatch){
639
+ Logger.error('Could not find entitiesConfig variable in config file.');
640
+ return false;
641
+ }
642
+ let entitiesConfigStart = existingContent.indexOf(entitiesConfigMatch[0]);
643
+ let searchStart = entitiesConfigStart + entitiesConfigMatch[0].length;
644
+ let braceCount = 1;
645
+ let entitiesConfigEnd = -1;
646
+ for(let i = searchStart; i < existingContent.length; i++){
647
+ if('{' === existingContent[i]){
648
+ braceCount++;
649
+ }
650
+ if('}' === existingContent[i]){
651
+ braceCount--;
652
+ if(0 === braceCount){
653
+ entitiesConfigEnd = i;
654
+ break;
655
+ }
656
+ }
657
+ }
658
+ if(-1 === entitiesConfigEnd){
659
+ Logger.error('Could not find end of entitiesConfig object.');
660
+ return false;
661
+ }
502
662
  let firstRequirePosition = existingContent.indexOf('const {');
503
- let entitiesConfigEnd = existingContent.indexOf('};\n\nmodule.exports');
504
- if(-1 === firstRequirePosition || -1 === entitiesConfigEnd){
505
- Logger.error('Could not find insertion points in config file.');
663
+ if(-1 === firstRequirePosition){
664
+ Logger.error('Could not find require statements in config file.');
506
665
  return false;
507
666
  }
508
667
  let beforeFirstRequire = existingContent.substring(0, firstRequirePosition);
509
668
  let afterFirstRequire = existingContent.substring(firstRequirePosition, entitiesConfigEnd);
669
+ let afterConfig = existingContent.substring(entitiesConfigEnd);
510
670
  let updatedContent = beforeFirstRequire;
511
671
  if(0 < newRequires.length){
512
672
  updatedContent += newRequires.join('\n') + '\n';
513
673
  }
514
674
  updatedContent += afterFirstRequire;
515
675
  if(0 < newConfigs.length){
516
- if(!afterFirstRequire.trim().endsWith(',')){
676
+ let trimmedContent = afterFirstRequire.trimEnd();
677
+ if(!trimmedContent.endsWith(',')){
517
678
  updatedContent = updatedContent.trimEnd() + ',';
518
679
  }
519
- updatedContent += '\n ' + newConfigs.join(',\n ') + '\n';
680
+ updatedContent += '\n ' + newConfigs.join(',\n ');
520
681
  }
521
- updatedContent += existingContent.substring(entitiesConfigEnd);
682
+ updatedContent += afterConfig;
522
683
  if(!FileHandler.writeFile(this.entitiesConfigPath, updatedContent)){
523
684
  Logger.error('Failed to append to entities config file: '+this.entitiesConfigPath);
524
685
  return false;
525
686
  }
687
+ Logger.info('Updated entities config file with '+newConfigs.length+' new entities.');
526
688
  return true;
527
689
  }
528
690
 
@@ -591,8 +753,8 @@ class EntitiesGenerator
591
753
  return true;
592
754
  }
593
755
  if(!FileHandler.exists(this.entitiesTranslationsPath)){
594
- Logger.error('Entities translations file does not exist, cannot append new entities.');
595
- return false;
756
+ Logger.info('Entities translations file does not exist, creating new file.');
757
+ return this.regenerateEntitiesTranslationsFile();
596
758
  }
597
759
  let existingContent = FileHandler.readFile(this.entitiesTranslationsPath);
598
760
  if(!existingContent){
@@ -611,22 +773,45 @@ class EntitiesGenerator
611
773
  if(0 === newTranslations.length){
612
774
  return true;
613
775
  }
614
- let labelsEnd = existingContent.indexOf('\n }\n');
615
- if(-1 === existingContent.indexOf('labels: {') || -1 === labelsEnd){
616
- Logger.error('Could not find labels object boundaries in existing translations file.');
776
+ let normalizedContent = existingContent.replace(/\s+/g, ' ');
777
+ let labelsPattern = /labels\s*:\s*\{/;
778
+ let labelsMatch = normalizedContent.match(labelsPattern);
779
+ if(!labelsMatch){
780
+ Logger.error('Could not find labels object in translations file.');
781
+ return false;
782
+ }
783
+ let labelsStart = existingContent.indexOf(labelsMatch[0]);
784
+ let searchStart = labelsStart + labelsMatch[0].length;
785
+ let braceCount = 1;
786
+ let labelsEnd = -1;
787
+ for(let i = searchStart; i < existingContent.length; i++){
788
+ if('{' === existingContent[i]){
789
+ braceCount++;
790
+ }
791
+ if('}' === existingContent[i]){
792
+ braceCount--;
793
+ if(0 === braceCount){
794
+ labelsEnd = i;
795
+ break;
796
+ }
797
+ }
798
+ }
799
+ if(-1 === labelsEnd){
800
+ Logger.error('Could not find end of labels object.');
617
801
  return false;
618
802
  }
619
803
  let beforeLabelsEnd = existingContent.substring(0, labelsEnd);
620
- if(!beforeLabelsEnd.trim().endsWith(',')){
621
- beforeLabelsEnd = beforeLabelsEnd.trimEnd() + ',';
804
+ let contentBeforeEnd = beforeLabelsEnd.trimEnd();
805
+ let lastChar = contentBeforeEnd[contentBeforeEnd.length - 1];
806
+ if('{' !== lastChar && ',' !== lastChar){
807
+ beforeLabelsEnd = contentBeforeEnd + ',';
622
808
  }
623
- if(!FileHandler.writeFile(
624
- this.entitiesTranslationsPath,
625
- beforeLabelsEnd + '\n ' + newTranslations.join(',\n ') + existingContent.substring(labelsEnd)
626
- )){
809
+ let updatedContent = beforeLabelsEnd + '\n ' + newTranslations.join(',\n ') + existingContent.substring(labelsEnd);
810
+ if(!FileHandler.writeFile(this.entitiesTranslationsPath, updatedContent)){
627
811
  Logger.error('Failed to append to entities translations file: '+this.entitiesTranslationsPath);
628
812
  return false;
629
813
  }
814
+ Logger.info('Updated translations file with '+newTranslations.length+' new labels.');
630
815
  return true;
631
816
  }
632
817
 
@@ -677,7 +862,9 @@ class EntitiesGenerator
677
862
  for(let tableName of Object.keys(allEntities)){
678
863
  let entity = allEntities[tableName];
679
864
  if(entity.driverKey && driverKey !== entity.driverKey){
680
- continue;
865
+ if(!this.existingModels[tableName] || !this.existingModels[tableName][driverKey]){
866
+ continue;
867
+ }
681
868
  }
682
869
  let modelClassName = entity.modelClassName || sc.capitalizedCamelCase(tableName)+'Model';
683
870
  let modelFileName = entity.modelFileName || sc.kebabCase(tableName)+'-model.js';
@@ -694,7 +881,9 @@ class EntitiesGenerator
694
881
  for(let tableName of Object.keys(allEntities)){
695
882
  let entity = allEntities[tableName];
696
883
  if(entity.driverKey && driverKey !== entity.driverKey){
697
- continue;
884
+ if(!this.existingModels[tableName] || !this.existingModels[tableName][driverKey]){
885
+ continue;
886
+ }
698
887
  }
699
888
  let modelClassName = entity.modelClassName || sc.capitalizedCamelCase(tableName)+'Model';
700
889
  registeredEntitiesObject.push(sc.camelCase(tableName)+': '+modelClassName);
@@ -731,10 +920,6 @@ class EntitiesGenerator
731
920
  return false;
732
921
  }
733
922
  this.detectExistingEntities();
734
- let tablesToGenerate = this.filterTablesToGenerate(tables);
735
- if(0 === Object.keys(tablesToGenerate).length && !this.isOverride){
736
- return true;
737
- }
738
923
  let driverKey = sc.get(
739
924
  this.connectionData,
740
925
  'driver',
@@ -744,6 +929,10 @@ class EntitiesGenerator
744
929
  Logger.critical('Failed to fetch driver key.');
745
930
  return false;
746
931
  }
932
+ let tablesToGenerate = this.filterTablesToGenerate(tables, driverKey);
933
+ if(0 === Object.keys(tablesToGenerate).length && !this.isOverride){
934
+ return true;
935
+ }
747
936
  for(let tableName of Object.keys(tablesToGenerate)){
748
937
  let tableData = tablesToGenerate[tableName];
749
938
  await this.generateEntityFile(tableName, tableData);
@@ -32,6 +32,7 @@ class PrismaDriver extends BaseDriver
32
32
  this.requiredFields = [];
33
33
  this.fieldTypes = {};
34
34
  this.idFieldType = 'String';
35
+ this.referenceFields = {};
35
36
  this.initFieldMetadata();
36
37
  }
37
38
 
@@ -55,6 +56,13 @@ class PrismaDriver extends BaseDriver
55
56
  if(field.isId){
56
57
  this.idFieldType = field.type;
57
58
  }
59
+ if(field.kind === 'scalar' && field.isList === false){
60
+ let isNumericType = ('Int' === field.type || 'BigInt' === field.type || 'Float' === field.type || 'Decimal' === field.type);
61
+ let isReferenceField = field.relationFromFields && 0 < field.relationFromFields.length;
62
+ if(isNumericType || isReferenceField){
63
+ this.referenceFields[field.name] = field.type;
64
+ }
65
+ }
58
66
  }
59
67
  } catch(error) {
60
68
  Logger.warning('Could not initialize field metadata: '+error.message);
@@ -75,6 +83,26 @@ class PrismaDriver extends BaseDriver
75
83
  return String(id);
76
84
  }
77
85
 
86
+ castNumericValue(value, fieldType)
87
+ {
88
+ if(null === value || undefined === value || '' === value){
89
+ return null;
90
+ }
91
+ if('Int' === fieldType || 'BigInt' === fieldType){
92
+ let numericValue = Number(value);
93
+ if(!isNaN(numericValue)){
94
+ return Math.floor(numericValue);
95
+ }
96
+ }
97
+ if('Float' === fieldType || 'Decimal' === fieldType){
98
+ let numericValue = Number(value);
99
+ if(!isNaN(numericValue)){
100
+ return numericValue;
101
+ }
102
+ }
103
+ return value;
104
+ }
105
+
78
106
  databaseName()
79
107
  {
80
108
  return this.config.database || '';
@@ -109,6 +137,10 @@ class PrismaDriver extends BaseDriver
109
137
  }
110
138
  if(this.isDateTimeField(key)){
111
139
  data[key] = this.convertToDate(data[key], key);
140
+ continue;
141
+ }
142
+ if(sc.hasOwn(this.referenceFields, key)){
143
+ data[key] = this.castNumericValue(data[key], this.referenceFields[key]);
112
144
  }
113
145
  }
114
146
  return data;
@@ -173,6 +205,12 @@ class PrismaDriver extends BaseDriver
173
205
  if('id' === key && !sc.isArray(operatorValue)){
174
206
  operatorValue = this.castIdValue(operatorValue);
175
207
  }
208
+ if(sc.hasOwn(this.referenceFields, key) && sc.isArray(operatorValue)){
209
+ operatorValue = operatorValue.map(val => this.castNumericValue(val, this.referenceFields[key]));
210
+ }
211
+ if(sc.hasOwn(this.referenceFields, key) && !sc.isArray(operatorValue)){
212
+ operatorValue = this.castNumericValue(operatorValue, this.referenceFields[key]);
213
+ }
176
214
  processedFilters[key] = this.applyOperator(operatorValue, value.operator);
177
215
  continue;
178
216
  }
@@ -188,6 +226,14 @@ class PrismaDriver extends BaseDriver
188
226
  processedFilters[key] = this.castIdValue(value);
189
227
  continue;
190
228
  }
229
+ if(sc.hasOwn(this.referenceFields, key) && sc.isArray(value)){
230
+ processedFilters[key] = value.map(val => this.castNumericValue(val, this.referenceFields[key]));
231
+ continue;
232
+ }
233
+ if(sc.hasOwn(this.referenceFields, key)){
234
+ processedFilters[key] = this.castNumericValue(value, this.referenceFields[key]);
235
+ continue;
236
+ }
191
237
  processedFilters[key] = value;
192
238
  }
193
239
  return processedFilters;
@@ -590,11 +636,18 @@ class PrismaDriver extends BaseDriver
590
636
  createSingleFilter(field, fieldValue, operator = null)
591
637
  {
592
638
  let filter = {};
639
+ let processedValue = fieldValue;
640
+ if(sc.hasOwn(this.referenceFields, field)){
641
+ processedValue = this.castNumericValue(fieldValue, this.referenceFields[field]);
642
+ }
643
+ if('id' === field){
644
+ processedValue = this.castIdValue(fieldValue);
645
+ }
593
646
  if(null === operator){
594
- filter[field] = fieldValue;
647
+ filter[field] = processedValue;
595
648
  return filter;
596
649
  }
597
- filter[field] = this.applyOperator(fieldValue, operator);
650
+ filter[field] = this.applyOperator(processedValue, operator);
598
651
  return filter;
599
652
  }
600
653
 
@@ -21,34 +21,50 @@ class PrismaSchemaGenerator
21
21
  this.maxWaitTime = sc.get(props, 'maxWaitTime', 30000);
22
22
  this.prismaSchemaPath = sc.get(props, 'prismaSchemaPath', FileHandler.joinPaths(process.cwd(), 'prisma'));
23
23
  this.schemaFilePath = FileHandler.joinPaths(this.prismaSchemaPath, 'schema.prisma');
24
- this.clientOutputPath = sc.get(props, 'clientOutputPath', './prisma/client');
24
+ this.clientOutputPath = sc.get(props, 'clientOutputPath', '');
25
25
  this.generateBinaryTargets = sc.get(props, 'generateBinaryTargets', ['native']);
26
26
  }
27
27
 
28
28
  async generate()
29
29
  {
30
+ FileHandler.createFolder(this.prismaSchemaPath);
31
+ this.generateSchemaFile();
32
+ Logger.info('Running prisma introspect "npx prisma db pull"...');
30
33
  try {
31
- FileHandler.createFolder(this.prismaSchemaPath);
32
- this.generateSchemaFile();
33
- Logger.info('Running prisma introspect "npx prisma db pull"...');
34
34
  execSync('npx prisma db pull', { stdio: 'inherit' });
35
- let generateCommand = 'npx prisma generate';
36
- if(this.dataProxy){
37
- generateCommand += ' --data-proxy';
38
- }
39
- Logger.info('Running prisma generate "'+generateCommand+'"...');
35
+ } catch(error) {
36
+ Logger.critical('Failed to pull database schema: ' + error.message);
37
+ return false;
38
+ }
39
+ let generateCommand = 'npx prisma generate';
40
+ if(this.dataProxy){
41
+ generateCommand += ' --data-proxy';
42
+ }
43
+ Logger.info('Running prisma generate "'+generateCommand+'"...');
44
+ try {
40
45
  execSync(generateCommand, { stdio: 'inherit' });
41
- await this.waitForSchemaGeneration();
42
- return true;
43
46
  } catch(error) {
44
- Logger.critical('Failed to generate Prisma schema. '+error.message);
47
+ let errorString = error.toString();
48
+ if(errorString.includes('EPERM') && errorString.includes('query_engine-windows.dll.node')){
49
+ Logger.warning('Windows permission error detected with query engine file.');
50
+ Logger.warning('This is a known Prisma issue on Windows.');
51
+ Logger.warning('Please manually run: ' + generateCommand);
52
+ Logger.warning('You may need to close any processes using the Prisma client first.');
53
+ return false;
54
+ }
55
+ Logger.critical('Generate command failed: ' + error.message);
56
+ return false;
45
57
  }
46
- return false;
58
+ await this.waitForSchemaGeneration();
59
+ return true;
47
60
  }
48
61
 
49
62
  async waitForSchemaGeneration()
50
63
  {
51
- let generatedClientPath = FileHandler.joinPaths(this.prismaSchemaPath, this.clientOutputPath);
64
+ let clientPath = this.clientOutputPath || 'node_modules/.prisma/client';
65
+ let generatedClientPath = clientPath.startsWith('node_modules')
66
+ ? FileHandler.joinPaths(process.cwd(), clientPath)
67
+ : FileHandler.joinPaths(this.prismaSchemaPath, clientPath);
52
68
  let clientIndexPath = FileHandler.joinPaths(generatedClientPath, 'index.js');
53
69
  let startTime = Date.now();
54
70
  return new Promise((resolve) => {
@@ -123,7 +139,10 @@ class PrismaSchemaGenerator
123
139
  {
124
140
  let generatorContent = 'generator client {\n';
125
141
  generatorContent += ' provider = "prisma-client-js"\n';
126
- generatorContent += ' output = "' + this.clientOutputPath + '"\n';
142
+ if(this.clientOutputPath){
143
+ let normalizedPath = this.clientOutputPath.replace(/\\/g, '/');
144
+ generatorContent += ' output = "' + normalizedPath + '"\n';
145
+ }
127
146
  if(0 < this.generateBinaryTargets.length){
128
147
  generatorContent += ' binaryTargets = [' +
129
148
  this.generateBinaryTargets.map(target => '"' + target + '"').join(', ')
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@reldens/storage",
3
3
  "scope": "@reldens",
4
- "version": "0.52.0",
4
+ "version": "0.54.0",
5
5
  "description": "Reldens - Storage",
6
6
  "author": "Damian A. Pastorini",
7
7
  "license": "MIT",