@mikro-orm/core 7.3.0-dev.4 → 7.3.0-dev.6

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.
@@ -404,7 +404,7 @@ export class ObjectHydrator extends Hydrator {
404
404
  const code = `// compiled hydrator for entity ${meta.className} (${type + normalizeAccessors ? ' normalized' : ''})\n` +
405
405
  `return function(entity, data, factory, newEntity, convertCustomTypes, schema, parentSchema, normalizeAccessors) {\n` +
406
406
  `${lines.join('\n')}\n}`;
407
- const fnKey = `hydrator-${meta.uniqueName}-${type}-${normalizeAccessors}`;
407
+ const fnKey = `hydrator-${meta.compiledName}-${type}-${normalizeAccessors}`;
408
408
  const hydrator = Utils.createFunction(context, code, this.config.get('compiledFunctions'), fnKey);
409
409
  this.#hydrators[key].set(meta.class, hydrator);
410
410
  return hydrator;
@@ -1461,7 +1461,8 @@ export class MetadataDiscovery {
1461
1461
  }
1462
1462
  }
1463
1463
  rootProp.stiFieldNameMap[meta.discriminatorValue] = prop.fieldNames[0];
1464
- rootProp.stiFieldNames.push(...prop.fieldNames);
1464
+ // subtypes that only differ in type share the column
1465
+ rootProp.stiFieldNames = Utils.unique([...rootProp.stiFieldNames, ...prop.fieldNames]);
1465
1466
  newProp.nullable = true;
1466
1467
  newProp.name = name;
1467
1468
  newProp.hydrate = false;
