@carno.js/orm 1.0.6 → 1.0.8

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.
@@ -21,16 +21,7 @@ class EntityKeyGenerator {
21
21
  }
22
22
  getPrimaryKeyName(entityClass) {
23
23
  const options = this.entityStorage.get(entityClass);
24
- if (!options) {
25
- return 'id';
26
- }
27
- for (const prop in options.properties) {
28
- const property = options.properties[prop];
29
- if (property.options.isPrimary) {
30
- return prop;
31
- }
32
- }
33
- return 'id';
24
+ return options?._primaryKeyPropertyName || 'id';
34
25
  }
35
26
  serializePrimaryKey(pk) {
36
27
  if (Array.isArray(pk)) {
package/dist/index.d.ts CHANGED
@@ -16,6 +16,7 @@ export { EntityStorage } from './domain/entities';
16
16
  export * from './driver/bun-pg.driver';
17
17
  export * from './driver/bun-mysql.driver';
18
18
  export * from './driver/bun-driver.base';
19
+ export * from './driver/driver-factory';
19
20
  export * from './utils';
20
21
  export * from './driver/driver.interface';
21
22
  export * from './entry';
package/dist/index.js CHANGED
@@ -33,6 +33,7 @@ Object.defineProperty(exports, "EntityStorage", { enumerable: true, get: functio
33
33
  __exportStar(require("./driver/bun-pg.driver"), exports);
34
34
  __exportStar(require("./driver/bun-mysql.driver"), exports);
35
35
  __exportStar(require("./driver/bun-driver.base"), exports);
36
+ __exportStar(require("./driver/driver-factory"), exports);
36
37
  __exportStar(require("./utils"), exports);
37
38
  __exportStar(require("./driver/driver.interface"), exports);
38
39
  __exportStar(require("./entry"), exports);
@@ -18,6 +18,7 @@ export declare class ModelTransformer {
18
18
  private populateProperties;
19
19
  private parseColumnKey;
20
20
  private setPropertyValue;
21
+ private normalizePropertyValue;
21
22
  private findPropertyByColumnName;
22
23
  private isValueObjectType;
23
24
  private linkJoinedEntities;
@@ -100,12 +100,8 @@ class ModelTransformer {
100
100
  return data[pkKey];
101
101
  }
102
102
  findPrimaryKeyProperty(options) {
103
- for (const prop in options.properties) {
104
- if (options.properties[prop].options.isPrimary) {
105
- return options.properties[prop];
106
- }
107
- }
108
- return null;
103
+ const pkPropertyName = options._primaryKeyPropertyName || 'id';
104
+ return options.properties[pkPropertyName] || null;
109
105
  }
110
106
  buildOptionsMap(instanceMap) {
111
107
  const optionsMap = new Map();
@@ -148,7 +144,25 @@ class ModelTransformer {
148
144
  entity[key] = new property.type(value);
149
145
  return;
150
146
  }
151
- entity[key] = value;
147
+ entity[key] = this.normalizePropertyValue(property, value);
148
+ }
149
+ // @todo refactor for performance
150
+ normalizePropertyValue(property, value) {
151
+ if (value === null || value === undefined) {
152
+ return value;
153
+ }
154
+ if (property?.type === Boolean) {
155
+ if (value === 1 || value === '1' || value === true || value === 'true') {
156
+ return true;
157
+ }
158
+ if (value === 0 || value === '0' || value === false || value === 'false') {
159
+ return false;
160
+ }
161
+ }
162
+ if (property?.type === Date && !(value instanceof Date)) {
163
+ return new Date(value);
164
+ }
165
+ return value;
152
166
  }
153
167
  findPropertyByColumnName(columnName, options) {
154
168
  // First, try to find in regular properties
@@ -1,10 +1,12 @@
1
- import { Statement } from '../driver/driver.interface';
1
+ import { DriverInterface, Statement } from '../driver/driver.interface';
2
2
  import { EntityStorage, Options } from '../domain/entities';
3
3
  export declare class SqlColumnManager {
4
4
  private entityStorage;
5
5
  private statements;
6
6
  private entity;
7
- constructor(entityStorage: EntityStorage, statements: Statement<any>, entity: Options);
7
+ private driver;
8
+ constructor(entityStorage: EntityStorage, statements: Statement<any>, entity: Options, driver: DriverInterface);
9
+ private quoteId;
8
10
  generateColumns(model: Function, updatedColumns: string[]): string[];
9
11
  processUserColumns(columns: string[]): string[];
10
12
  getColumnsForEntity(entity: Function, alias: string): string[];
@@ -2,10 +2,15 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SqlColumnManager = void 0;
4
4
  class SqlColumnManager {
5
- constructor(entityStorage, statements, entity) {
5
+ constructor(entityStorage, statements, entity, driver) {
6
6
  this.entityStorage = entityStorage;
7
7
  this.statements = statements;
8
8
  this.entity = entity;
9
+ this.driver = driver;
10
+ }
11
+ quoteId(identifier) {
12
+ const q = this.driver.getIdentifierQuote();
13
+ return `${q}${identifier}${q}`;
9
14
  }
10
15
  generateColumns(model, updatedColumns) {
11
16
  const baseColumns = this.getColumnsForEntity(model, this.statements.alias);
@@ -41,7 +46,9 @@ class SqlColumnManager {
41
46
  getPropertyColumns(entityOptions, alias) {
42
47
  return Object.keys(entityOptions.properties).map(key => {
43
48
  const columnName = entityOptions.properties[key].options.columnName;
44
- return `${alias}."${columnName}" as "${alias}_${columnName}"`;
49
+ const col = this.quoteId(columnName);
50
+ const aliasedCol = this.quoteId(`${alias}_${columnName}`);
51
+ return `${alias}.${col} as ${aliasedCol}`;
45
52
  });
46
53
  }
47
54
  getRelationColumns(entityOptions, alias) {
@@ -50,7 +57,11 @@ class SqlColumnManager {
50
57
  }
51
58
  return entityOptions.relations
52
59
  .filter(relation => relation.relation === 'many-to-one')
53
- .map(relation => `${alias}."${relation.columnName}" as "${alias}_${relation.columnName}"`);
60
+ .map(relation => {
61
+ const col = this.quoteId(relation.columnName);
62
+ const aliasedCol = this.quoteId(`${alias}_${relation.columnName}`);
63
+ return `${alias}.${col} as ${aliasedCol}`;
64
+ });
54
65
  }
55
66
  extractAliases(columns) {
56
67
  return columns
@@ -66,10 +77,11 @@ class SqlColumnManager {
66
77
  }
67
78
  buildSimpleColumnAlias(column, alias, onlyAlias) {
68
79
  const columnName = this.getColumnNameFromProperty(column);
80
+ const col = this.quoteId(columnName);
69
81
  if (onlyAlias) {
70
- return `${alias}."${columnName}"`;
82
+ return `${alias}.${col}`;
71
83
  }
72
- return `${alias}."${columnName}" as ${alias}_${columnName}`;
84
+ return `${alias}.${col} as ${alias}_${columnName}`;
73
85
  }
74
86
  buildNestedColumnAlias(column, onlyAlias) {
75
87
  this.validateJoinsExist();
@@ -122,10 +134,11 @@ class SqlColumnManager {
122
134
  formatColumnWithAlias(alias, propertyName, onlyAlias) {
123
135
  const entity = this.getEntityFromAlias(alias);
124
136
  const columnName = this.getColumnNameFromPropertyForEntity(propertyName, entity);
137
+ const col = this.quoteId(columnName);
125
138
  if (onlyAlias) {
126
- return `${alias}."${columnName}"`;
139
+ return `${alias}.${col}`;
127
140
  }
128
- return `${alias}."${columnName}" as ${alias}_${columnName}`;
141
+ return `${alias}.${col} as ${alias}_${columnName}`;
129
142
  }
130
143
  getColumnNameFromProperty(propertyName) {
131
144
  return this.getColumnNameFromPropertyForEntity(propertyName, this.entity);
@@ -1,4 +1,4 @@
1
- import { FilterQuery, Relationship, Statement } from '../driver/driver.interface';
1
+ import { DriverInterface, FilterQuery, Relationship, Statement } from '../driver/driver.interface';
2
2
  import { EntityStorage } from '../domain/entities';
3
3
  import { SqlSubqueryBuilder } from './sql-subquery-builder';
4
4
  type ApplyJoinCallback = (relationship: Relationship<any>, value: FilterQuery<any>, alias: string) => string;
@@ -6,9 +6,10 @@ export declare class SqlConditionBuilder<T> {
6
6
  private entityStorage;
7
7
  private applyJoinCallback;
8
8
  private statements;
9
+ private driver;
9
10
  private lastKeyNotOperator;
10
11
  private subqueryBuilder?;
11
- constructor(entityStorage: EntityStorage, applyJoinCallback: ApplyJoinCallback, statements: Statement<T>);
12
+ constructor(entityStorage: EntityStorage, applyJoinCallback: ApplyJoinCallback, statements: Statement<T>, driver: DriverInterface);
12
13
  setSubqueryBuilder(subqueryBuilder: SqlSubqueryBuilder): void;
13
14
  build(condition: FilterQuery<T>, alias: string, model: Function): string;
14
15
  private processConditions;
@@ -30,6 +31,7 @@ export declare class SqlConditionBuilder<T> {
30
31
  private extractValueFromValueObject;
31
32
  private formatValue;
32
33
  private formatDate;
34
+ private formatDateForMysql;
33
35
  private isNullish;
34
36
  private buildNullCondition;
35
37
  private isPrimitive;
@@ -12,10 +12,11 @@ const OPERATORS_SET = new Set([
12
12
  const LOGICAL_OPERATORS_SET = new Set(['$or', '$and']);
13
13
  const PRIMITIVES_SET = new Set(['string', 'number', 'boolean', 'bigint']);
14
14
  class SqlConditionBuilder {
15
- constructor(entityStorage, applyJoinCallback, statements) {
15
+ constructor(entityStorage, applyJoinCallback, statements, driver) {
16
16
  this.entityStorage = entityStorage;
17
17
  this.applyJoinCallback = applyJoinCallback;
18
18
  this.statements = statements;
19
+ this.driver = driver;
19
20
  this.lastKeyNotOperator = '';
20
21
  }
21
22
  setSubqueryBuilder(subqueryBuilder) {
@@ -178,7 +179,17 @@ class SqlConditionBuilder {
178
179
  return this.formatJson(value);
179
180
  }
180
181
  formatDate(value) {
181
- return `'${value.toISOString()}'`;
182
+ const formatted = this.driver.dbType === 'mysql'
183
+ ? this.formatDateForMysql(value)
184
+ : value.toISOString();
185
+ return `'${formatted}'`;
186
+ }
187
+ formatDateForMysql(value) {
188
+ return value
189
+ .toISOString()
190
+ .replace('T', ' ')
191
+ .replace('Z', '')
192
+ .replace(/\.\d{3}/, '');
182
193
  }
183
194
  isNullish(value) {
184
195
  return value === null || value === undefined;
@@ -17,14 +17,18 @@ export declare class SqlJoinManager<T> {
17
17
  private getOriginalColumnsCallback;
18
18
  private getAliasCallback;
19
19
  constructor(entityStorage: EntityStorage, statements: Statement<T>, entity: Options, model: new () => T, driver: DriverInterface, logger: Logger, conditionBuilder: SqlConditionBuilder<T>, columnManager: SqlColumnManager, modelTransformer: ModelTransformer, getOriginalColumnsCallback: () => string[], getAliasCallback: (tableName: string) => string);
20
+ private quoteId;
21
+ private qualifyTable;
20
22
  addJoinForRelationshipPath(relationshipPath: string): void;
21
23
  applyJoin(relationShip: Relationship<any>, value: FilterQuery<any>, alias: string): string;
22
24
  handleSelectJoin(entities: any, models: any): Promise<void>;
25
+ handleSelectJoinBatch(entities: any[], models: any[]): Promise<void>;
23
26
  getPathForSelectJoin(selectJoin: Statement<any>): string[] | null;
24
27
  private buildJoinOn;
25
28
  private addJoinedJoin;
26
29
  private addSelectJoin;
27
30
  private processSelectJoin;
31
+ private processSelectJoinBatch;
28
32
  private getIds;
29
33
  private updateJoinWhere;
30
34
  private updateJoinColumns;
@@ -15,6 +15,16 @@ class SqlJoinManager {
15
15
  this.getOriginalColumnsCallback = getOriginalColumnsCallback;
16
16
  this.getAliasCallback = getAliasCallback;
17
17
  }
18
+ quoteId(identifier) {
19
+ const q = this.driver.getIdentifierQuote();
20
+ return `${q}${identifier}${q}`;
21
+ }
22
+ qualifyTable(schema, tableName) {
23
+ if (this.driver.dbType === 'mysql') {
24
+ return this.quoteId(tableName);
25
+ }
26
+ return `${this.quoteId(schema)}.${this.quoteId(tableName)}`;
27
+ }
18
28
  addJoinForRelationshipPath(relationshipPath) {
19
29
  const relationshipNames = relationshipPath.split('.');
20
30
  let currentEntity = this.entity;
@@ -69,18 +79,29 @@ class SqlJoinManager {
69
79
  }
70
80
  return models;
71
81
  }
82
+ async handleSelectJoinBatch(entities, models) {
83
+ if (!this.statements.selectJoin || this.statements.selectJoin.length === 0) {
84
+ return;
85
+ }
86
+ for (const join of this.statements.selectJoin.reverse()) {
87
+ await this.processSelectJoinBatch(join, entities, models);
88
+ }
89
+ }
72
90
  getPathForSelectJoin(selectJoin) {
73
91
  const path = this.getPathForSelectJoinRecursive(selectJoin);
74
92
  return path.reverse();
75
93
  }
76
94
  buildJoinOn(relationShip, alias, joinAlias, originPrimaryKey) {
77
95
  let on = '';
96
+ const fkKey = this.quoteId(this.getFkKey(relationShip));
97
+ const pk = this.quoteId(originPrimaryKey);
78
98
  switch (relationShip.relation) {
79
99
  case "one-to-many":
80
- on = `${joinAlias}."${this.getFkKey(relationShip)}" = ${alias}."${originPrimaryKey}"`;
100
+ on = `${joinAlias}.${fkKey} = ${alias}.${pk}`;
81
101
  break;
82
102
  case "many-to-one":
83
- on = `${alias}."${relationShip.columnName}" = ${joinAlias}."${this.getFkKey(relationShip)}"`;
103
+ const col = this.quoteId(relationShip.columnName);
104
+ on = `${alias}.${col} = ${joinAlias}.${fkKey}`;
84
105
  break;
85
106
  }
86
107
  return on;
@@ -109,7 +130,7 @@ class SqlJoinManager {
109
130
  this.statements.selectJoin.push({
110
131
  statement: 'select',
111
132
  columns: this.getOriginalColumnsCallback().filter(column => column.startsWith(`${relationShip.propertyKey}`)).map(column => column.split('.')[1]) || [],
112
- table: `"${joinSchema || 'public'}"."${joinTableName}"`,
133
+ table: this.qualifyTable(joinSchema || 'public', joinTableName),
113
134
  alias: joinAlias,
114
135
  where: joinWhere,
115
136
  joinProperty: relationShip.propertyKey,
@@ -133,6 +154,35 @@ class SqlJoinManager {
133
154
  this.logger.debug(`SQL: ${child.sql} [${Date.now() - child.startTime}ms]`);
134
155
  this.attachJoinResults(join, child, models);
135
156
  }
157
+ async processSelectJoinBatch(join, entities, models) {
158
+ const allIds = new Set();
159
+ for (let i = 0; i < entities.length; i++) {
160
+ const ids = this.getIds(join, entities[i], models[i]);
161
+ if (Array.isArray(ids)) {
162
+ ids.forEach(id => {
163
+ if (id !== undefined && id !== null) {
164
+ allIds.add(id);
165
+ }
166
+ });
167
+ }
168
+ else if (ids !== undefined && ids !== null) {
169
+ allIds.add(ids);
170
+ }
171
+ }
172
+ if (allIds.size === 0) {
173
+ return;
174
+ }
175
+ const idsString = Array.from(allIds)
176
+ .map((id) => this.formatValue(id))
177
+ .join(', ');
178
+ this.updateJoinWhere(join, idsString);
179
+ this.updateJoinColumns(join);
180
+ const result = await this.driver.executeStatement(join);
181
+ this.logger.debug(`SQL (BATCHED): ${result.sql} [${Date.now() - result.startTime}ms]`);
182
+ for (let i = 0; i < entities.length; i++) {
183
+ this.attachJoinResults(join, result, models[i]);
184
+ }
185
+ }
136
186
  getIds(join, entities, models) {
137
187
  let ids = entities[`${join.originAlias}_${join.primaryKey}`];
138
188
  if (typeof ids === 'undefined') {
@@ -145,16 +195,21 @@ class SqlJoinManager {
145
195
  return ids;
146
196
  }
147
197
  updateJoinWhere(join, ids) {
198
+ const fkCol = this.quoteId(join.fkKey);
148
199
  if (join.where) {
149
- join.where = `${join.where} AND ${join.alias}."${join.fkKey}" IN (${ids})`;
200
+ join.where = `${join.where} AND ${join.alias}.${fkCol} IN (${ids})`;
150
201
  }
151
202
  else {
152
- join.where = `${join.alias}."${join.fkKey}" IN (${ids})`;
203
+ join.where = `${join.alias}.${fkCol} IN (${ids})`;
153
204
  }
154
205
  }
155
206
  updateJoinColumns(join) {
156
207
  if (join.columns && join.columns.length > 0) {
157
- join.columns = join.columns.map((column) => `${join.alias}."${column}" as "${join.alias}_${column}"`);
208
+ join.columns = join.columns.map((column) => {
209
+ const col = this.quoteId(column);
210
+ const aliasedCol = this.quoteId(`${join.alias}_${column}`);
211
+ return `${join.alias}.${col} as ${aliasedCol}`;
212
+ });
158
213
  }
159
214
  else {
160
215
  join.columns = this.columnManager.getColumnsForEntity(join.joinEntity, join.alias);
@@ -198,6 +253,11 @@ class SqlJoinManager {
198
253
  }
199
254
  getFkKey(relationShip) {
200
255
  if (typeof relationShip.fkKey === 'undefined') {
256
+ // Use cached primary key column name from the related entity instead of hardcoded 'id'
257
+ const relatedEntity = this.entityStorage.get(relationShip.entity());
258
+ if (relatedEntity) {
259
+ return relatedEntity._primaryKeyColumnName || 'id';
260
+ }
201
261
  return 'id';
202
262
  }
203
263
  if (typeof relationShip.fkKey === 'string') {
@@ -228,12 +288,7 @@ class SqlJoinManager {
228
288
  return { tableName, schema };
229
289
  }
230
290
  getPrimaryKey() {
231
- for (const prop in this.entity.properties) {
232
- if (this.entity.properties[prop].options.isPrimary) {
233
- return prop;
234
- }
235
- }
236
- return 'id';
291
+ return this.entity._primaryKeyPropertyName || 'id';
237
292
  }
238
293
  formatValue(value) {
239
294
  return (typeof value === 'string') ? `'${value}'` : value;
@@ -1,11 +1,14 @@
1
- import { FilterQuery, Relationship } from '../driver/driver.interface';
1
+ import { DriverInterface, FilterQuery, Relationship } from '../driver/driver.interface';
2
2
  import { EntityStorage } from '../domain/entities';
3
3
  import { SqlConditionBuilder } from './sql-condition-builder';
4
4
  export declare class SqlSubqueryBuilder {
5
5
  private entityStorage;
6
6
  private getConditionBuilder;
7
+ private driver;
7
8
  private aliasCounter;
8
- constructor(entityStorage: EntityStorage, getConditionBuilder: () => SqlConditionBuilder<any>);
9
+ constructor(entityStorage: EntityStorage, getConditionBuilder: () => SqlConditionBuilder<any>, driver: DriverInterface);
10
+ private quoteId;
11
+ private qualifyTable;
9
12
  buildExistsSubquery(relationship: Relationship<any>, filters: FilterQuery<any>, outerAlias: string, negate: boolean, outerModel?: Function): string;
10
13
  private buildSubquery;
11
14
  private buildWhereClause;
@@ -2,11 +2,22 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SqlSubqueryBuilder = void 0;
4
4
  class SqlSubqueryBuilder {
5
- constructor(entityStorage, getConditionBuilder) {
5
+ constructor(entityStorage, getConditionBuilder, driver) {
6
6
  this.entityStorage = entityStorage;
7
7
  this.getConditionBuilder = getConditionBuilder;
8
+ this.driver = driver;
8
9
  this.aliasCounter = 1;
9
10
  }
11
+ quoteId(identifier) {
12
+ const q = this.driver.getIdentifierQuote();
13
+ return `${q}${identifier}${q}`;
14
+ }
15
+ qualifyTable(schema, tableName) {
16
+ if (this.driver.dbType === 'mysql') {
17
+ return this.quoteId(tableName);
18
+ }
19
+ return `${this.quoteId(schema)}.${this.quoteId(tableName)}`;
20
+ }
10
21
  buildExistsSubquery(relationship, filters, outerAlias, negate, outerModel) {
11
22
  const prefix = negate ? 'NOT EXISTS' : 'EXISTS';
12
23
  const subquery = this.buildSubquery(relationship, filters, outerAlias, outerModel);
@@ -28,10 +39,13 @@ class SqlSubqueryBuilder {
28
39
  const outerPkKey = this.getOuterPrimaryKey(relationship, outerModel);
29
40
  const relatedPkKey = this.getRelatedPrimaryKey(relationship);
30
41
  if (relationship.relation === 'one-to-many') {
31
- return `${subqueryAlias}."${fkKey}" = ${outerAlias}."${outerPkKey}"`;
42
+ const fk = this.quoteId(fkKey);
43
+ const pk = this.quoteId(outerPkKey);
44
+ return `${subqueryAlias}.${fk} = ${outerAlias}.${pk}`;
32
45
  }
33
- const outerFk = relationship.columnName;
34
- return `${outerAlias}."${outerFk}" = ${subqueryAlias}."${relatedPkKey}"`;
46
+ const outerFk = this.quoteId(relationship.columnName);
47
+ const relatedPk = this.quoteId(relatedPkKey);
48
+ return `${outerAlias}.${outerFk} = ${subqueryAlias}.${relatedPk}`;
35
49
  }
36
50
  getOuterPrimaryKey(relationship, outerModel) {
37
51
  if (!outerModel) {
@@ -66,11 +80,11 @@ class SqlSubqueryBuilder {
66
80
  const entity = this.entityStorage.get(relationship.entity());
67
81
  if (!entity) {
68
82
  const name = relationship.entity().name.toLowerCase();
69
- return `public.${name}`;
83
+ return this.qualifyTable('public', name);
70
84
  }
71
85
  const schema = entity.schema || 'public';
72
86
  const tableName = entity.tableName || relationship.entity().name.toLowerCase();
73
- return `${schema}."${tableName}"`;
87
+ return this.qualifyTable(schema, tableName);
74
88
  }
75
89
  generateAlias() {
76
90
  const alias = `sq${this.aliasCounter}`;
@@ -1,18 +1,20 @@
1
1
  import { Orm } from '../orm';
2
- import { BunPgDriver } from '../driver/bun-pg.driver';
3
- import { ConnectionSettings } from '../driver/driver.interface';
2
+ import { ConnectionSettings, DriverInterface } from '../driver/driver.interface';
4
3
  import type { Logger } from '../logger';
4
+ import { DriverType } from '../driver/driver-factory';
5
5
  export type DatabaseTestContext = {
6
- orm: Orm<BunPgDriver>;
6
+ orm: Orm<DriverInterface>;
7
7
  executeSql: (sql: string) => Promise<{
8
8
  rows: unknown[];
9
9
  }>;
10
+ driverType: DriverType;
11
+ getDbType: () => 'postgres' | 'mysql';
10
12
  };
11
13
  export type DatabaseTestOptions = {
12
14
  schema?: string;
13
15
  entityFile?: string;
14
16
  logger?: Logger;
15
- connection?: Partial<ConnectionSettings<BunPgDriver>>;
17
+ connection?: Partial<ConnectionSettings>;
16
18
  };
17
19
  type DatabaseTestRoutine = (context: DatabaseTestContext) => Promise<void>;
18
20
  export declare function withDatabase(tables: string[], routine: DatabaseTestRoutine, options?: DatabaseTestOptions): Promise<void>;