@mikro-orm/core 7.2.2-dev.3 → 7.2.2-dev.30

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.
@@ -175,7 +175,7 @@ export interface PropertyOptions<Owner> {
175
175
  */
176
176
  autoincrement?: boolean;
177
177
  /**
178
- * Add the property to the `returning` statement.
178
+ * Add the property to the `returning` clause of inserts, updates and upserts, reloading the database value into the entity.
179
179
  */
180
180
  returning?: boolean;
181
181
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/core",
3
- "version": "7.2.2-dev.3",
3
+ "version": "7.2.2-dev.30",
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
@@ -1173,6 +1173,7 @@ export interface EntityMetadata<Entity = any, Class extends EntityCtor<Entity> =
1173
1173
  meta: EntityMetadata<Entity>;
1174
1174
  prop: EntityProperty<Entity>;
1175
1175
  }[];
1176
+ targetKeys?: EntityKey<Entity>[];
1176
1177
  comparableProps: EntityProperty<Entity>[];
1177
1178
  trackingProps: EntityProperty<Entity>[];
1178
1179
  hydrateProps: EntityProperty<Entity>[];
@@ -1254,6 +1255,8 @@ export interface EntityMetadata<Entity = any, Class extends EntityCtor<Entity> =
1254
1255
  orderBy?: QueryOrderMap<Entity> | QueryOrderMap<Entity>[];
1255
1256
  /** @internal can be used for computed numeric cache keys */
1256
1257
  readonly _id: number;
1258
+ /** @internal name that does not depend on module evaluation order (unlike `_id`), used to key `compiledFunctions` */
1259
+ compiledName?: string;
1257
1260
  }
1258
1261
  /** Options for `ISchemaGenerator.create()`. */
