@reldens/storage 0.117.0 → 0.119.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/.claude/test-architecture.md +27 -17
  2. package/CLAUDE.md +67 -11
  3. package/README.md +204 -22
  4. package/bin/reldens-storage.js +43 -10
  5. package/index.js +53 -12
  6. package/lib/drizzle/drizzle-data-server.js +123 -0
  7. package/lib/drizzle/drizzle-driver.js +228 -0
  8. package/lib/drizzle/drizzle-models-generation.js +49 -0
  9. package/lib/drizzle/drizzle-modules-loader.js +32 -0
  10. package/lib/drizzle/drizzle-modules-validator.js +74 -0
  11. package/lib/entities-generator.js +36 -7
  12. package/lib/entity-templates/drizzle-model.template +28 -0
  13. package/lib/entity-templates/query-builder-model.template +17 -0
  14. package/lib/generators/base-generator.js +13 -0
  15. package/lib/generators/entities-generation.js +0 -13
  16. package/lib/generators/models-generation.js +179 -680
  17. package/lib/generators/relations-detection.js +171 -0
  18. package/lib/knex/knex-data-server.js +128 -0
  19. package/lib/knex/knex-driver.js +157 -0
  20. package/lib/knex/knex-modules-loader.js +28 -0
  21. package/lib/knex/knex-modules-validator.js +45 -0
  22. package/lib/kysely/kysely-data-server.js +128 -0
  23. package/lib/kysely/kysely-driver.js +176 -0
  24. package/lib/kysely/kysely-modules-loader.js +28 -0
  25. package/lib/kysely/kysely-modules-validator.js +53 -0
  26. package/lib/mikro-orm/mikro-orm-data-server.js +48 -29
  27. package/lib/mikro-orm/mikro-orm-driver.js +317 -108
  28. package/lib/mikro-orm/mikro-orm-models-generation.js +178 -0
  29. package/lib/mikro-orm/mikro-orm-modules-loader.js +50 -0
  30. package/lib/mikro-orm/mikro-orm-modules-validator.js +46 -0
  31. package/lib/mysql2-connection-config.js +38 -0
  32. package/lib/objection-js/objection-js-data-server.js +71 -125
  33. package/lib/objection-js/objection-js-driver.js +4 -1
  34. package/lib/objection-js/objection-js-models-generation.js +98 -0
  35. package/lib/objection-js/objection-modules-loader.js +28 -0
  36. package/lib/objection-js/objection-modules-validator.js +31 -0
  37. package/lib/package-resolver.js +45 -0
  38. package/lib/prisma/prisma-client-loader.js +11 -1
  39. package/lib/prisma/prisma-driver.js +16 -2
  40. package/lib/prisma/prisma-filter-processor.js +5 -6
  41. package/lib/prisma/prisma-models-generation.js +236 -0
  42. package/lib/prisma/prisma-schema-generator.js +5 -1
  43. package/lib/prisma/prisma-type-caster.js +14 -0
  44. package/lib/query-builder-driver.js +210 -0
  45. package/lib/query-builder-models-generation.js +80 -0
  46. package/lib/relations-loader.js +192 -0
  47. package/lib/type-mapper.js +38 -0
  48. package/package.json +8 -8
  49. package/tests/.env.test.example +6 -0
  50. package/tests/fixtures/categories-fixtures.js +8 -0
  51. package/tests/fixtures/expected-entities/entities/test-product-details-entity.js +10 -1
  52. package/tests/fixtures/expected-entities/entities-translations.js +2 -0
  53. package/tests/fixtures/expected-entities-mikro-orm/models/mikro-orm/test-categories-model.js +13 -8
  54. package/tests/fixtures/expected-entities-mikro-orm/models/mikro-orm/test-product-details-model.js +30 -10
  55. package/tests/fixtures/expected-entities-mikro-orm/models/mikro-orm/test-products-model.js +15 -14
  56. package/tests/fixtures/expected-entities-mikro-orm/models/mikro-orm/test-reviews-model.js +13 -12
  57. package/tests/fixtures/expected-entities-prisma/models/prisma/test-product-details-model.js +3 -1
  58. package/tests/fixtures/product-details-fixtures.js +4 -1
  59. package/tests/fixtures/reviews-fixtures.js +2 -1
  60. package/tests/fixtures/sql/test-schema.sql +11 -2
  61. package/tests/fixtures/table-columns-fixtures.js +97 -0
  62. package/tests/integration/test-cross-driver-equivalence.js +286 -0
  63. package/tests/integration/test-nested-filters.js +265 -408
  64. package/tests/integration/test-relations.js +135 -0
  65. package/tests/integration/test-reldens-sample-data.js +255 -0
  66. package/tests/integration/test-reldens-shape-relations.js +292 -0
  67. package/tests/run-tests.js +111 -65
  68. package/tests/unit/test-drivers.js +7 -1
  69. package/tests/unit/test-models-generation.js +28 -18
  70. package/tests/utils/column-value-assert.js +139 -0
  71. package/tests/utils/entity-projection.js +111 -0
  72. package/tests/utils/observed-value.js +41 -0
  73. package/tests/utils/projection-difference.js +151 -0
  74. package/tests/utils/relations-shape-support.js +104 -0
  75. package/tests/utils/reldens-schema-loader.js +274 -0
  76. package/tests/utils/test-helpers.js +143 -13
  77. package/tests/utils/test-runner.js +26 -5
  78. package/tests/utils/whole-result-assert.js +243 -0
