@mikro-orm/core 7.3.0-dev.2 → 7.3.0-dev.20

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 (49) hide show
  1. package/EntityManager.js +131 -32
  2. package/drivers/DatabaseDriver.d.ts +2 -0
  3. package/drivers/DatabaseDriver.js +16 -3
  4. package/entity/EntityAssigner.js +15 -3
  5. package/entity/EntityFactory.d.ts +8 -0
  6. package/entity/EntityFactory.js +69 -3
  7. package/entity/EntityLoader.js +31 -15
  8. package/entity/WrappedEntity.js +6 -1
  9. package/entity/defineEntity.d.ts +32 -19
  10. package/entity/defineEntity.js +9 -0
  11. package/entity/validators.js +4 -1
  12. package/errors.d.ts +7 -0
  13. package/errors.js +15 -0
  14. package/hydration/ObjectHydrator.js +6 -2
  15. package/metadata/MetadataDiscovery.d.ts +10 -0
  16. package/metadata/MetadataDiscovery.js +178 -30
  17. package/metadata/MetadataStorage.d.ts +8 -0
  18. package/metadata/MetadataStorage.js +47 -3
  19. package/metadata/MetadataValidator.js +2 -1
  20. package/metadata/types.d.ts +12 -0
  21. package/package.json +1 -1
  22. package/platforms/Platform.d.ts +6 -0
  23. package/platforms/Platform.js +26 -0
  24. package/serialization/EntitySerializer.js +52 -12
  25. package/types/DecimalType.js +34 -1
  26. package/types/DoubleType.d.ts +7 -3
  27. package/types/DoubleType.js +8 -3
  28. package/typings.d.ts +26 -6
  29. package/typings.js +5 -2
  30. package/unit-of-work/ChangeSetComputer.js +22 -7
  31. package/unit-of-work/ChangeSetPersister.js +24 -5
  32. package/unit-of-work/CommitOrderCalculator.js +2 -1
  33. package/unit-of-work/IdentityMap.d.ts +3 -2
  34. package/unit-of-work/IdentityMap.js +30 -7
  35. package/unit-of-work/UnitOfWork.js +45 -7
  36. package/utils/Configuration.d.ts +12 -1
  37. package/utils/EntityComparator.d.ts +5 -0
  38. package/utils/EntityComparator.js +87 -19
  39. package/utils/QueryHelper.d.ts +14 -0
  40. package/utils/QueryHelper.js +90 -13
  41. package/utils/RawQueryFragment.d.ts +5 -1
  42. package/utils/RawQueryFragment.js +27 -19
  43. package/utils/TransactionManager.js +4 -0
  44. package/utils/Utils.d.ts +2 -0
  45. package/utils/Utils.js +24 -8
  46. package/utils/clone.js +5 -7
  47. package/utils/env-vars.js +2 -0
  48. package/utils/fs-utils.js +19 -15
  49. package/utils/upsert-utils.js +2 -1
@@ -89,7 +89,10 @@ export class UnitOfWork {
89
89
  Utils.isPlainObject(data[prop.name])) {
90
90
  // Skip polymorphic relations - they use PolymorphicRef wrapper
91
91
  if (!prop.polymorphic) {
92
- data[prop.name] = Utils.getPrimaryKeyValues(data[prop.name], prop.targetMeta, true);
92
+ // with `targetKey`, the FK holds that value instead of the PK
93
+ data[prop.name] = prop.targetKey
94
+ ? data[prop.name][prop.targetKey]
95
+ : Utils.getPrimaryKeyValues(data[prop.name], prop.targetMeta, true);
93
96
  }
94
97
  }