1259
1262
  export interface CreateSchemaOptions {
@@ -7,6 +7,7 @@ import { Reference } from '../entity/Reference.js';
7
7
  import { PolymorphicRef } from '../entity/PolymorphicRef.js';
8
8
  import { ReferenceKind } from '../enums.js';
9
9
  import { isRaw } from '../utils/RawQueryFragment.js';
10
+ import { ValidationError } from '../errors.js';
10
11
  /** @internal Computes change sets by comparing entity state against original snapshots. */
11
12
  export class ChangeSetComputer {
12
13
  #comparator;
@@ -48,6 +49,10 @@ export class ChangeSetComputer {
48
49
  if (Utils.equals(data, wrapped.__originalEntityData)) {
49
50
  return null;
50
51
  }
52
+ // a reference known only by its `targetKey` has no PK, so the update would have no condition
53
+ if (!wrapped.hasPrimaryKey()) {
54
+ throw ValidationError.referenceWithoutPK(entity);
55
+ }
51
56
  }
52
57
  const changeSet = new ChangeSet(entity, type, this.computePayload(entity), meta);
53
58
  changeSet.originalEntity = wrapped.__originalEntityData;
@@ -162,21 +167,30 @@ export class ChangeSetComputer {
162
167
  const target = Reference.unwrapReference(rawTarget);
163
168
  const needsProcessing = target != null && (prop.targetKey != null || !target.__helper.hasPrimaryKey());
164
169
  if (needsProcessing) {
170
+ // a reference known only by its `targetKey` has no PK, and is never inserted to generate one
171
+ if (!prop.targetKey && !target.__helper.__initialized) {
172
+ throw ValidationError.referenceWithoutPK(target);
173
+ }
165
174
  let value = prop.targetKey ? target[prop.targetKey] : target.__helper.__identifier;
166
- /* v8 ignore next */
167
- if (prop.targetKey && prop.targetMeta) {
168
- const targetProp = prop.targetMeta.properties[prop.targetKey];
175
+ // use the actual target's meta, a polymorphic relation has several
176
+ if (prop.targetKey) {
177
+ const targetProp = target.__meta.properties[prop.targetKey];
169
178
  if (targetProp?.customType) {
170
179
  value = targetProp.customType.convertToDatabaseValue(value, this.#platform, { mode: 'serialization' });
171
180
  }
172
181
  }
173
182
  if (prop.polymorphic) {
174
183
  const discriminator = QueryHelper.findDiscriminatorValue(prop.discriminatorMap, target.constructor);
175
- Utils.setPayloadProperty(changeSet.payload, changeSet.meta, prop, new PolymorphicRef(discriminator, value), idx);
184
+ value = new PolymorphicRef(discriminator, value);
176
185
  }
177
- else {
178
- Utils.setPayloadProperty(changeSet.payload, changeSet.meta, prop, value, idx);
186
+ // an unchanged `targetKey` FK is not part of the diff
187
+ if (prop.targetKey &&
188
+ changeSet.type === ChangeSetType.UPDATE &&
189
+ !(prop.name in changeSet.payload) &&
190
+ Utils.equals(changeSet.originalEntity?.[prop.name], value)) {
191
+ return;
179
192
  }
193
+ Utils.setPayloadProperty(changeSet.payload, changeSet.meta, prop, value, idx);
180
194
  }
181
195
  });
182
196
  }
@@ -207,7 +207,7 @@ export class ChangeSetPersister {
207
207
  const keys = meta.getOwnConcurrencyCheckKeys();
208
208
  for (const key of keys) {
209
209
  cond[key] = changeSet.originalEntity[key];
210
- if (changeSet.payload[key]) {
210
+ if (Object.hasOwn(changeSet.payload, key)) {
211
211
  tmp.push(key);
212
212
  }
213
213
  }
@@ -322,17 +322,19 @@ export class ChangeSetPersister {
322
322
  }
323
323
  // skip entity references as they don't have version values loaded
324
324
  changeSets = changeSets.filter(cs => helper(cs.entity).__initialized);
325
- const primaryKeys = meta.primaryKeys.concat(...concurrencyCheckKeys);
325
+ const fields = meta.primaryKeys.concat(...concurrencyCheckKeys);
326
326
  const $or = changeSets.map(cs => {
327
- const cond = Utils.getPrimaryKeyCond(cs.originalEntity, primaryKeys);
327
+ const cond = Utils.getPrimaryKeyCond(cs.originalEntity, meta.primaryKeys);
328
+ for (const key of concurrencyCheckKeys) {
329
+ cond[key] = cs.originalEntity[key];
330
+ }
328
331
  if (meta.ownsVersionProperty()) {
329
- // @ts-ignore
330
332
  cond[meta.versionProperty] = this.#platform.convertVersionValue(cs.entity[meta.versionProperty], meta.properties[meta.versionProperty]);
331
333
  }
332
334
  return cond;
333
335
  });
334
336
  options = this.prepareOptions(meta, options, {
335
- fields: primaryKeys,
337
+ fields,
336
338
  orderBy: meta.primaryKeys.reduce((o, pk) => {
337
339
  o[pk] = 'asc';
338
340
  return o;
@@ -344,8 +346,8 @@ export class ChangeSetPersister {
344
346
  if (res.length !== changeSets.length) {
345
347
  // a FK pointing to a composite PK is an array, so the values need to be compared deeply
346
348
  const compare = (a, b, keys) => keys.every(k => equals(a[k], b[k]));
347
- const entity = changeSets.find(cs => {
348
- return !res.some(row => compare(Utils.getPrimaryKeyCond(cs.entity, primaryKeys), row, primaryKeys));
349
+ const entity = changeSets.find((_, index) => {
350
+ return !res.some(row => compare($or[index], row, meta.primaryKeys));
349
351
  }).entity;
350
352
  throw OptimisticLockError.lockFailed(entity);
351
353
  }
@@ -1,9 +1,9 @@
1
1
  import type { AnyEntity, EntityMetadata } from '../typings.js';
2
- /** @internal Stores managed entity instances keyed by their primary key hash, ensuring each row is loaded once. */
2
+ /** @internal Stores managed entity instances keyed by their primary key hash (and `targetKey` values), ensuring each row is loaded once. */
3
3
  export declare class IdentityMap {
4
4
  #private;
5
5
  constructor(defaultSchema?: string);
6
- /** Stores an entity in the identity map under its primary key hash. */
6
+ /** Stores an entity in the identity map under its primary key hash and the `targetKey` values referencing it. */
7
7
  store<T>(item: T): void;
8
8
  /**
9
9
  * Stores an entity under an alternate key (non-PK property).
@@ -25,6 +25,7 @@ export declare class IdentityMap {
25
25
  /** Returns all entities currently in the identity map. */
26
26
  values(): AnyEntity[];
27
27
  [Symbol.iterator](): IterableIterator<AnyEntity>;
28
+ private getEntities;
28
29
  /** Returns all hash keys currently in the identity map. */
29
30
  keys(): string[];
30
31
  /**
@@ -1,15 +1,29 @@
1
- /** @internal Stores managed entity instances keyed by their primary key hash, ensuring each row is loaded once. */
1
+ /** @internal Stores managed entity instances keyed by their primary key hash (and `targetKey` values), ensuring each row is loaded once. */
2
2
  export class IdentityMap {
3
3
  #defaultSchema;
4
4
  #registry = new Map();
5
5
  /** Tracks alternate key hashes for each entity so we can clean them up on delete */
6
6
  #alternateKeys = new WeakMap();
7
+ #storesWithAlternateKeys = new WeakSet();
7
8
  constructor(defaultSchema) {
8
9
  this.#defaultSchema = defaultSchema;
9
10
  }
10
- /** Stores an entity in the identity map under its primary key hash. */
11
+ /** Stores an entity in the identity map under its primary key hash and the `targetKey` values referencing it. */
11
12
  store(item) {
12
- this.getStore(item.__meta.root).set(this.getPkHash(item), item);
13
+ const meta = item.__meta.root;
14
+ const wrapped = item.__helper;
15
+ // PK-less references (known only by a `targetKey`) would share one slot that outlives their later removal
16
+ if (!meta.targetKeys || wrapped.hasPrimaryKey()) {
17
+ this.getStore(meta).set(this.getPkHash(item), item);
18
+ }
19
+ // references resolved via `targetKey` look the entity up by that key instead of the PK
20
+ for (const key of meta.targetKeys ?? []) {
21
+ const value = item[key];
22
+ if (value != null) {
23
+ const schema = wrapped.__schema ?? meta.schema ?? this.#defaultSchema;
24
+ this.storeByKey(item, key, '' + value, schema);
25
+ }
26
+ }
13
27
  }
14
28
  /**
15
29
  * Stores an entity under an alternate key (non-PK property).
@@ -17,7 +31,9 @@ export class IdentityMap {
17
31
  */
18
32
  storeByKey(item, key, value, schema) {
19
33
  const hash = this.getKeyHash(key, value, schema);
20
- this.getStore(item.__meta.root).set(hash, item);
34
+ const store = this.getStore(item.__meta.root);
35
+ store.set(hash, item);
36
+ this.#storesWithAlternateKeys.add(store);
21
37
  // Track this alternate key so we can clean it up when the entity is deleted
22
38
  let keys = this.#alternateKeys.get(item);
23
39
  if (!keys) {
@@ -35,7 +51,10 @@ export class IdentityMap {
35
51
  const altKeys = this.#alternateKeys.get(item);
36
52
  if (altKeys) {
37
53
  for (const hash of altKeys) {
38
- store.delete(hash);
54
+ // the hash might be owned by another entity by now, e.g. after swapping unique values
55
+ if (store.get(hash) === item) {
56
+ store.delete(hash);
57
+ }
39
58
  }
40
59
  this.#alternateKeys.delete(item);
41
60
  }
@@ -69,7 +88,7 @@ export class IdentityMap {
69
88
  values() {
70
89
  const ret = [];
71
90
  for (const store of this.#registry.values()) {
72
- for (const item of store.values()) {
91
+ for (const item of this.getEntities(store)) {
73
92
  ret.push(item);
74
93
  }
75
94
  }
@@ -77,11 +96,15 @@ export class IdentityMap {
77
96
  }
78
97
  *[Symbol.iterator]() {
79
98
  for (const store of this.#registry.values()) {
80
- for (const item of store.values()) {
99
+ for (const item of this.getEntities(store)) {
81
100
  yield item;
82
101
  }
83
102
  }
84
103
  }
104
+ getEntities(store) {
105
+ // an entity indexed by alternate keys occupies several slots of its store
106
+ return this.#storesWithAlternateKeys.has(store) ? new Set(store.values()) : store.values();
107
+ }
85
108
  /** Returns all hash keys currently in the identity map. */
86
109
  keys() {
87
110
  const ret = [];
@@ -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);
@@ -467,7 +485,10 @@ export class UnitOfWork {
467
485
  }
468
486
  const groups = this.getChangeSetGroups();
469
487
  const platform = this.#em.getPlatform();
470
- const runInTransaction = !this.#em.isInTransaction() && platform.supportsTransactions() && this.#em.config.get('implicitTransactions');
488
+ const runInTransaction = !this.#em.isInTransaction() &&
489
+ !this.#em.isTransactionDisabled() &&
490
+ platform.supportsTransactions() &&
491
+ this.#em.config.get('implicitTransactions');
471
492
  if (runInTransaction) {
472
493
  const loggerContext = Utils.merge({ id: this.#em._id }, this.#em.getLoggerContext({ disableContextResolution: true }));
473
494
  await this.#em.getConnection('write').transactional(trx => this.persistToDatabase(groups, trx), {
@@ -1115,6 +1136,11 @@ export class UnitOfWork {
1115
1136
  for (const changeSet of changeSets) {
1116
1137
  const wrapped = helper(changeSet.entity);
1117
1138
  wrapped.__originalEntityData = this.#comparator.prepareEntity(changeSet.entity);
1139
+ // re-index so the entity is found by its new `targetKey` values instead of the old ones
1140
+ if (changeSet.meta.root.targetKeys?.some(key => key in changeSet.payload)) {
1141
+ this.#identityMap.delete(changeSet.entity);
1142
+ this.#identityMap.store(changeSet.entity);
1143
+ }
1118
1144
  if (!wrapped.__initialized) {
1119
1145
  for (const prop of changeSet.meta.relations) {
1120
1146
  if ([ReferenceKind.MANY_TO_MANY, ReferenceKind.ONE_TO_MANY].includes(prop.kind) &&
@@ -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;
@@ -608,9 +621,24 @@ export class EntityComparator {
608
621
  ret += ` if (discriminator !== undefined) break;\n`;
609
622
  ret += ` __cls${level} = Object.getPrototypeOf(__cls${level});\n`;
610
623
  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`;
624
+ if (prop.targetKey) {
625
+ // the FK holds the `targetKey` value in the format of the actual target's column
626
+ const targetKey = prop.targetKey;
627
+ const targetKeyValueKey = `targetKeyValue_${this.safeKey(prop.name)}`;
628
+ context.set(targetKeyValueKey, (val) => {
629
+ const customType = val.__meta.properties[targetKey]?.customType;
630
+ const value = val[targetKey];
631
+ return customType
632
+ ? customType.convertToDatabaseValue(value, this.#platform, { mode: 'serialization' })
633
+ : value;
634
+ });
635
+ ret += ` const pk = ${targetKeyValueKey}(val${level});\n`;
636
+ }
637
+ else {
638
+ ret += ` const pk = val${level}?.__helper?.__identifier && !val${level}?.__helper?.hasPrimaryKey()\n`;
639
+ ret += ` ? val${level}.__helper.__identifier\n`;
640
+ ret += ` : toArray(val${level}?.__helper?.getPrimaryKey(true));\n`;
641
+ }
614
642
  ret += ` ret${dataKey} = new PolymorphicRef(discriminator, pk);\n`;
615
643
  ret += ` }\n`;
616
644
  }
@@ -624,8 +652,8 @@ export class EntityComparator {
624
652
  ret += ` } else if (typeof entity${entityKey} !== 'undefined') {\n`;
625
653
  ret += ` const val${level} = entity${entityKey}${unwrap};\n`;
626
654
  if (targetProp?.customType) {
627
- // If targetKey property has a custom type, convert to database value
628
- const convertorKey = this.registerCustomType(targetProp, context);
655
+ // 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`
656
+ const convertorKey = this.registerCustomType({ ...prop, customType: targetProp.customType }, context);
629
657
  ret += ` ret${dataKey} = convertToDatabaseValue_${convertorKey}(val${level}${targetAccess});\n`;
630
658
  }
631
659
  else {
@@ -688,7 +716,7 @@ export class EntityComparator {
688
716
  lines.push(`}`);
689
717
  const code = `// compiled comparator for entity ${meta.className}\n` +
690
718
  `return function(last, current, options) {\n const diff = {};\n${lines.join('\n')}\n return diff;\n}`;
691
- const fnKey = `comparator-${meta.uniqueName}`;
719
+ const fnKey = `comparator-${meta.compiledName}`;
692
720
  const comparator = Utils.createFunction(context, code, this.#config?.get('compiledFunctions'), fnKey);
693
721
  this.#comparators.set(meta, comparator);
694
722
  return comparator;
@@ -35,6 +35,12 @@ 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>;
@@ -4,7 +4,7 @@ import { ARRAY_OPERATORS, GroupOperator, JSON_KEY_OPERATORS, ReferenceKind } fro
4
4
  import { JsonType } from '../types/JsonType.js';
5
5
  import { helper } from '../entity/wrap.js';
6
6
  import { isRaw, Raw } from './RawQueryFragment.js';
7
- import { MetadataError } from '../errors.js';
7
+ import { MetadataError, ValidationError } from '../errors.js';
8
8
  /** @internal */
9
9
  export class QueryHelper {
10
10
  static SUPPORTED_OPERATORS = ['>', '<', '<=', '>=', '!', '!='];
@@ -102,18 +102,23 @@ 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);
112
109
  if (op) {
113
110
  delete where[k];
114
- where[op] = value[op].map((v) => {
111
+ const group = value[op].map((v) => {
115
112
  return { [k]: v };
116
113
  });
114
+ if (op in where) {
115
+ // Sibling groups are conjunctive, even when both use `$or`.
116
+ where.$and ??= [];
117
+ where.$and.push({ [op]: group });
118
+ }
119
+ else {
120
+ where[op] = group;
121
+ }
117
122
  }
118
123
  }
119
124
  return undefined;
@@ -122,7 +127,7 @@ export class QueryHelper {
122
127
  if (Array.isArray(where)) {
123
128
  where.forEach((item, i) => {
124
129
  if (this.inlinePrimaryKeyObjects(item, meta, metadata, key)) {
125
- where[i] = Utils.getPrimaryKeyValues(item, meta, false);
130
+ where[i] = Utils.getPrimaryKeyValues(item, meta, true);
126
131
  }
127
132
  });
128
133
  }
@@ -152,15 +157,19 @@ export class QueryHelper {
152
157
  Object.keys(where).forEach(k => {
153
158
  const prop = meta.properties[k];
154
159
  const meta2 = metadata.find(prop?.targetMeta?.class) || meta;
160
+ if (prop?.polymorphic) {
161
+ where[k] = this.inlinePolymorphicPrimaryKeys(where[k], meta2, prop);
162
+ }
155
163
  if (this.inlinePrimaryKeyObjects(where[k], meta2, metadata, k)) {
156
164
  // Skip the PK collapse when an owning M:1/1:1 relation's FK column count does not match
157
165
  // the target's PK column count (e.g. FK references target PK + an extra unique column).
158
166
  // The criteria layer would otherwise emit a malformed predicate such as
159
167
  // `(joinCol1, joinCol2) = scalar`. Limited to owning relations — `joinColumns` on the
160
168
  // inverse side (1:m) describes the owning entity's FK columns, not the LHS tuple.
169
+ // Polymorphic relations are skipped too, a scalar would be compared to the discriminator column.
161
170
  if (prop?.owner &&
162
171
  [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) &&
163
- prop.joinColumns.length !== meta2.primaryKeys.length) {
172
+ (prop.polymorphic || prop.joinColumns.length !== meta2.primaryKeys.length)) {
164
173
  return;
165
174
  }
166
175
  where[k] = Utils.getPrimaryKeyValues(where[k], meta2, true);
@@ -168,6 +177,37 @@ export class QueryHelper {
168
177
  });
169
178
  return false;
170
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
+ }
171
211
  /** Copies the plain object and array structure of the condition, keeping the leaf values (e.g. entities) by reference. */
172
212
  static cloneWhere(where) {
173
213
  if (Array.isArray(where)) {
@@ -486,6 +526,9 @@ export class QueryHelper {
486
526
  if (expanded !== w) {
487
527
  where[k] = expanded;
488
528
  }
529
+ else if (Array.isArray(w)) {
530
+ where[k] = w.map(expandItem);
531
+ }
489
532
  else if (Utils.isPlainObject(w)) {
490
533
  for (const op of Object.keys(w)) {
491
534
  if (!Utils.isOperator(op, false)) {
@@ -1,6 +1,7 @@
1
1
  import type { EntityManager } from '../EntityManager.js';
2
2
  /** Uses `AsyncLocalStorage` to maintain a transaction-scoped EntityManager context across async operations. */
3
3
  export declare class TransactionContext {
4
+ #private;
4
5
  readonly em: EntityManager;
5
6
  private static storage;
6
7
  readonly id: number;
@@ -1,8 +1,10 @@
1
+ var _a;
1
2
  import { createAsyncContext } from './AsyncContext.js';
2
3
  /** Uses `AsyncLocalStorage` to maintain a transaction-scoped EntityManager context across async operations. */
3
4
  export class TransactionContext {
4
5
  em;
5
6
  static storage = createAsyncContext();
7
+ #parent = _a.currentTransactionContext();
6
8
  id;
7
9
  constructor(em) {
8
10
  this.em = em;
@@ -12,7 +14,7 @@ export class TransactionContext {
12
14
  * Creates new TransactionContext instance and runs the code inside its domain.
13
15
  */
14
16
  static create(em, next) {
15
- const context = new TransactionContext(em);
17
+ const context = new _a(em);
16
18
  return this.storage.run(context, next);
17
19
  }
18
20
  /**
@@ -25,7 +27,11 @@ export class TransactionContext {
25
27
  * Returns current EntityManager (if available).
26
28
  */
27
29
  static getEntityManager(name = 'default') {
28
- const context = TransactionContext.currentTransactionContext();
29
- return context?.em.name === name ? context.em : undefined;
30
+ let context = _a.currentTransactionContext();
31
+ while (context && context.em.name !== name) {
32
+ context = context.#parent;
33
+ }
34
+ return context?.em;
30
35
  }
31
36
  }
37
+ _a = TransactionContext;
@@ -81,10 +81,10 @@ export class TransactionManager {
81
81
  */
82
82
  async executeWithoutTransaction(em, cb, options) {
83
83
  const suspended = this.suspendTransaction(em);
84
- const fork = this.createFork(em, { ...options, disableTransactions: true });
84
+ const fork = this.createFork(em, options);
85
85
  const propagateToUpperContext = this.shouldPropagateToUpperContext(em);
86
86
  try {
87
- return await this.executeTransactionFlow(fork, cb, propagateToUpperContext, em);
87
+ return await TransactionContext.create(fork, () => this.executeTransactionFlow(fork, cb, propagateToUpperContext, em));
88
88
  }
89
89
  finally {
90
90
  this.resumeTransaction(em, suspended);
@@ -189,6 +189,10 @@ export class TransactionManager {
189
189
  if (wrapped.__originalEntityData) {
190
190
  parentWrapped.__originalEntityData = originalEntityData;
191
191
  }
192
+ // index a merged `targetKey` value, `getByKey` ignores the old one as stale
193
+ if (meta.root.targetKeys) {
194
+ parentUoW.getIdentityMap().store(parentEntity);
195
+ }
192
196
  }
193
197
  else {
194
198
  parentUoW.merge(entity, new Set([entity]));