@@ -0,0 +1,176 @@
1
+ /**
2
+ *
3
+ * Reldens - KyselyDriver
4
+ *
5
+ */
6
+
7
+ const { QueryBuilderDriver } = require('../query-builder-driver');
8
+ const { sc } = require('@reldens/utils');
9
+
10
+ class KyselyDriver extends QueryBuilderDriver
11
+ {
12
+
13
+ constructor(props)
14
+ {
15
+ super(props);
16
+ this.db = sc.get(props, 'db', false);
17
+ this.sql = sc.get(props, 'sql', false);
18
+ }
19
+
20
+ async insertRow(data)
21
+ {
22
+ let result = await this.db.insertInto(this.tableName()).values(data).executeTakeFirst();
23
+ return Number(sc.get(result, 'insertId', 0) || 0);
24
+ }
25
+
26
+ selectRows(filters, options)
27
+ {
28
+ let query = this.db.selectFrom(this.tableName());
29
+ query = 0 < options.select.length ? query.select(options.select) : query.selectAll();
30
+ query = this.appendFilters(query, filters);
31
+ if(0 < options.limit){
32
+ query = query.limit(options.limit);
33
+ }
34
+ if(0 < options.offset){
35
+ query = query.offset(options.offset);
36
+ }
37
+ if(options.sortBy && options.sortDirection){
38
+ query = query.orderBy(options.sortBy, options.sortDirection.toLowerCase());
39
+ }
40
+ return query.execute();
41
+ }
42
+
43
+ async update(filters, updatePatch)
44
+ {
45
+ let query = this.appendFilters(this.db.updateTable(this.tableName()).set(updatePatch), filters);
46
+ let result = await query.executeTakeFirst();
47
+ return Number(sc.get(result, 'numUpdatedRows', 0));
48
+ }
49
+
50
+ async delete(filters = {})
51
+ {
52
+ let result = await this.appendFilters(this.db.deleteFrom(this.tableName()), filters).executeTakeFirst();
53
+ return Number(sc.get(result, 'numDeletedRows', 0));
54
+ }
55
+
56
+ async count(filters = {})
57
+ {
58
+ let query = this.db.selectFrom(this.tableName()).select(builder => builder.fn.countAll().as('count'));
59
+ let result = await this.appendFilters(query, filters).executeTakeFirst();
60
+ if(!result){
61
+ return 0;
62
+ }
63
+ return Number(result.count);
64
+ }
65
+
66
+ appendFilters(query, filters = {})
67
+ {
68
+ if(0 === Object.keys(filters).length){
69
+ return query;
70
+ }
71
+ return query.where(builder => this.filtersExpression(builder, filters) || builder.lit(true));
72
+ }
73
+
74
+ filtersExpression(builder, filters)
75
+ {
76
+ let expressions = [];
77
+ for(let key of Object.keys(filters)){
78
+ expressions = this.appendExpression(builder, expressions, key, filters[key]);
79
+ }
80
+ if(0 === expressions.length){
81
+ return null;
82
+ }
83
+ return builder.and(expressions);
84
+ }
85
+
86
+ appendExpression(builder, expressions, key, filter)
87
+ {
88
+ if(('AND' === key || 'OR' === key) && sc.isArray(filter)){
89
+ return this.appendGroupExpression(builder, expressions, key, filter);
90
+ }
91
+ let operator = sc.get(filter, 'operator', '').toUpperCase();
92
+ if('OR' === operator){
93
+ return this.appendOrValueExpression(builder, expressions, key, filter);
94
+ }
95
+ if(operator){
96
+ expressions.push(this.operatorExpression(builder, key, operator, filter));
97
+ return expressions;
98
+ }
99
+ if(sc.hasOwn(this.relationsLoader.relationMappings, key) && sc.isObject(filter)){
100
+ let mapping = this.relationsLoader.relationMappings[key];
101
+ expressions.push(builder(mapping.from, 'in', this.relationSubQuery(builder, mapping, filter)));
102
+ return expressions;
103
+ }
104
+ if(null === filter){
105
+ expressions.push(builder(key, 'is', null));
106
+ return expressions;
107
+ }
108
+ expressions.push(builder(key, '=', filter));
109
+ return expressions;
110
+ }
111
+
112
+ appendGroupExpression(builder, expressions, key, conditions)
113
+ {
114
+ let nested = [];
115
+ for(let condition of conditions){
116
+ let expression = this.filtersExpression(builder, condition);
117
+ if(expression){
118
+ nested.push(expression);
119
+ }
120
+ }
121
+ if(0 === nested.length){
122
+ return expressions;
123
+ }
124
+ expressions.push('AND' === key ? builder.and(nested) : builder.or(nested));
125
+ return expressions;
126
+ }
127
+
128
+ appendOrValueExpression(builder, expressions, key, filter)
129
+ {
130
+ let orExpression = builder(key, '=', filter.value);
131
+ if(0 === expressions.length){
132
+ return [orExpression];
133
+ }
134
+ return [builder.or([builder.and(expressions), orExpression])];
135
+ }
136
+
137
+ operatorExpression(builder, field, operator, filter)
138
+ {
139
+ if('IN' === operator){
140
+ return builder(field, 'in', filter.value);
141
+ }
142
+ if('NOT' === operator){
143
+ return builder.not(builder(field, '=', filter.value));
144
+ }
145
+ if('LIKE' === operator){
146
+ return this.likeExpression(builder, field, filter.value);
147
+ }
148
+ return builder(field, sc.get(this.operatorsMap, operator, filter.operator), filter.value);
149
+ }
150
+
151
+ likeExpression(builder, field, value)
152
+ {
153
+ let likeValue = '%'+String(value).replace(/%/g, '')+'%';
154
+ if(this.isJsonField(field)){
155
+ return builder(builder.cast(field, 'char'), 'like', likeValue);
156
+ }
157
+ return builder(field, 'like', likeValue);
158
+ }
159
+
160
+ relationSubQuery(builder, mapping, nestedFilters)
161
+ {
162
+ let subQuery = builder.selectFrom(mapping.tableName).select(mapping.to);
163
+ let relatedDriver = this.relationsLoader.relatedDriver(mapping);
164
+ if(!relatedDriver){
165
+ return subQuery;
166
+ }
167
+ let expression = relatedDriver.filtersExpression(builder, nestedFilters);
168
+ if(!expression){
169
+ return subQuery;
170
+ }
171
+ return subQuery.where(expression);
172
+ }
173
+
174
+ }
175
+
176
+ module.exports.KyselyDriver = KyselyDriver;
@@ -0,0 +1,28 @@
1
+ /**
2
+ *
3
+ * Reldens - Storage - KyselyModulesLoader
4
+ *
5
+ */
6
+
7
+ const { KyselyModulesValidator } = require('./kysely-modules-validator');
8
+ const { PackageResolver } = require('../package-resolver');
9
+
10
+ class KyselyModulesLoader
11
+ {
12
+
13
+ static load(projectPath)
14
+ {
15
+ let kysely = PackageResolver.loadPackage('kysely', projectPath);
16
+ if(!kysely){
17
+ return false;
18
+ }
19
+ let kyselyModules = {Kysely: kysely.Kysely, MysqlDialect: kysely.MysqlDialect, sql: kysely.sql};
20
+ if(!KyselyModulesValidator.validate(kyselyModules)){
21
+ return false;
22
+ }
23
+ return kyselyModules;
24
+ }
25
+
26
+ }
27
+
28
+ module.exports.KyselyModulesLoader = KyselyModulesLoader;
@@ -0,0 +1,53 @@
1
+ /**
2
+ *
3
+ * Reldens - KyselyModulesValidator
4
+ *
5
+ */
6
+
7
+ const { Logger, sc } = require('@reldens/utils');
8
+
9
+ class KyselyModulesValidator
10
+ {
11
+
12
+ static validate(kyselyModules)
13
+ {
14
+ if(!sc.isObject(kyselyModules)){
15
+ Logger.critical('Missing "kyselyModules" object.');
16
+ return false;
17
+ }
18
+ if(!sc.isFunction(kyselyModules.sql)){
19
+ Logger.critical('Invalid "kyselyModules.sql".');
20
+ return false;
21
+ }
22
+ if(!sc.isFunction(kyselyModules.sql.raw)){
23
+ Logger.critical('Invalid "kyselyModules.sql", "raw" not found.');
24
+ return false;
25
+ }
26
+ if(kyselyModules.db){
27
+ return KyselyModulesValidator.validateDbInstance(kyselyModules.db);
28
+ }
29
+ if(!sc.isFunction(kyselyModules.Kysely)){
30
+ Logger.critical('Invalid "kyselyModules.Kysely".');
31
+ return false;
32
+ }
33
+ if(!sc.isFunction(kyselyModules.MysqlDialect)){
34
+ Logger.critical('Invalid "kyselyModules.MysqlDialect".');
35
+ return false;
36
+ }
37
+ return true;
38
+ }
39
+
40
+ static validateDbInstance(db)
41
+ {
42
+ let requiredMethods = ['selectFrom', 'insertInto', 'updateTable', 'deleteFrom', 'destroy'];
43
+ let missingMethods = requiredMethods.filter(method => !sc.isFunction(db[method]));
44
+ if(0 < missingMethods.length){
45
+ Logger.critical('Invalid Kysely instance, missing: '+missingMethods.join(', '));
46
+ return false;
47
+ }
48
+ return true;
49
+ }
50
+
51
+ }
52
+
53
+ module.exports.KyselyModulesValidator = KyselyModulesValidator;
@@ -7,46 +7,49 @@
7
7
  const { MikroOrmDriver } = require('./mikro-orm-driver');