95
98
  else if (prop.kind === ReferenceKind.EMBEDDED && !prop.object && Utils.isPlainObject(data[prop.name])) {
@@ -205,7 +208,18 @@ export class UnitOfWork {
205
208
  value = prop.customType.convertToJSValue(value, this.#platform, { mode: 'hydration' });
206
209
  }
207
210
  const hash = this.#identityMap.getKeyHash(key, '' + value, schema);
208
- return this.#identityMap.getByHash(meta, hash);
211
+ const entity = this.#identityMap.getByHash(meta, hash);
212
+ // the entry is stale when the key changed via refresh or merge, unless only the unflushed value differs
213
+ if (entity && '' + entity[key] !== '' + value) {
214
+ // the snapshot holds the database value
215
+ const dbValue = prop?.customType
216
+ ? prop.customType.convertToDatabaseValue(value, this.#platform, { mode: 'serialization' })
217
+ : value;
218
+ if ('' + helper(entity).__originalEntityData?.[key] !== '' + dbValue) {
219
+ return undefined;
220
+ }
221
+ }
222
+ return entity;
209
223
  }
210
224
  /**
211
225
  * Stores an entity in the identity map under an alternate key (non-PK property).
@@ -368,6 +382,10 @@ export class UnitOfWork {
368
382
  }
369
383
  /** Marks an entity for removal, cascading to related entities. */
370
384
  remove(entity, visited, options = {}) {
385
+ // a reference known only by its `targetKey` has no PK, so the delete would match nothing
386
+ if (!helper(entity).__initialized && !helper(entity).hasPrimaryKey()) {
387
+ throw ValidationError.referenceWithoutPK(entity);
388
+ }
371
389
  // allow removing not managed entities if they are not part of the persist stack
372
390
  if (helper(entity).__managed || !this.#persistStack.has(entity)) {
373
391
  this.#removeStack.add(entity);
@@ -582,19 +600,27 @@ export class UnitOfWork {
582
600
  this.findEarlyUpdates(cs, inserts[cs.meta.uniqueName]);
583
601
  }
584
602
  }
603
+ // hashes of the inserts' PKs and unique values per type, built lazily, so matching a removed entity is a set lookup
604
+ const insertHashes = {};
585
605
  for (const entity of this.#removeStack) {
586
606
  const wrapped = helper(entity);
587
607
  /* v8 ignore next */
588
608
  if (wrapped.__processing) {
589
609
  continue;
590
610
  }
591
- const deletePkHash = [wrapped.getSerializedPrimaryKey(), ...this.expandUniqueProps(entity)];
592
- let type = ChangeSetType.DELETE;
593
- for (const cs of inserts[wrapped.__meta.uniqueName] ?? []) {
594
- if (deletePkHash.some(hash => hash === cs.getSerializedPrimaryKey() || this.expandUniqueProps(cs.entity).find(child => hash === child))) {
595
- type = ChangeSetType.DELETE_EARLY;
611
+ const uniqueName = wrapped.__meta.uniqueName;
612
+ let hashes = insertHashes[uniqueName];
613
+ if (!hashes) {
614
+ hashes = insertHashes[uniqueName] = new Set();
615
+ for (const cs of inserts[uniqueName] ?? []) {
616
+ hashes.add(cs.getSerializedPrimaryKey());
617
+ for (const hash of this.expandUniqueProps(cs.entity)) {
618
+ hashes.add(hash);
619
+ }
596
620
  }
597
621
  }
622
+ const deletePkHash = [wrapped.getSerializedPrimaryKey(), ...this.expandUniqueProps(entity)];
623
+ const type = deletePkHash.some(hash => hashes.has(hash)) ? ChangeSetType.DELETE_EARLY : ChangeSetType.DELETE;
598
624
  this.computeChangeSet(entity, type);
599
625
  }
600
626
  }
@@ -1118,6 +1144,11 @@ export class UnitOfWork {
1118
1144
  for (const changeSet of changeSets) {
1119
1145
  const wrapped = helper(changeSet.entity);
1120
1146
  wrapped.__originalEntityData = this.#comparator.prepareEntity(changeSet.entity);
1147
+ // re-index so the entity is found by its new `targetKey` values instead of the old ones
1148
+ if (changeSet.meta.root.targetKeys?.some(key => key in changeSet.payload)) {
1149
+ this.#identityMap.delete(changeSet.entity);
1150
+ this.#identityMap.store(changeSet.entity);
1151
+ }
1121
1152
  if (!wrapped.__initialized) {
1122
1153
  for (const prop of changeSet.meta.relations) {
1123
1154
  if ([ReferenceKind.MANY_TO_MANY, ReferenceKind.ONE_TO_MANY].includes(prop.kind) &&
@@ -1260,6 +1291,13 @@ export class UnitOfWork {
1260
1291
  calc.discoverProperty({ ...prop, targetMeta }, meta._id);
1261
1292
  }
1262
1293
  }
1294
+ else if (prop.targetMeta?.inheritanceType === 'tpt') {
1295
+ calc.discoverProperty(prop, meta._id);
1296
+ // a TPT subclass delete has only a leaf change set that also removes the target row; weak edge so it can't break insert order
1297
+ for (const targetMeta of this.#metadata.find(prop.targetMeta.class).allTPTDescendants ?? []) {
1298
+ calc.discoverProperty({ ...prop, targetMeta, nullable: true }, meta._id);
1299
+ }
1300
+ }
1263
1301
  else {
1264
1302
  calc.discoverProperty(prop, meta._id);
1265
1303
  }
@@ -218,6 +218,17 @@ export type MigrationsOptions = {
218
218
  * @default true
219
219
  */
220
220
  allOrNothing?: boolean;
221
+ /**
222
+ * Serialize concurrent `migrator.up()`/`migrator.down()` runs (e.g. several replicas migrating on start) with a
223
+ * database lock, held on a pooled connection of its own for the whole run (so `pool.max` needs to be at least 2).
224
+ * Supported on PostgreSQL, MySQL, MariaDB and MSSQL, ignored elsewhere.
225
+ * @default false
226
+ */
227
+ lock?: boolean;
228
+ /**
229
+ * How long to wait for the migration lock (in milliseconds) before failing. Waits indefinitely by default.
230
+ */
231
+ lockTimeout?: number;
221
232
  /**
222
233
  * Allow dropping tables during schema diff.
223
234
  * @default true
@@ -593,7 +604,7 @@ export interface Options<Driver extends IDatabaseDriver = IDatabaseDriver, EM ex
593
604
  /**
594
605
  * Force `Date` values to be stored in UTC for datetime columns without timezone.
595
606
  * Works for MySQL (`datetime` type), PostgreSQL (`timestamp` type), and MSSQL (`datetime`/`datetime2` types).
596
- * SQLite does this by default.
607
+ * SQLite does this by default. MySQL and MariaDB also get their session time zone set on every new connection.
597
608
  * @default true
598
609
  */
599
610
  forceUtcTimezone: boolean;
@@ -19,6 +19,11 @@ export declare class EntityComparator {
19
19
  diffEntities<T extends object>(entityName: EntityName<T>, a: EntityData<T>, b: EntityData<T>, options?: {
20
20
  includeInverseSides?: boolean;
21
21
  }): EntityData<T>;
22
+ /**
23
+ * Returns true if the comparator checks this property with `!==`, so matching values are identical primitives.
24
+ * @internal
25
+ */
26
+ isStrictlyCompared<T>(prop: EntityProperty<T>): boolean;
22
27
  /** Returns true if two entity snapshots are identical (no differences). */
23
28
  matching<T extends object>(entityName: EntityName<T>, a: EntityData<T>, b: EntityData<T>): boolean;
24
29
  /**
@@ -1,6 +1,7 @@
1
1
  import { clone } from './clone.js';
2
2
  import { ReferenceKind } from '../enums.js';
3
3
  import { compareArrays, compareBooleans, compareBuffers, compareObjects, equals, parseJsonSafe, Utils, } from './Utils.js';
4
+ import { DateType } from '../types/DateType.js';
4
5
  import { JsonType } from '../types/JsonType.js';
5
6
  import { Raw } from './RawQueryFragment.js';
6
7
  import { EntityIdentifier } from '../entity/EntityIdentifier.js';
@@ -29,6 +30,17 @@ export class EntityComparator {
29
30
  const comparator = this.getEntityComparator(entityName);
30
31
  return Utils.callCompiledFunction(comparator, a, b, options);
31
32
  }
33
+ /**
34
+ * Returns true if the comparator checks this property with `!==`, so matching values are identical primitives.
35
+ * @internal
36
+ */
37
+ isStrictlyCompared(prop) {
38
+ if (prop.kind !== ReferenceKind.SCALAR || prop.customType?.compareValues) {
39
+ return false;
40
+ }
41
+ const type = (prop.customType ? prop.customType.compareAsType() : prop.type).toLowerCase();
42
+ return ['string', 'number', 'bigint'].includes(type);
43
+ }
32
44
  /** Returns true if two entity snapshots are identical (no differences). */
33
45
  matching(entityName, a, b) {
34
46
  const diff = this.diffEntities(entityName, a, b);
@@ -95,7 +107,7 @@ export class EntityComparator {
95
107
  lines.push(` return entity${this.wrap(pk)};`);
96
108
  }
97
109
  const code = `// compiled pk getter for entity ${meta.className}\n` + `return function(entity) {\n${lines.join('\n')}\n}`;
98
- const fnKey = `pkGetter-${meta.uniqueName}`;
110
+ const fnKey = `pkGetter-${meta.compiledName}`;
99
111
  const pkSerializer = Utils.createFunction(context, code, this.#config?.get('compiledFunctions'), fnKey);
100
112
  this.#pkGetters.set(meta, pkSerializer);
101
113
  return pkSerializer;
@@ -147,7 +159,7 @@ export class EntityComparator {
147
159
  }
148
160
  const code = `// compiled pk getter (with converted custom types) for entity ${meta.className}\n` +
149
161
  `return function(entity) {\n${lines.join('\n')}\n}`;
150
- const fnKey = `pkGetterConverted-${meta.uniqueName}`;
162
+ const fnKey = `pkGetterConverted-${meta.compiledName}`;
151
163
  const pkSerializer = Utils.createFunction(context, code, this.#config?.get('compiledFunctions'), fnKey);
152
164
  this.#pkGettersConverted.set(meta, pkSerializer);
153
165
  return pkSerializer;
@@ -206,7 +218,7 @@ export class EntityComparator {
206
218
  }
207
219
  }
208
220
  const code = `// compiled pk serializer for entity ${meta.className}\n` + `return function(entity) {\n${lines.join('\n')}\n}`;
209
- const fnKey = `pkSerializer-${meta.uniqueName}`;
221
+ const fnKey = `pkSerializer-${meta.compiledName}`;
210
222
  const pkSerializer = Utils.createFunction(context, code, this.#config?.get('compiledFunctions'), fnKey);
211
223
  this.#pkSerializers.set(meta, pkSerializer);
212
224
  return pkSerializer;
@@ -236,7 +248,7 @@ export class EntityComparator {
236
248
  })
237
249
  .forEach(prop => lines.push(this.getPropertySnapshot(meta, prop, context, this.wrap(prop.name), this.wrap(prop.name), [prop.name])));
238
250
  const code = `return function(entity) {\n const ret = {};\n${lines.join('\n')}\n return ret;\n}`;
239
- const fnKey = `snapshotGenerator-${meta.uniqueName}`;
251
+ const fnKey = `snapshotGenerator-${meta.compiledName}`;
240
252
  const snapshotGenerator = Utils.createFunction(context, code, this.#config?.get('compiledFunctions'), fnKey);
241
253
  this.#snapshotGenerators.set(meta, snapshotGenerator);
242
254
  return snapshotGenerator;
@@ -350,19 +362,48 @@ export class EntityComparator {
350
362
  if (prop.embedded && (meta.embeddable || meta.properties[prop.embedded[0]].object)) {
351
363
  continue;
352
364
  }
365
+ let defined = `typeof ${this.propName(prop.fieldNames[0])} !== 'undefined'`;
366
+ // STI subtypes with conflicting columns share the property, so another subtype's null column must not override it
367
+ if (prop.renamedFrom) {
368
+ lines.push(`${padding} ${this.propName(prop.fieldNames[0], 'mapped')} = true;`);
369
+ defined = `${this.propName(prop.fieldNames[0])} != null`;
370
+ const sharesColumn = meta.props.some(p => p !== prop && p.name === prop.name && p.fieldNames?.[0] === prop.fieldNames[0]);
371
+ // hydrators expect mapped embeddable keys, so a shared object embeddable is mapped only for its own subtypes
372
+ if (sharesColumn && prop.kind === ReferenceKind.EMBEDDED && prop.object && meta.discriminatorMap) {
373
+ const owners = Object.entries(meta.discriminatorMap)
374
+ .filter(([, cls]) => this.#metadata.find(cls)?.properties[prop.name]?.targetMeta === prop.targetMeta)
375
+ .map(([value]) => `${this.propName(meta.discriminatorColumn)} == ${JSON.stringify(value)}`);
376
+ defined += ` && (${owners.join(' || ')})`;
377
+ }
378
+ else if (sharesColumn) {
379
+ // a column shared with a differently typed subtype is left for each subtype's hydrator to convert
380
+ lines.push(`${padding} if (${defined}) {`);
381
+ lines.push(`${padding} ret${this.wrap(prop.name)} = ${this.propName(prop.fieldNames[0])};`);
382
+ lines.push(`${padding} }`);
383
+ continue;
384
+ }
385
+ }
353
386
  if (prop.runtimeType === 'boolean') {
354
- lines.push(`${padding} if (typeof ${this.propName(prop.fieldNames[0])} !== 'undefined') {`);
387
+ lines.push(`${padding} if (${defined}) {`);
355
388
  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
389
  lines.push(`${padding} ${this.propName(prop.fieldNames[0], 'mapped')} = true;`);
357
390
  lines.push(`${padding} }`);
358
391
  }
359
392
  else if (prop.runtimeType === 'Date' && !this.#platform.isNumericProperty(prop)) {
360
- lines.push(`${padding} if (typeof ${this.propName(prop.fieldNames[0])} !== 'undefined') {`);
393
+ lines.push(`${padding} if (${defined}) {`);
361
394
  context.set('parseDate', (value) => this.#platform.parseDate(value));
362
395
  parseDate('ret' + this.wrap(prop.name), this.propName(prop.fieldNames[0]), padding);
363
396
  lines.push(`${padding} ${this.propName(prop.fieldNames[0], 'mapped')} = true;`);
364
397
  lines.push(`${padding} }`);
365
398
  }
399
+ else if (prop.customType instanceof DateType) {
400
+ // tedious returns DATE columns as `Date`, map them to the string the entity holds, so the snapshot matches it
401
+ context.set('convertDateToJSValue', (value) => this.#platform.convertDateToJSValue(value));
402
+ lines.push(`${padding} if (${defined}) {`);
403
+ lines.push(`${padding} ret${this.wrap(prop.name)} = ${this.propName(prop.fieldNames[0])} instanceof Date ? convertDateToJSValue(${this.propName(prop.fieldNames[0])}) : ${this.propName(prop.fieldNames[0])};`);
404
+ lines.push(`${padding} ${this.propName(prop.fieldNames[0], 'mapped')} = true;`);
405
+ lines.push(`${padding} }`);
406
+ }
366
407
  else if (prop.kind === ReferenceKind.EMBEDDED && (prop.object || meta.embeddable)) {
367
408
  const idx = this.#tmpIndex++;
368
409
  context.set(`mapEmbeddedResult_${idx}`, (data) => {
@@ -372,13 +413,13 @@ export class EntityComparator {
372
413
  }
373
414
  return item == null ? item : this.getResultMapper(prop.targetMeta)(item);
374
415
  });
375
- lines.push(`${padding} if (typeof ${this.propName(prop.fieldNames[0])} !== 'undefined') {`);
416
+ lines.push(`${padding} if (${defined}) {`);
376
417
  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
418
  lines.push(`${padding} ${this.propName(prop.fieldNames[0], 'mapped')} = true;`);
378
419
  lines.push(`${padding} }`);
379
420
  }
380
421
  else if (prop.kind !== ReferenceKind.EMBEDDED) {
381
- lines.push(`${padding} if (typeof ${this.propName(prop.fieldNames[0])} !== 'undefined') {`);
422
+ lines.push(`${padding} if (${defined}) {`);
382
423
  lines.push(`${padding} ret${this.wrap(prop.name)} = ${this.propName(prop.fieldNames[0])};`);
383
424
  lines.push(`${padding} ${this.propName(prop.fieldNames[0], 'mapped')} = true;`);
384
425
  lines.push(`${padding} }`);
@@ -420,7 +461,7 @@ export class EntityComparator {
420
461
  lines.push(` for (let k in result) { if (Object.hasOwn(result, k) && !mapped[k] && ret[k] === undefined${knownKeysGuard}) ret[k] = result[k]; }`);
421
462
  const code = `// compiled mapper for entity ${meta.className}\n` +
422
463
  `return function(result) {\n const ret = {};\n${lines.join('\n')}\n return ret;\n}`;
423
- const fnKey = `resultMapper-${meta.uniqueName}`;
464
+ const fnKey = `resultMapper-${meta.compiledName}`;
424
465
  const resultMapper = Utils.createFunction(context, code, this.#config?.get('compiledFunctions'), fnKey);
425
466
  this.#mappers.set(meta, resultMapper);
426
467
  return resultMapper;
@@ -499,11 +540,22 @@ export class EntityComparator {
499
540
  }
500
541
  return true;
501
542
  }
543
+ // a same-named property of a different kind in another polymorphic variant applies only to its own variants
544
+ const guard = (childProp, code) => {
545
+ const variants = prop.targetMeta?.polymorphs?.filter(m => m.properties[childProp.embedded[1]]) ?? [];
546
+ const owners = variants.filter(m => Utils.isSameKind(m.properties[childProp.embedded[1]], childProp));
547
+ if (owners.length === variants.length) {
548
+ return code;
549
+ }
550
+ const discriminator = `entity${[...path, prop.targetMeta.discriminatorColumn].map(k => this.wrap(k)).join('')}`;
551
+ const conds = owners.map(m => `${discriminator} == ${JSON.stringify(m.discriminatorValue)}`);
552
+ return `${padding} if (${conds.join(' || ')}) {\n${code}\n${padding} }`;
553
+ };
554
+ const childProps = meta.props.filter(p => p.embedded?.[0] === prop.name &&
555
+ // object for JSON embeddable
556
+ (p.object || p.persist !== false));
502
557
  ret +=
503
- meta.props
504
- .filter(p => p.embedded?.[0] === prop.name &&
505
- // object for JSON embeddable
506
- (p.object || p.persist !== false))
558
+ childProps
507
559
  .map(childProp => {
508
560
  const childDataKey = meta.embeddable || prop.object ? dataKey + this.wrap(childProp.embedded[1]) : this.wrap(childProp.name);
509
561
  const childEntityKey = [...path, childProp.embedded[1]].map(k => this.wrap(k)).join('');
@@ -526,6 +578,7 @@ export class EntityComparator {
526
578
  }
527
579
  return `${padding} if (${childCond}) ret${childDataKey} = clone(entity${childEntityKey});`;
528
580
  })
581
+ .map((code, i) => guard(childProps[i], code))
529
582
  .join('\n') + `\n`;
530
583
  if (this.shouldSerialize(prop, dataKey)) {
531
584
  return `${ret + padding} ret${dataKey} = cloneEmbeddable(ret${dataKey});\n${padding}}`;
@@ -608,9 +661,24 @@ export class EntityComparator {
608
661
  ret += ` if (discriminator !== undefined) break;\n`;
609
662
  ret += ` __cls${level} = Object.getPrototypeOf(__cls${level});\n`;
610
663
  ret += ` }\n`;
611
- ret += ` const pk = val${level}?.__helper?.__identifier && !val${level}?.__helper?.hasPrimaryKey()\n`;
612
- ret += ` ? val${level}.__helper.__identifier\n`;
613
- ret += ` : toArray(val${level}?.__helper?.getPrimaryKey(true));\n`;
664
+ if (prop.targetKey) {
665
+ // the FK holds the `targetKey` value in the format of the actual target's column
666
+ const targetKey = prop.targetKey;
667
+ const targetKeyValueKey = `targetKeyValue_${this.safeKey(prop.name)}`;
668
+ context.set(targetKeyValueKey, (val) => {
669
+ const customType = val.__meta.properties[targetKey]?.customType;
670
+ const value = val[targetKey];
671
+ return customType
672
+ ? customType.convertToDatabaseValue(value, this.#platform, { mode: 'serialization' })
673
+ : value;
674
+ });
675
+ ret += ` const pk = ${targetKeyValueKey}(val${level});\n`;
676
+ }
677
+ else {
678
+ ret += ` const pk = val${level}?.__helper?.__identifier && !val${level}?.__helper?.hasPrimaryKey()\n`;
679
+ ret += ` ? val${level}.__helper.__identifier\n`;
680
+ ret += ` : toArray(val${level}?.__helper?.getPrimaryKey(true));\n`;
681
+ }
614
682
  ret += ` ret${dataKey} = new PolymorphicRef(discriminator, pk);\n`;
615
683
  ret += ` }\n`;
616
684
  }
@@ -624,8 +692,8 @@ export class EntityComparator {
624
692
  ret += ` } else if (typeof entity${entityKey} !== 'undefined') {\n`;
625
693
  ret += ` const val${level} = entity${entityKey}${unwrap};\n`;
626
694
  if (targetProp?.customType) {
627
- // If targetKey property has a custom type, convert to database value
628
- const convertorKey = this.registerCustomType(targetProp, context);
695
+ // keyed by the relation name to avoid clashing with a same-named owner prop, the relation itself lacks the custom type with an explicit `columnType`
696
+ const convertorKey = this.registerCustomType({ ...prop, customType: targetProp.customType }, context);
629
697
  ret += ` ret${dataKey} = convertToDatabaseValue_${convertorKey}(val${level}${targetAccess});\n`;
630
698
  }
631
699
  else {
@@ -688,7 +756,7 @@ export class EntityComparator {
688
756
  lines.push(`}`);
689
757
  const code = `// compiled comparator for entity ${meta.className}\n` +
690
758
  `return function(last, current, options) {\n const diff = {};\n${lines.join('\n')}\n return diff;\n}`;
691
- const fnKey = `comparator-${meta.uniqueName}`;
759
+ const fnKey = `comparator-${meta.compiledName}`;
692
760
  const comparator = Utils.createFunction(context, code, this.#config?.get('compiledFunctions'), fnKey);
693
761
  this.#comparators.set(meta, comparator);
694
762
  return comparator;
@@ -35,11 +35,19 @@ export declare class QueryHelper {
35
35
  */
36
36
  static liftGroupOperators<T extends object>(where: Dictionary, meta: EntityMetadata<T>, metadata: MetadataStorage, key?: string): string | undefined;
37
37
  static inlinePrimaryKeyObjects<T extends object>(where: Dictionary, meta: EntityMetadata<T>, metadata: MetadataStorage, key?: string): boolean;
38
+ /**
39
+ * A bare value on a polymorphic relation would be compared to the discriminator column, so PK values are turned
40
+ * into a PK condition instead, e.g. `1` becomes `{ id: 1 }` and `[{ id: 1 }, { id: 2 }]` becomes `{ id: { $in: [1, 2] } }`.
41
+ * Null checks and `[type, id]` tuples keep their meaning.
42
+ */
43
+ private static inlinePolymorphicPrimaryKeys;
38
44
  /** Copies the plain object and array structure of the condition, keeping the leaf values (e.g. entities) by reference. */
39
45
  private static cloneWhere;
40
46
  static processWhere<T extends object>(options: ProcessWhereOptions<T>): FilterQuery<T>;
41
47
  /** @internal Splits a polymorphic target key like `imageable[Article]` into the property and target class names. */
42
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[];
43
51
  /** @internal Resolves the target entity of a condition key, including polymorphic target keys like `imageable[Article]`. */
44
52
  static findTargetMeta(prop: EntityProperty | null | undefined, key: string): EntityMetadata | undefined;
45
53
  /**
@@ -48,6 +56,12 @@ export declare class QueryHelper {
48
56
  * when more targets match. The first target alone keeps the regular relation join.
49
57
  */
50
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;
51
65
  /** Property names used in a condition, including those nested in group operators. */
52
66
  private static collectConditionKeys;
53
67
  static getActiveFilters<T>(meta: EntityMetadata<T>, options: FilterOptions | undefined, filters: Dictionary<FilterDef>): FilterDef[];
@@ -3,8 +3,8 @@ import { DANGEROUS_PROPERTY_NAMES, Utils } from './Utils.js';
3
3
  import { ARRAY_OPERATORS, GroupOperator, JSON_KEY_OPERATORS, ReferenceKind } from '../enums.js';
4
4
  import { JsonType } from '../types/JsonType.js';
5
5
  import { helper } from '../entity/wrap.js';
6
- import { isRaw, Raw } from './RawQueryFragment.js';
7
- import { MetadataError } from '../errors.js';
6
+ import { isRaw, isSubquery, Raw } from './RawQueryFragment.js';
7
+ import { MetadataError, ValidationError } from '../errors.js';
8
8
  /** @internal */
9
9
  export class QueryHelper {
10
10
  static SUPPORTED_OPERATORS = ['>', '<', '<=', '>=', '!', '!='];
@@ -102,10 +102,7 @@ export class QueryHelper {
102
102
  for (const k of keys) {
103
103
  const value = where[k];
104
104
  const prop = meta.properties[k];
105
- // Polymorphic relations use multiple columns (discriminator + FK), so they cannot
106
- // participate in the standard single-column FK expansion. Query by discriminator
107
- // column directly instead, e.g. { likeableType: 'post', likeableId: 1 }.
108
- if (!prop || ![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) || prop.polymorphic) {
105
+ if (!prop || ![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
109
106
  continue;
110
107
  }
111
108
  const op = this.liftGroupOperators(value, prop.targetMeta, metadata, k);
@@ -130,7 +127,7 @@ export class QueryHelper {
130
127
  if (Array.isArray(where)) {
131
128
  where.forEach((item, i) => {
132
129
  if (this.inlinePrimaryKeyObjects(item, meta, metadata, key)) {
133
- where[i] = Utils.getPrimaryKeyValues(item, meta, false);
130
+ where[i] = Utils.getPrimaryKeyValues(item, meta, true);
134
131
  }
135
132
  });
136
133
  }
@@ -160,6 +157,9 @@ export class QueryHelper {
160
157
  Object.keys(where).forEach(k => {
161
158
  const prop = meta.properties[QueryHelper.splitPolymorphicKey(k)[0]];
162
159
  const meta2 = metadata.find(QueryHelper.findTargetMeta(prop, k)?.class) || meta;
160
+ if (prop?.polymorphic) {
161
+ where[k] = this.inlinePolymorphicPrimaryKeys(where[k], meta2, prop);
162
+ }
163
163
  if (this.inlinePrimaryKeyObjects(where[k], meta2, metadata, k)) {
164
164
  // Skip the PK collapse when an owning M:1/1:1 relation's FK column count does not match
165
165
  // the target's PK column count (e.g. FK references target PK + an extra unique column).
@@ -177,6 +177,37 @@ export class QueryHelper {
177
177
  });
178
178
  return false;
179
179
  }
180
+ /**
181
+ * A bare value on a polymorphic relation would be compared to the discriminator column, so PK values are turned
182
+ * into a PK condition instead, e.g. `1` becomes `{ id: 1 }` and `[{ id: 1 }, { id: 2 }]` becomes `{ id: { $in: [1, 2] } }`.
183
+ * Null checks and `[type, id]` tuples keep their meaning.
184
+ */
185
+ static inlinePolymorphicPrimaryKeys(value, meta, prop) {
186
+ const pk = meta.primaryKeys[0];
187
+ const isScalar = (v) => v != null && !Array.isArray(v) && Utils.isPrimaryKey(v);
188
+ const isPrimaryKeyObject = (v) => Utils.isPlainObject(v) && Utils.getObjectKeysSize(v) === 1 && pk in v;
189
+ // lists of plain values are how `[type, id]` tuples get normalized (`{ $in: [type, id] }`), so only PK objects are inlined
190
+ const isList = (v) => Array.isArray(v) && v.length > 0 && v.every(isPrimaryKeyObject);
191
+ let cond;
192
+ if (isScalar(value)) {
193
+ cond = value;
194
+ }
195
+ else if (isList(value)) {
196
+ cond = { $in: value.map(v => v[pk]) };
197
+ }
198
+ else if (Utils.isPlainObject(value) &&
199
+ Utils.hasObjectKeys(value) &&
200
+ Object.entries(value).every(([op, v]) => Utils.isOperator(op) && !(op in GroupOperator) && op !== '$not' && (isScalar(v) || isList(v)))) {
201
+ cond = Object.fromEntries(Object.entries(value).map(([op, v]) => [op, isList(v) ? v.map(i => i[pk]) : v]));
202
+ }
203
+ else {
204
+ return value;
205
+ }
206
+ if (meta.compositePK) {
207
+ throw new ValidationError(`Polymorphic relation ${prop.name} targets entities with a composite primary key, use PK objects or entity references in the condition instead.`);
208
+ }
209
+ return { [pk]: cond };
210
+ }
180
211
  /** Copies the plain object and array structure of the condition, keeping the leaf values (e.g. entities) by reference. */
181
212
  static cloneWhere(where) {
182
213
  if (Array.isArray(where)) {
@@ -231,8 +262,10 @@ export class QueryHelper {
231
262
  if (!Utils.isPlainObject(where)) {
232
263
  return where;
233
264
  }
234
- if (meta && options.type !== 'orderBy') {
235
- where = QueryHelper.splitPolymorphicConditions(meta, where);
265
+ if (meta) {
266
+ where = (options.type === 'orderBy'
267
+ ? QueryHelper.splitPolymorphicOrderBy(meta, where)
268
+ : QueryHelper.splitPolymorphicConditions(meta, where));
236
269
  }
237
270
  return Utils.getObjectQueryKeys(where).reduce((o, key) => {
238
271
  let value = where[key];
@@ -280,6 +313,13 @@ export class QueryHelper {
280
313
  o[key] = { [op]: value };
281
314
  return o;
282
315
  }
316
+ // `$all` items are matched like separate `$some` payloads, so they need the same processing
317
+ if (key === '$all' && Array.isArray(value)) {
318
+ o[key] = value.map(item => Utils.isPlainObject(item)
319
+ ? QueryHelper.processWhere({ ...options, where: item, entityName, root: false })
320
+ : item);
321
+ return o;
322
+ }
283
323
  if (Utils.isPlainObject(value)) {
284
324
  const targetMeta = customExpression ? undefined : QueryHelper.findTargetMeta(prop, key);
285
325
  o[key] = QueryHelper.processWhere({
@@ -297,13 +337,18 @@ export class QueryHelper {
297
337
  }
298
338
  /** @internal Splits a polymorphic target key like `imageable[Article]` into the property and target class names. */
299
339
  static splitPolymorphicKey(key) {
300
- const match = /^(\w+)\[(\w+)]$/.exec(key);
340
+ const match = /^(\w+)\[([\w,]+)]$/.exec(key);
301
341
  return match ? [match[1], match[2]] : [key];
302
342
  }
343
+ /** @internal Resolves the targets of a polymorphic target key like `imageable[Article]` or `imageable[Product,Article]`. */
344
+ static findTargetMetas(prop, key) {
345
+ const [, targetNames] = QueryHelper.splitPolymorphicKey(key);
346
+ const targets = targetNames?.split(',').map(name => prop?.polymorphTargets?.find(t => t.className === name));
347
+ return targets?.every(Boolean) ? targets : [];
348
+ }
303
349
  /** @internal Resolves the target entity of a condition key, including polymorphic target keys like `imageable[Article]`. */
304
350
  static findTargetMeta(prop, key) {
305
- const [, targetName] = QueryHelper.splitPolymorphicKey(key);
306
- return targetName ? prop?.polymorphTargets?.find(t => t.className === targetName) : prop?.targetMeta;
351
+ return QueryHelper.splitPolymorphicKey(key)[1] ? QueryHelper.findTargetMetas(prop, key)[0] : prop?.targetMeta;
307
352
  }
308
353
  /**
309
354
  * Routes conditions on polymorphic to-one relations to the targets that define all the queried properties,
@@ -342,6 +387,34 @@ export class QueryHelper {
342
387
  }
343
388
  return ret;
344
389
  }
390
+ /**
391
+ * Routes ordering by properties of polymorphic to-one relations to the targets that define them, e.g.
392
+ * `{ imageable: { name: 'asc' } }` becomes `{ 'imageable[Product,Article]': { name: 'asc' } }`, ordered by
393
+ * the first non-null value. Nested relations and formulas are ordered via the first target defining them.
394
+ */
395
+ static splitPolymorphicOrderBy(meta, orderBy) {
396
+ const ret = {};
397
+ for (const key of Utils.getObjectQueryKeys(orderBy)) {
398
+ const prop = meta.properties[key];
399
+ const value = orderBy[key];
400
+ if (!prop?.polymorphTargets ||
401
+ ![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) ||
402
+ !Utils.isPlainObject(value)) {
403
+ ret[key] = value;
404
+ continue;
405
+ }
406
+ for (const field of Utils.getObjectQueryKeys(value)) {
407
+ const found = prop.polymorphTargets.filter(target => field in target.properties);
408
+ const sharedPK = found.length === prop.polymorphTargets.length && found.every(target => target.primaryKeys.includes(field));
409
+ const single = Utils.isPlainObject(value[field]) || found.some(target => target.properties[field].formula);
410
+ const targets = found.length === 0 ? [prop.targetMeta] : single ? [found[0]] : found;
411
+ // shared PKs are ordered by the FK column without a join
412
+ const targetKey = sharedPK ? key : `${key}[${targets.map(target => target.className).join(',')}]`;
413
+ ret[targetKey] = { ...ret[targetKey], [field]: value[field] };
414
+ }
415
+ }
416
+ return ret;
417
+ }
345
418
  /** Property names used in a condition, including those nested in group operators. */
346
419
  static collectConditionKeys(payload) {
347
420
  return Object.keys(payload).flatMap(k => {
@@ -480,7 +553,8 @@ export class QueryHelper {
480
553
  if (Array.isArray(cond) && !(key && ARRAY_OPERATORS.includes(key))) {
481
554
  return cond.map(v => QueryHelper.processCustomType(prop, v, platform, key, fromQuery));
482
555
  }
483
- if (isRaw(cond)) {
556
+ // a query builder renders as a sub-query, not as a value of the property type
557
+ if (isRaw(cond) || isSubquery(cond)) {
484
558
  return cond;
485
559
  }
486
560
  return prop.customType.convertToDatabaseValue(cond, platform, { fromQuery, key, mode: 'query' });
@@ -555,6 +629,9 @@ export class QueryHelper {
555
629
  if (expanded !== w) {
556
630
  where[k] = expanded;
557
631
  }
632
+ else if (Array.isArray(w)) {
633
+ where[k] = w.map(expandItem);
634
+ }
558
635
  else if (Utils.isPlainObject(w)) {
559
636
  for (const op of Object.keys(w)) {
560
637
  if (!Utils.isOperator(op, false)) {
@@ -1,11 +1,15 @@
1
- import type { AnyString, Dictionary, EntityKey } from '../typings.js';
1
+ import type { AnyString, Dictionary, EntityKey, Subquery } from '../typings.js';
2
2
  declare const rawFragmentSymbolBrand: unique symbol;
3
3
  /** Branded symbol type used as a unique key for tracking raw SQL fragments in object properties. */
4
4
  export type RawQueryFragmentSymbol = symbol & {
5
5
  readonly [rawFragmentSymbolBrand]: true;
6
6
  };
7
+ /** Prototype brand of query builders usable as sub-queries; a symbol so JSON payloads cannot carry it, `Symbol.for` so package copies agree. */
8
+ export declare const SUBQUERY_BRAND: unique symbol;
7
9
  /** Checks whether the given value is a `RawQueryFragment` instance. */
8
10
  export declare function isRaw(value: unknown): value is RawQueryFragment;
11
+ /** Checks whether the given value is a query builder usable as a sub-query (its prototype carries the `SUBQUERY_BRAND`). */
12
+ export declare function isSubquery(value: unknown): value is Subquery;
9
13
  /** Represents a raw SQL fragment with optional parameters, usable as both a value and an object key via Symbol coercion. */
10
14
  export declare class RawQueryFragment<Alias extends string = string> {
11
15
  #private;