@@ -32,6 +32,13 @@ export declare class MetadataStorage {
32
32
  getById<T>(id: number): EntityMetadata<T>;
33
33
  /** Returns metadata by class name, optionally throwing if not found. */
34
34
  getByClassName<T = any, V extends boolean = true>(className: string, validate?: V): V extends true ? EntityMetadata<T> : EntityMetadata<T> | undefined;
35
+ /**
36
+ * Returns the first registered metadata with the given table name, in registration order (entities sharing
37
+ * a table, like STI children, resolve to the one registered first). The index is built on first use, as table
38
+ * names are only final once discovery has finished.
39
+ * @internal
40
+ */
41
+ getByTableName<T = any>(tableName: string): EntityMetadata<T> | undefined;
35
42
  /** Returns metadata by unique name, optionally throwing if not found. */
36
43
  getByUniqueName<T = any, V extends boolean = true>(uniqueName: string, validate?: V): V extends true ? EntityMetadata<T> : EntityMetadata<T> | undefined;
37
44
  private validate;
@@ -18,6 +18,8 @@ export class MetadataStorage {
18
18
  #classNameMap;
19
19
  #uniqueNameMap;
20
20
  #ambiguousNames = new Set();
21
+ /** Built on first use by `getByTableName()`, dropped whenever an entity is registered or removed. */
22
+ #tableNameMap;
21
23
  constructor(metadata = {}) {
22
24
  this.#idMap = {};
23
25
  this.#uniqueNameMap = {};
@@ -102,6 +104,7 @@ export class MetadataStorage {
102
104
  /** Registers metadata for the given entity. */
103
105
  set(entityName, meta) {
104
106
  this.#metadataMap.set(entityName, meta);
107
+ this.#tableNameMap = undefined;
105
108
  this.#idMap[meta._id] = meta;
106
109
  this.#uniqueNameMap[meta.uniqueName] = meta;
107
110
  const className = Utils.className(entityName);
@@ -110,6 +113,12 @@ export class MetadataStorage {
110
113
  if (existing && existing !== meta && existing.class !== meta.class) {
111
114
  this.#ambiguousNames.add(className);
112
115
  }
116
+ // colliding names are numbered in storage order, which follows the discovery config, not module evaluation
117
+ const sameName = this.#ambiguousNames.has(className)
118
+ ? [...new Set(this.#metadataMap.values())].filter(m => m.className === meta.className)
119
+ : [meta];
120
+ const ordinal = sameName.indexOf(meta);
121
+ meta.compiledName = ordinal > 0 ? `${meta.className}#${ordinal}` : meta.className;
113
122
  this.#classNameMap[className] = meta;
114
123
  return meta;
115
124
  }
@@ -118,6 +127,7 @@ export class MetadataStorage {
118
127
  const meta = this.find(entityName);
119
128
  if (meta) {
120
129
  this.#metadataMap.delete(meta.class);
130
+ this.#tableNameMap = undefined;
121
131
  delete this.#idMap[meta._id];
122
132
  delete this.#uniqueNameMap[meta.uniqueName];
123
133
  delete this.#classNameMap[meta.className];
@@ -145,6 +155,23 @@ export class MetadataStorage {
145
155
  getByClassName(className, validate = true) {
146
156
  return this.validate(this.#classNameMap[className], className, validate);
147
157
  }
158
+ /**
159
+ * Returns the first registered metadata with the given table name, in registration order (entities sharing
160
+ * a table, like STI children, resolve to the one registered first). The index is built on first use, as table
161
+ * names are only final once discovery has finished.
162
+ * @internal
163
+ */
164
+ getByTableName(tableName) {
165
+ if (!this.#tableNameMap) {
166
+ this.#tableNameMap = new Map();
167
+ for (const meta of this.#metadataMap.values()) {
168
+ if (!this.#tableNameMap.has(meta.tableName)) {
169
+ this.#tableNameMap.set(meta.tableName, meta);
170
+ }
171
+ }
172
+ }
173
+ return this.#tableNameMap.get(tableName);
174
+ }
148
175
  /** Returns metadata by unique name, optionally throwing if not found. */
149
176
  getByUniqueName(uniqueName, validate = true) {
150
177
  return this.validate(this.#uniqueNameMap[uniqueName], uniqueName, validate);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/core",
3
- "version": "7.3.0-dev.4",
3
+ "version": "7.3.0-dev.6",
4
4
  "description": "TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.",
5
5
  "keywords": [
6
6
  "data-mapper",
package/typings.d.ts CHANGED
@@ -1259,6 +1259,8 @@ export interface EntityMetadata<Entity = any, Class extends EntityCtor<Entity> =
1259
1259
  orderBy?: QueryOrderMap<Entity> | QueryOrderMap<Entity>[];
1260
1260
  /** @internal can be used for computed numeric cache keys */
1261
1261
  readonly _id: number;
1262
+ /** @internal name that does not depend on module evaluation order (unlike `_id`), used to key `compiledFunctions` */
1263
+ compiledName?: string;
1262
1264
  }
1263
1265
  /** Options for `ISchemaGenerator.create()`. */
1264
1266
  export interface CreateSchemaOptions {
@@ -95,7 +95,7 @@ export class EntityComparator {
95
95
  lines.push(` return entity${this.wrap(pk)};`);
96
96
  }
97
97
  const code = `// compiled pk getter for entity ${meta.className}\n` + `return function(entity) {\n${lines.join('\n')}\n}`;
98
- const fnKey = `pkGetter-${meta.uniqueName}`;
98
+ const fnKey = `pkGetter-${meta.compiledName}`;
99
99
  const pkSerializer = Utils.createFunction(context, code, this.#config?.get('compiledFunctions'), fnKey);
100
100
  this.#pkGetters.set(meta, pkSerializer);
101
101
  return pkSerializer;
@@ -147,7 +147,7 @@ export class EntityComparator {
147
147
  }
148
148
  const code = `// compiled pk getter (with converted custom types) for entity ${meta.className}\n` +
149
149
  `return function(entity) {\n${lines.join('\n')}\n}`;
150
- const fnKey = `pkGetterConverted-${meta.uniqueName}`;
150
+ const fnKey = `pkGetterConverted-${meta.compiledName}`;
151
151
  const pkSerializer = Utils.createFunction(context, code, this.#config?.get('compiledFunctions'), fnKey);
152
152
  this.#pkGettersConverted.set(meta, pkSerializer);
153
153
  return pkSerializer;
@@ -206,7 +206,7 @@ export class EntityComparator {
206
206
  }
207
207
  }
208
208
  const code = `// compiled pk serializer for entity ${meta.className}\n` + `return function(entity) {\n${lines.join('\n')}\n}`;
209
- const fnKey = `pkSerializer-${meta.uniqueName}`;
209
+ const fnKey = `pkSerializer-${meta.compiledName}`;
210
210
  const pkSerializer = Utils.createFunction(context, code, this.#config?.get('compiledFunctions'), fnKey);
211
211
  this.#pkSerializers.set(meta, pkSerializer);
212
212
  return pkSerializer;
@@ -236,7 +236,7 @@ export class EntityComparator {
236
236
  })
237
237
  .forEach(prop => lines.push(this.getPropertySnapshot(meta, prop, context, this.wrap(prop.name), this.wrap(prop.name), [prop.name])));
238
238
  const code = `return function(entity) {\n const ret = {};\n${lines.join('\n')}\n return ret;\n}`;
239
- const fnKey = `snapshotGenerator-${meta.uniqueName}`;
239
+ const fnKey = `snapshotGenerator-${meta.compiledName}`;
240
240
  const snapshotGenerator = Utils.createFunction(context, code, this.#config?.get('compiledFunctions'), fnKey);
241
241
  this.#snapshotGenerators.set(meta, snapshotGenerator);
242
242
  return snapshotGenerator;
@@ -350,14 +350,27 @@ export class EntityComparator {
350
350
  if (prop.embedded && (meta.embeddable || meta.properties[prop.embedded[0]].object)) {
351
351
  continue;
352
352
  }
353
+ let defined = `typeof ${this.propName(prop.fieldNames[0])} !== 'undefined'`;
354
+ // STI subtypes with conflicting columns share the property, so another subtype's null column must not override it
355
+ if (prop.renamedFrom) {
356
+ lines.push(`${padding} ${this.propName(prop.fieldNames[0], 'mapped')} = true;`);
357
+ defined = `${this.propName(prop.fieldNames[0])} != null`;
358
+ // a column shared with a differently typed subtype is left for each subtype's hydrator to convert
359
+ if (meta.props.some(p => p !== prop && p.name === prop.name && p.fieldNames?.[0] === prop.fieldNames[0])) {
360
+ lines.push(`${padding} if (${defined}) {`);
361
+ lines.push(`${padding} ret${this.wrap(prop.name)} = ${this.propName(prop.fieldNames[0])};`);
362
+ lines.push(`${padding} }`);
363
+ continue;
364
+ }
365
+ }
353
366
  if (prop.runtimeType === 'boolean') {
354
- lines.push(`${padding} if (typeof ${this.propName(prop.fieldNames[0])} !== 'undefined') {`);
367
+ lines.push(`${padding} if (${defined}) {`);
355
368
  lines.push(`${padding} ret${this.wrap(prop.name)} = ${this.propName(prop.fieldNames[0])} == null ? ${this.propName(prop.fieldNames[0])} : !!${this.propName(prop.fieldNames[0])};`);
356
369
  lines.push(`${padding} ${this.propName(prop.fieldNames[0], 'mapped')} = true;`);
357
370
  lines.push(`${padding} }`);
358
371
  }
359
372
  else if (prop.runtimeType === 'Date' && !this.#platform.isNumericProperty(prop)) {
360
- lines.push(`${padding} if (typeof ${this.propName(prop.fieldNames[0])} !== 'undefined') {`);
373
+ lines.push(`${padding} if (${defined}) {`);
361
374
  context.set('parseDate', (value) => this.#platform.parseDate(value));
362
375
  parseDate('ret' + this.wrap(prop.name), this.propName(prop.fieldNames[0]), padding);
363
376
  lines.push(`${padding} ${this.propName(prop.fieldNames[0], 'mapped')} = true;`);
@@ -372,13 +385,13 @@ export class EntityComparator {
372
385
  }
373
386
  return item == null ? item : this.getResultMapper(prop.targetMeta)(item);
374
387
  });
375
- lines.push(`${padding} if (typeof ${this.propName(prop.fieldNames[0])} !== 'undefined') {`);
388
+ lines.push(`${padding} if (${defined}) {`);
376
389
  lines.push(`${padding} ret${this.wrap(prop.name)} = ${this.propName(prop.fieldNames[0])} == null ? ${this.propName(prop.fieldNames[0])} : mapEmbeddedResult_${idx}(${this.propName(prop.fieldNames[0])});`);
377
390
  lines.push(`${padding} ${this.propName(prop.fieldNames[0], 'mapped')} = true;`);
378
391
  lines.push(`${padding} }`);
379
392
  }
380
393
  else if (prop.kind !== ReferenceKind.EMBEDDED) {
381
- lines.push(`${padding} if (typeof ${this.propName(prop.fieldNames[0])} !== 'undefined') {`);
394
+ lines.push(`${padding} if (${defined}) {`);
382
395
  lines.push(`${padding} ret${this.wrap(prop.name)} = ${this.propName(prop.fieldNames[0])};`);
383
396
  lines.push(`${padding} ${this.propName(prop.fieldNames[0], 'mapped')} = true;`);
384
397
  lines.push(`${padding} }`);
@@ -420,7 +433,7 @@ export class EntityComparator {
420
433
  lines.push(` for (let k in result) { if (Object.hasOwn(result, k) && !mapped[k] && ret[k] === undefined${knownKeysGuard}) ret[k] = result[k]; }`);
421
434
  const code = `// compiled mapper for entity ${meta.className}\n` +
422
435
  `return function(result) {\n const ret = {};\n${lines.join('\n')}\n return ret;\n}`;
423
- const fnKey = `resultMapper-${meta.uniqueName}`;
436
+ const fnKey = `resultMapper-${meta.compiledName}`;
424
437
  const resultMapper = Utils.createFunction(context, code, this.#config?.get('compiledFunctions'), fnKey);
425
438
  this.#mappers.set(meta, resultMapper);
426
439
  return resultMapper;
@@ -703,7 +716,7 @@ export class EntityComparator {
703
716
  lines.push(`}`);
704
717
  const code = `// compiled comparator for entity ${meta.className}\n` +
705
718
  `return function(last, current, options) {\n const diff = {};\n${lines.join('\n')}\n return diff;\n}`;
706
- const fnKey = `comparator-${meta.uniqueName}`;
719
+ const fnKey = `comparator-${meta.compiledName}`;
707
720
  const comparator = Utils.createFunction(context, code, this.#config?.get('compiledFunctions'), fnKey);
708
721
  this.#comparators.set(meta, comparator);
709
722
  return comparator;
@@ -46,6 +46,8 @@ export declare class QueryHelper {
46
46
  static processWhere<T extends object>(options: ProcessWhereOptions<T>): FilterQuery<T>;
47
47
  /** @internal Splits a polymorphic target key like `imageable[Article]` into the property and target class names. */
48
48
  static splitPolymorphicKey(key: string): [string, string?];
49
+ /** @internal Resolves the targets of a polymorphic target key like `imageable[Article]` or `imageable[Product,Article]`. */
50
+ static findTargetMetas(prop: EntityProperty | null | undefined, key: string): EntityMetadata[];
49
51
  /** @internal Resolves the target entity of a condition key, including polymorphic target keys like `imageable[Article]`. */
50
52
  static findTargetMeta(prop: EntityProperty | null | undefined, key: string): EntityMetadata | undefined;
51
53
  /**
@@ -54,6 +56,12 @@ export declare class QueryHelper {
54
56
  * when more targets match. The first target alone keeps the regular relation join.
55
57
  */
56
58
  private static splitPolymorphicConditions;
59
+ /**
60
+ * Routes ordering by properties of polymorphic to-one relations to the targets that define them, e.g.
61
+ * `{ imageable: { name: 'asc' } }` becomes `{ 'imageable[Product,Article]': { name: 'asc' } }`, ordered by
62
+ * the first non-null value. Nested relations and formulas are ordered via the first target defining them.
63
+ */
64
+ private static splitPolymorphicOrderBy;
57
65
  /** Property names used in a condition, including those nested in group operators. */
58
66
  private static collectConditionKeys;
59
67
  static getActiveFilters<T>(meta: EntityMetadata<T>, options: FilterOptions | undefined, filters: Dictionary<FilterDef>): FilterDef[];
@@ -262,8 +262,10 @@ export class QueryHelper {
262
262
  if (!Utils.isPlainObject(where)) {
263
263
  return where;
264
264
  }
265
- if (meta && options.type !== 'orderBy') {
266
- where = QueryHelper.splitPolymorphicConditions(meta, where);
265
+ if (meta) {
266
+ where = (options.type === 'orderBy'
267
+ ? QueryHelper.splitPolymorphicOrderBy(meta, where)
268
+ : QueryHelper.splitPolymorphicConditions(meta, where));
267
269
  }
268
270
  return Utils.getObjectQueryKeys(where).reduce((o, key) => {
269
271
  let value = where[key];
@@ -328,13 +330,18 @@ export class QueryHelper {
328
330
  }
329
331
  /** @internal Splits a polymorphic target key like `imageable[Article]` into the property and target class names. */
330
332
  static splitPolymorphicKey(key) {
331
- const match = /^(\w+)\[(\w+)]$/.exec(key);
333
+ const match = /^(\w+)\[([\w,]+)]$/.exec(key);
332
334
  return match ? [match[1], match[2]] : [key];
333
335
  }
336
+ /** @internal Resolves the targets of a polymorphic target key like `imageable[Article]` or `imageable[Product,Article]`. */
337
+ static findTargetMetas(prop, key) {
338
+ const [, targetNames] = QueryHelper.splitPolymorphicKey(key);
339
+ const targets = targetNames?.split(',').map(name => prop?.polymorphTargets?.find(t => t.className === name));
340
+ return targets?.every(Boolean) ? targets : [];
341
+ }
334
342
  /** @internal Resolves the target entity of a condition key, including polymorphic target keys like `imageable[Article]`. */
335
343
  static findTargetMeta(prop, key) {
336
- const [, targetName] = QueryHelper.splitPolymorphicKey(key);
337
- return targetName ? prop?.polymorphTargets?.find(t => t.className === targetName) : prop?.targetMeta;
344
+ return QueryHelper.splitPolymorphicKey(key)[1] ? QueryHelper.findTargetMetas(prop, key)[0] : prop?.targetMeta;
338
345
  }
339
346
  /**
340
347
  * Routes conditions on polymorphic to-one relations to the targets that define all the queried properties,
@@ -373,6 +380,34 @@ export class QueryHelper {
373
380
  }
374
381
  return ret;
375
382
  }
383
+ /**
384
+ * Routes ordering by properties of polymorphic to-one relations to the targets that define them, e.g.
385
+ * `{ imageable: { name: 'asc' } }` becomes `{ 'imageable[Product,Article]': { name: 'asc' } }`, ordered by
386
+ * the first non-null value. Nested relations and formulas are ordered via the first target defining them.
387
+ */
388
+ static splitPolymorphicOrderBy(meta, orderBy) {
389
+ const ret = {};
390
+ for (const key of Utils.getObjectQueryKeys(orderBy)) {
391
+ const prop = meta.properties[key];
392
+ const value = orderBy[key];
393
+ if (!prop?.polymorphTargets ||
394
+ ![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) ||
395
+ !Utils.isPlainObject(value)) {
396
+ ret[key] = value;
397
+ continue;
398
+ }
399
+ for (const field of Utils.getObjectQueryKeys(value)) {
400
+ const found = prop.polymorphTargets.filter(target => field in target.properties);
401
+ const sharedPK = found.length === prop.polymorphTargets.length && found.every(target => target.primaryKeys.includes(field));
402
+ const single = Utils.isPlainObject(value[field]) || found.some(target => target.properties[field].formula);
403
+ const targets = found.length === 0 ? [prop.targetMeta] : single ? [found[0]] : found;
404
+ // shared PKs are ordered by the FK column without a join
405
+ const targetKey = sharedPK ? key : `${key}[${targets.map(target => target.className).join(',')}]`;
406
+ ret[targetKey] = { ...ret[targetKey], [field]: value[field] };
407
+ }
408
+ }
409
+ return ret;
410
+ }
376
411
  /** Property names used in a condition, including those nested in group operators. */
377
412
  static collectConditionKeys(payload) {
378
413
  return Object.keys(payload).flatMap(k => {
package/utils/Utils.js CHANGED
@@ -153,7 +153,7 @@ export function parseJsonSafe(value) {
153
153
  /** Collection of general-purpose utility methods used throughout the ORM. */
154
154
  export class Utils {
155
155
  static PK_SEPARATOR = '~~~';
156
- static #ORM_VERSION = '7.3.0-dev.4';
156
+ static #ORM_VERSION = '7.3.0-dev.6';
157
157
  /** Default session variable name backing an RLS filter argument (`current_setting('mikro.<filter>.<arg>')`). */
158
158
  static getRlsSettingName(filterName, argName) {
159
159
  return `mikro.${filterName}.${argName}`;