8
8
  const { BaseDataServer } = require('../base-data-server');
9
9
  const { MySQLTablesProvider } = require('../mysql-tables-provider');
10
- const { MikroORM } = require('@mikro-orm/core');
11
- const { MySqlDriver } = require('@mikro-orm/mysql');
12
- const { MongoDriver } = require('@mikro-orm/mongodb');
10
+ const { MikroOrmModulesValidator } = require('./mikro-orm-modules-validator');
11
+ const { MikroOrmModulesLoader } = require('./mikro-orm-modules-loader');
13
12
  const { Logger, sc } = require('@reldens/utils');
14
13
 
15
14
  class MikroOrmDataServer extends BaseDataServer
16
15
  {
17
16
 
18
17
  orm = {};
19
- clientsMapped = {
20
- mysql: MySqlDriver,
21
- mongodb: MongoDriver
22
- };
18
+ mysqlClients = ['mysql', 'mysql2'];
23
19
 
24
20
  constructor(props)
25
21
  {
26
22
  super(props);
27
23
  this.entitiesPath = props.entitiesPath;
28
24
  this.warnWhenNoEntities = Boolean(props.warnWhenNoEntities);
25
+ this.mikroOrmModules = sc.get(props, 'mikroOrmModules', false);
26
+ }
27
+
28
+ clientDriver()
29
+ {
30
+ if('mongodb' === this.client){
31
+ return this.mikroOrmModules.MongoDriver;
32
+ }
33
+ return this.mikroOrmModules.MySqlDriver;
29
34
  }
30
35
 
31
36
  getEntitiesToRegister()
32
37
  {
33
38
  if(this.entitiesPath){
34
- //Logger.debug('MikroORM using entitiesPath');
35
39
  return this.entitiesPath;
36
40
  }
37
41
  if(0 < Object.keys(this.entities).length){
38
- //Logger.debug('MikroORM using this.entities: '+Object.keys(this.entities).join(', '));
39
42
  return this.entities;
40
43
  }
41
44
  if(!this.rawEntities){
42
- //Logger.debug('MikroORM has no entities!');
43
45
  return [];
44
46
  }
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
- });
47
+ let entities = [];
48
+ for(let i of Object.keys(this.rawEntities)){
49
+ let rawEntity = this.rawEntities[i];
50
+ entities.push((rawEntity && rawEntity.schema) ? rawEntity.schema : rawEntity);
51
+ }
52
+ return entities;
50
53
  }
