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

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.
@@ -190,7 +190,7 @@ export class EntityFactory {
190
190
  const initialized = options.initialized || helper(entity).__initialized;
191
191
  this.hydrate(entity, meta, diff2, initialized ? { ...options, initialized } : options);
192
192
  // we need to update the entity data only with keys that were not present before
193
- const nullVal = this.#config.get('forceUndefined') ? undefined : null;
193
+ const nullVal = (meta.forceUndefined ?? this.#config.get('forceUndefined')) ? undefined : null;
194
194
  Utils.keys(diff2).forEach(key => {
195
195
  const prop = meta.properties[key];
196
196
  if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) &&
@@ -336,7 +336,7 @@ export class EntityFactory {
336
336
  prop.default == null &&
337
337
  !prop.defaultRaw &&
338
338
  entity[prop.name] === undefined) {
339
- entity[prop.name] = (this.#config.get('forceUndefined') ? undefined : null);
339
+ entity[prop.name] = ((meta.forceUndefined ?? this.#config.get('forceUndefined')) ? undefined : null);
340
340
  }
341
341
  if (prop.kind === ReferenceKind.EMBEDDED && entity[prop.name]) {
342
342
  const items = prop.array ? entity[prop.name] : [entity[prop.name]];
@@ -458,7 +458,8 @@ export class EntityLoader {
458
458
  if ([ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind) &&
459
459
  items.length !== children.length &&
460
460
  Utils.isEmpty(options.where)) {
461
- const nullVal = this.#em.config.get('forceUndefined') ? undefined : null;
461
+ const ownerMeta = this.#metadata.find(prop.entity());
462
+ const nullVal = (ownerMeta.forceUndefined ?? this.#em.config.get('forceUndefined')) ? undefined : null;
462
463
  const itemsMap = new Set();
463
464
  const childrenMap = new Set();
464
465
  // `e` may be an unresolved reference here, so read the targetKey value directly instead of
@@ -631,7 +631,7 @@ type PartialWhere<TProperties, TBase> = string | FilterQuery<{
631
631
  [K in AllKeys<TProperties, TBase> & string]?: unknown;
632
632
  }>;
633
633
  /** Metadata descriptor for `defineEntity()`, combining entity options with property definitions. */
634
- export interface EntityMetadataWithProperties<TName extends string, TTableName extends string, TProperties extends Record<string, any>, TPK extends (keyof TProperties)[] | undefined = undefined, TBase = never, TRepository = never, TForceObject extends boolean = false, TDiscriminatorColumn extends string | undefined = undefined, TDiscriminatorValue extends string | number | undefined = undefined, TBaseDiscriminatorColumn extends string | undefined = undefined, TEmbeddable extends boolean = false> extends Omit<Partial<EntityMetadata<InferEntityFromProperties<TProperties, TPK, TBase, TRepository>>>, 'properties' | 'extends' | 'primaryKeys' | 'hooks' | 'discriminator' | 'discriminatorColumn' | 'discriminatorValue' | 'versionProperty' | 'concurrencyCheckKeys' | 'serializedPrimaryKey' | 'indexes' | 'uniques' | 'triggers' | 'repository' | 'filters' | 'orderBy'> {
634
+ export interface EntityMetadataWithProperties<TName extends string, TTableName extends string, TProperties extends Record<string, any>, TPK extends (keyof TProperties)[] | undefined = undefined, TBase = never, TRepository = never, TForceObject extends boolean = false, TDiscriminatorColumn extends string | undefined = undefined, TDiscriminatorValue extends string | number | undefined = undefined, TBaseDiscriminatorColumn extends string | undefined = undefined, TEmbeddable extends boolean = false, TForceUndefined extends boolean = false> extends Omit<Partial<EntityMetadata<InferEntityFromProperties<TProperties, TPK, TBase, TRepository>>>, 'properties' | 'extends' | 'primaryKeys' | 'hooks' | 'discriminator' | 'discriminatorColumn' | 'discriminatorValue' | 'versionProperty' | 'concurrencyCheckKeys' | 'serializedPrimaryKey' | 'indexes' | 'uniques' | 'triggers' | 'repository' | 'filters' | 'orderBy'> {
635
635
  name: TName;
636
636
  tableName?: TTableName;
637
637
  extends?: {
@@ -656,6 +656,8 @@ export interface EntityMetadataWithProperties<TName extends string, TTableName e
656
656
  };
657
657
  }>;
658
658
  forceObject?: TForceObject;
659
+ /** Hydrate `null` as `undefined` for this entity and infer nullable properties as `T | undefined`. */
660
+ forceUndefined?: TForceUndefined;
659
661
  embeddable?: TEmbeddable;
660
662
  inheritance?: 'tpt';
661
663
  orderBy?: {
@@ -707,7 +709,7 @@ export interface EntityMetadataWithProperties<TName extends string, TTableName e
707
709
  }[];
708
710
  }
709
711
  /** Defines an entity schema using property builders, with full type inference from the property definitions. */
710
- export declare function defineEntity<const TName extends string, const TTableName extends string, const TProperties extends Record<string, any>, const TPK extends (keyof TProperties)[] | undefined = undefined, const TBase = never, const TRepository = never, const TForceObject extends boolean = false, const TDiscriminatorColumn extends string | undefined = undefined, const TDiscriminatorValue extends string | number | undefined = undefined, const TBaseDiscriminatorColumn extends string | undefined = undefined, const TEmbeddable extends boolean = false>(meta: EntityMetadataWithProperties<TName, TTableName, TProperties, TPK, TBase, TRepository, TForceObject, TDiscriminatorColumn, TDiscriminatorValue, TBaseDiscriminatorColumn, TEmbeddable>): EntitySchemaWithMeta<TName, TTableName, InferEntityFromProperties<TProperties, TPK, TBase, TRepository, TForceObject, TBaseDiscriminatorColumn, TDiscriminatorValue, TEmbeddable>, TBase, TProperties, EntityCtor<InferEntityFromProperties<TProperties, TPK, TBase, TRepository, TForceObject, TBaseDiscriminatorColumn, TDiscriminatorValue, TEmbeddable>>, TDiscriminatorColumn>;
712
+ export declare function defineEntity<const TName extends string, const TTableName extends string, const TProperties extends Record<string, any>, const TPK extends (keyof TProperties)[] | undefined = undefined, const TBase = never, const TRepository = never, const TForceObject extends boolean = false, const TDiscriminatorColumn extends string | undefined = undefined, const TDiscriminatorValue extends string | number | undefined = undefined, const TBaseDiscriminatorColumn extends string | undefined = undefined, const TEmbeddable extends boolean = false, const TForceUndefined extends boolean = false>(meta: EntityMetadataWithProperties<TName, TTableName, TProperties, TPK, TBase, TRepository, TForceObject, TDiscriminatorColumn, TDiscriminatorValue, TBaseDiscriminatorColumn, TEmbeddable, TForceUndefined>): EntitySchemaWithMeta<TName, TTableName, MaybeForceUndefined<InferEntityFromProperties<TProperties, TPK, TBase, TRepository, TForceObject, TBaseDiscriminatorColumn, TDiscriminatorValue, TEmbeddable, TForceUndefined>, EffectiveForceUndefined<TForceUndefined, TBase>>, TBase, TProperties, EntityCtor<MaybeForceUndefined<InferEntityFromProperties<TProperties, TPK, TBase, TRepository, TForceObject, TBaseDiscriminatorColumn, TDiscriminatorValue, TEmbeddable, TForceUndefined>, EffectiveForceUndefined<TForceUndefined, TBase>>>, TDiscriminatorColumn>;
711
713
  export declare namespace defineEntity {
712
714
  export { propertyBuilders as properties };
713
715
  }
@@ -744,8 +746,17 @@ type InferColumnType<T extends string> = T extends 'int' | 'int4' | 'integer' |
744
746
  type BaseEntityMethodKeys = 'toObject' | 'toPOJO' | 'serialize' | 'assign' | 'populate' | 'init' | 'toReference';
745
747
  interface BaseEntityMethods<in out Entity extends object> extends Pick<IWrappedEntity<Entity>, BaseEntityMethodKeys> {
746
748
  }
749
+ type MaybeForceUndefined<Entity, ForceUndefined> = [ForceUndefined] extends [true] ? {
750
+ [K in keyof Entity]: SwapNull<Entity[K]>;
751
+ } : Entity;
752
+ type SwapNull<Value> = [Value] extends [ScalarReference<infer Inner>] ? [Extract<keyof Value, symbol>] extends [never] ? ScalarReference<SwapNull<Inner>> : ScalarReference<SwapNull<Inner>> & Pick<Value, Extract<keyof Value, symbol>> : null extends Value ? Exclude<Value, null> | undefined : Value;
753
+ type EffectiveForceUndefined<Flag, Base> = [Flag] extends [true] ? true : IsNever<Base> extends true ? false : [Base] extends [{
754
+ [Config]?: infer C;
755
+ }] ? [NonNullable<C>] extends [{
756
+ forceUndefined: true;
757
+ }] ? true : false : false;
747
758
  /** Infers the entity type from a `defineEntity()` properties map, resolving builders, base classes, and primary keys. */
748
- export type InferEntityFromProperties<Properties extends Record<string, any>, PK extends (keyof Properties)[] | undefined = undefined, Base = never, Repository = never, ForceObject extends boolean = false, BaseDiscriminatorColumn extends string | undefined = undefined, DiscriminatorValue extends string | number | undefined = undefined, Embeddable extends boolean = false> = (IsNever<Base> extends true ? {} : Base extends {
759
+ export type InferEntityFromProperties<Properties extends Record<string, any>, PK extends (keyof Properties)[] | undefined = undefined, Base = never, Repository = never, ForceObject extends boolean = false, BaseDiscriminatorColumn extends string | undefined = undefined, DiscriminatorValue extends string | number | undefined = undefined, Embeddable extends boolean = false, ForceUndefined extends boolean = false> = (IsNever<Base> extends true ? {} : Base extends {
749
760
  toObject(...args: any[]): any;
750
761
  } ? BaseEntityMethods<{
751
762
  -readonly [K in keyof Properties]: InferBuilderValue<MaybeReturnType<Properties[K]>>;
@@ -763,6 +774,10 @@ export type InferEntityFromProperties<Properties extends Record<string, any>, PK
763
774
  [Config]?: DefineConfig<{
764
775
  forceObject: true;
765
776
  }>;
777
+ } : {}) & (ForceUndefined extends true ? {
778
+ [Config]?: DefineConfig<{
779
+ forceUndefined: true;
780
+ }>;
766
781
  } : {}) & {
767
782
  [IndexHints]?: [Omit<ExtractBaseProperties<Base>, keyof Properties> & Properties];
768
783
  };
@@ -50,6 +50,8 @@ export class ObjectHydrator extends Hydrator {
50
50
  const lines = [];
51
51
  const context = new Map();
52
52
  const props = this.getProperties(meta, type);
53
+ const forceUndefined = meta.forceUndefined ?? this.config.get('forceUndefined');
54
+ const nullVal = forceUndefined ? 'undefined' : 'null';
53
55
  context.set('isPrimaryKey', Utils.isPrimaryKey);
54
56
  context.set('isEntity', EntityHelper.isEntity);
55
57
  context.set('isScalarReference', ScalarReference.isScalarReference);
@@ -76,7 +78,6 @@ export class ObjectHydrator extends Hydrator {
76
78
  .join('_');
77
79
  const ret = [];
78
80
  const idx = this.#tmpIndex++;
79
- const nullVal = this.config.get('forceUndefined') ? 'undefined' : 'null';
80
81
  if (prop.getter && !prop.setter && prop.persist === false) {
81
82
  return [];
82
83
  }
@@ -142,7 +143,6 @@ export class ObjectHydrator extends Hydrator {
142
143
  const hydrateToOne = (prop, dataKey, entityKey) => {
143
144
  const ret = [];
144
145
  const method = type === 'reference' ? 'createReference' : 'create';
145
- const nullVal = this.config.get('forceUndefined') ? 'undefined' : 'null';
146
146
  ret.push(` if (data${dataKey} === null) {\n entity${entityKey} = ${nullVal};`);
147
147
  ret.push(` } else if (typeof data${dataKey} !== 'undefined') {`);
148
148
  // For polymorphic: instanceof check; for regular: isPrimaryKey() check
@@ -341,8 +341,6 @@ export class ObjectHydrator extends Hydrator {
341
341
  ...hydrateProperty(childProp, prop.object, [...path, childProp.embedded[1]], childDataKey).map(l => ' ' + l));
342
342
  });
343
343
  }
344
- /* v8 ignore next */
345
- const nullVal = this.config.get('forceUndefined') ? 'undefined' : 'null';
346
344
  if (prop.object) {
347
345
  ret.push(` } else if (data${dataKey} === null) {`);
348
346
  }
@@ -365,8 +363,7 @@ export class ObjectHydrator extends Hydrator {
365
363
  ret.push(...hydrateEmbedded(prop, [...path, `[idx_${idx}]`], `${dataKey}[idx_${idx}]`).map(l => ' ' + l));
366
364
  ret.push(` });`);
367
365
  ret.push(` } else if (data${dataKey} === null) {`);
368
- /* v8 ignore next */
369
- ret.push(` entity${entityKey} = ${this.config.get('forceUndefined') ? 'undefined' : 'null'};`);
366
+ ret.push(` entity${entityKey} = ${nullVal};`);
370
367
  ret.push(` }`);
371
368
  return ret;
372
369
  };
@@ -396,7 +393,7 @@ export class ObjectHydrator extends Hydrator {
396
393
  // ReferenceKind.SCALAR
397
394
  ret.push(...hydrateScalar(prop, path, dataKey));
398
395
  }
399
- if (this.config.get('forceUndefined')) {
396
+ if (forceUndefined) {
400
397
  ret.push(` if (data${dataKey} === null) entity${entityKey} = undefined;`);
401
398
  }
402
399
  return ret;
@@ -547,7 +547,7 @@ export class MetadataDiscovery {
547
547
  const fieldNames1 = prop.targetMeta.getPrimaryProps().flatMap(pk => pk.fieldNames);
548
548
  const idColumns = fieldNames1.map(fieldName => this.#namingStrategy.joinKeyColumnName(prop.discriminator, fieldName, fieldNames1.length > 1));
549
549
  prop.fieldNames ??= [prop.discriminatorColumn, ...idColumns];
550
- prop.joinColumns ??= idColumns;
550
+ prop.joinColumns ??= prop.fieldNames.slice(1);
551
551
  prop.referencedColumnNames ??= fieldNames1;
552
552
  prop.referencedTableName ??= prop.targetMeta.tableName;
553
553
  return;
@@ -1062,6 +1062,7 @@ export class MetadataDiscovery {
1062
1062
  });
1063
1063
  ownProps.forEach(prop => (meta.properties[prop.name] = prop));
1064
1064
  meta.filters = { ...base.filters, ...meta.filters };
1065
+ meta.forceUndefined ??= base.forceUndefined;
1065
1066
  if (!meta.discriminatorValue) {
1066
1067
  Object.values(base.properties)
1067
1068
  .filter(prop => !old.includes(prop.name))
@@ -1174,12 +1175,24 @@ export class MetadataDiscovery {
1174
1175
  if (!prop.discriminator && !prop.discriminatorColumn && !prop.discriminatorMap && !Array.isArray(prop.target)) {
1175
1176
  return;
1176
1177
  }
1178
+ const isToOne = [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind);
1179
+ const isUnionTargetMN = prop.kind === ReferenceKind.MANY_TO_MANY && Array.isArray(prop.target);
1177
1180
  prop.polymorphic = true;
1178
1181
  prop.discriminator ??= prop.name;
1182
+ // explicit `fieldNames` use the `[discriminatorColumn, ...fkIdColumns]` layout
1183
+ if (isToOne && prop.fieldNames && prop.fieldNames.length > 1) {
1184
+ prop.discriminatorColumn ??= prop.fieldNames[0];
1185
+ // `EntitySchema` mirrors `fieldNames` into `joinColumns`, let `initManyToOneFields` derive them after embedded prefixing
1186
+ if (Utils.equals(prop.joinColumns, prop.fieldNames)) {
1187
+ delete prop.joinColumns;
1188
+ }
1189
+ }
1179
1190
  prop.discriminatorColumn ??= this.#namingStrategy.discriminatorColumnName(prop.discriminator);
1180
1191
  prop.createForeignKeyConstraint = false;
1181
- const isToOne = [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind);
1182
- const isUnionTargetMN = prop.kind === ReferenceKind.MANY_TO_MANY && Array.isArray(prop.target);
1192
+ // a single field name can only be the FK column, e.g. mirrored from `joinColumn`
1193
+ if (isToOne && prop.fieldNames?.length === 1) {
1194
+ prop.fieldNames = [prop.discriminatorColumn, ...prop.fieldNames];
1195
+ }
1183
1196
  if (isToOne || isUnionTargetMN) {
1184
1197
  const types = prop.type.split(/ ?\| ?/);
1185
1198
  prop.polymorphTargets = discovered.filter(m => types.includes(m.className) && !m.embeddable);
@@ -85,6 +85,8 @@ export type EntityOptions<T, E = T extends EntityClass<infer P> ? P : T> = {
85
85
  abstract?: boolean;
86
86
  /** Disables change tracking - such entities are ignored during flush. */
87
87
  readonly?: boolean;
88
+ /** Hydrate `null` values as `undefined` for this entity, overriding the global `forceUndefined` option. */
89
+ forceUndefined?: boolean;
88
90
  /** Marks entity as {@doclink virtual-entities | virtual}. This is set automatically when you use `expression` option (unless `view` is set). */
89
91
  virtual?: boolean;
90
92
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/core",
3
- "version": "7.3.0-dev.0",
3
+ "version": "7.3.0-dev.2",
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
@@ -246,6 +246,8 @@ export type CleanTypeConfig<T> = Compute<Pick<T, Extract<keyof T, keyof TypeConf
246
246
  /** Configuration options that can be set on an entity via the `[Config]` symbol. */
247
247
  export interface TypeConfig {
248
248
  forceObject?: boolean;
249
+ /** Type-level marker of the `forceUndefined` entity option: `defineEntity` infers nullable properties as `T | undefined` instead of `T | null | undefined`. */
250
+ forceUndefined?: boolean;
249
251
  }
250
252
  declare const __selectedType: unique symbol;
251
253
  declare const __loadedType: unique symbol;
@@ -1223,6 +1225,8 @@ export interface EntityMetadata<Entity = any, Class extends EntityCtor<Entity> =
1223
1225
  selfReferencing?: boolean;
1224
1226
  hasUniqueProps?: boolean;
1225
1227
  readonly?: boolean;
1228
+ /** Per-entity override of the global `forceUndefined` option. */
1229
+ forceUndefined?: boolean;
1226
1230
  polymorphs?: EntityMetadata[];
1227
1231
  root: EntityMetadata<Entity>;
1228
1232
  definedProperties: Dictionary;
@@ -79,7 +79,7 @@ export class UnitOfWork {
79
79
  * @internal
80
80
  */
81
81
  normalizeEntityData(meta, data) {
82
- const forceUndefined = this.#em.config.get('forceUndefined');
82
+ const forceUndefined = meta.forceUndefined ?? this.#em.config.get('forceUndefined');
83
83
  for (const key of Utils.keys(data)) {
84
84
  const prop = meta.properties[key];
85
85
  if (!prop) {
@@ -38,6 +38,18 @@ export declare class QueryHelper {
38
38
  /** Copies the plain object and array structure of the condition, keeping the leaf values (e.g. entities) by reference. */
39
39
  private static cloneWhere;
40
40
  static processWhere<T extends object>(options: ProcessWhereOptions<T>): FilterQuery<T>;
41
+ /** @internal Splits a polymorphic target key like `imageable[Article]` into the property and target class names. */
42
+ static splitPolymorphicKey(key: string): [string, string?];
43
+ /** @internal Resolves the target entity of a condition key, including polymorphic target keys like `imageable[Article]`. */
44
+ static findTargetMeta(prop: EntityProperty | null | undefined, key: string): EntityMetadata | undefined;
45
+ /**
46
+ * Routes conditions on polymorphic to-one relations to the targets that define all the queried properties,
47
+ * e.g. `{ imageable: { title } }` becomes `{ 'imageable[Article]': { title } }`, or an `$or` of such keys
48
+ * when more targets match. The first target alone keeps the regular relation join.
49
+ */
50
+ private static splitPolymorphicConditions;
51
+ /** Property names used in a condition, including those nested in group operators. */
52
+ private static collectConditionKeys;
41
53
  static getActiveFilters<T>(meta: EntityMetadata<T>, options: FilterOptions | undefined, filters: Dictionary<FilterDef>): FilterDef[];
42
54
  /** @internal Sentinel wrapping for arguments accessed while statically resolving an `rls` filter condition. */
43
55
  static readonly RLS_SENTINEL_PREFIX = "__mikro_rls_arg__";
@@ -158,17 +158,18 @@ export class QueryHelper {
158
158
  })));
159
159
  }
160
160
  Object.keys(where).forEach(k => {
161
- const prop = meta.properties[k];
162
- const meta2 = metadata.find(prop?.targetMeta?.class) || meta;
161
+ const prop = meta.properties[QueryHelper.splitPolymorphicKey(k)[0]];
162
+ const meta2 = metadata.find(QueryHelper.findTargetMeta(prop, k)?.class) || meta;
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).
166
166
  // The criteria layer would otherwise emit a malformed predicate such as
167
167
  // `(joinCol1, joinCol2) = scalar`. Limited to owning relations — `joinColumns` on the
168
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.
169
170
  if (prop?.owner &&
170
171
  [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) &&
171
- prop.joinColumns.length !== meta2.primaryKeys.length) {
172
+ (prop.polymorphic || prop.joinColumns.length !== meta2.primaryKeys.length)) {
172
173
  return;
173
174
  }
174
175
  where[k] = Utils.getPrimaryKeyValues(where[k], meta2, true);
@@ -230,6 +231,9 @@ export class QueryHelper {
230
231
  if (!Utils.isPlainObject(where)) {
231
232
  return where;
232
233
  }
234
+ if (meta && options.type !== 'orderBy') {
235
+ where = QueryHelper.splitPolymorphicConditions(meta, where);
236
+ }
233
237
  return Utils.getObjectQueryKeys(where).reduce((o, key) => {
234
238
  let value = where[key];
235
239
  const customExpression = Raw.isKnownFragmentSymbol(key);
@@ -277,10 +281,11 @@ export class QueryHelper {
277
281
  return o;
278
282
  }
279
283
  if (Utils.isPlainObject(value)) {
284
+ const targetMeta = customExpression ? undefined : QueryHelper.findTargetMeta(prop, key);
280
285
  o[key] = QueryHelper.processWhere({
281
286
  ...options,
282
287
  where: value,
283
- entityName: prop?.targetMeta?.class ?? entityName,
288
+ entityName: targetMeta?.class ?? entityName,
284
289
  root: false,
285
290
  });
286
291
  }
@@ -290,6 +295,62 @@ export class QueryHelper {
290
295
  return o;
291
296
  }, {});
292
297
  }
298
+ /** @internal Splits a polymorphic target key like `imageable[Article]` into the property and target class names. */
299
+ static splitPolymorphicKey(key) {
300
+ const match = /^(\w+)\[(\w+)]$/.exec(key);
301
+ return match ? [match[1], match[2]] : [key];
302
+ }
303
+ /** @internal Resolves the target entity of a condition key, including polymorphic target keys like `imageable[Article]`. */
304
+ static findTargetMeta(prop, key) {
305
+ const [, targetName] = QueryHelper.splitPolymorphicKey(key);
306
+ return targetName ? prop?.polymorphTargets?.find(t => t.className === targetName) : prop?.targetMeta;
307
+ }
308
+ /**
309
+ * Routes conditions on polymorphic to-one relations to the targets that define all the queried properties,
310
+ * e.g. `{ imageable: { title } }` becomes `{ 'imageable[Article]': { title } }`, or an `$or` of such keys
311
+ * when more targets match. The first target alone keeps the regular relation join.
312
+ */
313
+ static splitPolymorphicConditions(meta, payload) {
314
+ let ret = payload;
315
+ for (const key of Object.keys(payload)) {
316
+ const prop = meta.properties[key];
317
+ const value = payload[key];
318
+ if (!prop?.polymorphTargets ||
319
+ ![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) ||
320
+ !Utils.isPlainObject(value)) {
321
+ continue;
322
+ }
323
+ const keys = this.collectConditionKeys(value);
324
+ const targets = prop.polymorphTargets.filter(target => keys.every(k => k in target.properties));
325
+ const primaryKeys = keys.every(k => targets.every(target => target.primaryKeys.includes(k)));
326
+ // PK conditions compare the FK columns without checking the type, so only when every target defines them
327
+ if (keys.length === 0 ||
328
+ targets.length === 0 ||
329
+ (primaryKeys && targets.length === prop.polymorphTargets.length) ||
330
+ (!primaryKeys && targets.length === 1 && targets[0] === prop.targetMeta)) {
331
+ continue;
332
+ }
333
+ const branches = targets.map(target => ({ [`${key}[${target.className}]`]: value }));
334
+ ret = { ...ret };
335
+ delete ret[key];
336
+ if (branches.length === 1) {
337
+ Object.assign(ret, branches[0]);
338
+ }
339
+ else {
340
+ ret.$and = [...(ret.$and ?? []), { $or: branches }];
341
+ }
342
+ }
343
+ return ret;
344
+ }
345
+ /** Property names used in a condition, including those nested in group operators. */
346
+ static collectConditionKeys(payload) {
347
+ return Object.keys(payload).flatMap(k => {
348
+ if (k in GroupOperator || k === '$not') {
349
+ return Utils.asArray(payload[k]).flatMap(item => Utils.isPlainObject(item) ? this.collectConditionKeys(item) : []);
350
+ }
351
+ return Utils.isOperator(k) ? [] : [k];
352
+ });
353
+ }
293
354
  static getActiveFilters(meta, options, filters) {
294
355
  if (options === false) {
295
356
  return [];
@@ -450,7 +511,7 @@ export class QueryHelper {
450
511
  }
451
512
  static findProperty(fieldName, options) {
452
513
  const parts = fieldName.split('.');
453
- const propName = parts.pop();
514
+ const propName = QueryHelper.splitPolymorphicKey(parts.pop())[0];
454
515
  const alias = parts.length > 0 ? parts.join('.') : undefined;
455
516
  const entityName = alias ? options.aliasMap?.[alias] : options.entityName;
456
517
  const meta = entityName ? options.metadata.find(entityName) : undefined;
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.0';
156
+ static #ORM_VERSION = '7.3.0-dev.2';
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}`;