51
54
 
52
55
  async connect()
@@ -54,16 +57,29 @@ class MikroOrmDataServer extends BaseDataServer
54
57
  if(this.initialized){
55
58
  return this.initialized;
56
59
  }
60
+ if(!this.client){
61
+ this.client = 'mongodb';
62
+ }
63
+ if(!this.mikroOrmModules){
64
+ this.mikroOrmModules = MikroOrmModulesLoader.load(this.projectRoot, this.client);
65
+ }
66
+ if(!MikroOrmModulesValidator.validate(this.mikroOrmModules, this.client)){
67
+ return false;
68
+ }
57
69
  let providedSetup = {
58
- driver: this.clientsMapped[(this.client || 'mongodb')],
70
+ driver: this.clientDriver(),
59
71
  dbName: this.config.database,
60
72
  clientUrl: this.connectString,
61
73
  allowGlobalContext: true,
62
- multipleStatements: this.multipleStatements
74
+ multipleStatements: this.multipleStatements,
75
+ namingStrategy: this.mikroOrmModules.EntityCaseNamingStrategy
63
76
  };
77
+ if(0 < Object.keys(this.poolConfig).length){
78
+ providedSetup.pool = this.poolConfig;
79
+ }
64
80
  providedSetup.entities = this.getEntitiesToRegister();
65
81
  providedSetup.discovery = {warnWhenNoEntities: this.warnWhenNoEntities};
66
- this.orm = await MikroORM.init(providedSetup);
82
+ this.orm = await this.mikroOrmModules.MikroORM.init(providedSetup);
67
83
  this.initialized = Date.now();
68
84
  return this.initialized;
69
85
  }
@@ -97,7 +113,8 @@ class MikroOrmDataServer extends BaseDataServer
97
113
  name: i,
98
114
  config: { dbName: this.orm.config.get('dbName') },
99
115
  orm: this.orm,
100
- server: this
116
+ server: this,
117
+ mikroOrmModules: this.mikroOrmModules
101
118
  });
102
119
  }
103
120
  this.entityManager.setEntities(this.entities);
@@ -137,19 +154,11 @@ class MikroOrmDataServer extends BaseDataServer
137
154
  return false;
138
155
  }
139
156
  try {
140
- if('mysql' === this.client){
157
+ if(-1 !== this.mysqlClients.indexOf(this.client)){
141
158
  return await MySQLTablesProvider.fetchTables(this);
142
159
  }
143
160
  if('mongodb' === this.client){
144
- let tables = {};
145
- let collections = await this.orm.em.getDriver().getConnection().db().listCollections().toArray();
146
- for(let collection of collections){
147
- tables[collection.name] = {
148
- name: collection.name,
149
- columns: {}
150
- };
151
- }
152
- return tables;
161
+ return await this.fetchMongoCollections();
153
162
  }
154
163
  Logger.critical('Unsupported client type: '+this.client);
155
164
  } catch(error) {
@@ -158,6 +167,16 @@ class MikroOrmDataServer extends BaseDataServer
158
167
  return false;
159
168
  }
160
169
 
170
+ async fetchMongoCollections()
171
+ {
172
+ let tables = {};
173
+ let collections = await this.orm.em.getDriver().getConnection().db().listCollections().toArray();
174
+ for(let collection of collections){
175
+ tables[collection.name] = {name: collection.name, columns: {}};
176
+ }
177
+ return tables;
178
+ }
179
+
161
180
  }
162
181
 
163
182
  module.exports.MikroOrmDataServer = MikroOrmDataServer;