@mikro-orm/core 7.1.0-dev.37 → 7.1.0-dev.38

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.
package/EntityManager.js CHANGED
@@ -1,4 +1,4 @@
1
- import { getOnConflictReturningFields, getWhereCondition } from './utils/upsert-utils.js';
1
+ import { getOnConflictReturningFields, getWhereCondition, resetUntouchedCollections } from './utils/upsert-utils.js';
2
2
  import { Utils } from './utils/Utils.js';
3
3
  import { Cursor } from './utils/Cursor.js';
4
4
  import { QueryHelper } from './utils/QueryHelper.js';
@@ -322,13 +322,13 @@ export class EntityManager {
322
322
  }
323
323
  const types = Object.values(meta.root.discriminatorMap).map(cls => this.metadata.get(cls));
324
324
  const children = [];
325
- const lookUpChildren = (ret, type) => {
326
- const children = types.filter(meta2 => meta2.extends === type);
327
- children.forEach(m => lookUpChildren(ret, m.class));
325
+ const lookUpChildren = (ret, parent) => {
326
+ const children = types.filter(meta2 => meta2.extends && this.metadata.find(meta2.extends) === parent);
327
+ children.forEach(m => lookUpChildren(ret, m));
328
328
  ret.push(...children.filter(c => c.discriminatorValue));
329
329
  return children;
330
330
  };
331
- lookUpChildren(children, meta.class);
331
+ lookUpChildren(children, meta);
332
332
  /* v8 ignore next */
333
333
  where[meta.root.discriminatorColumn] =
334
334
  children.length > 0
@@ -363,6 +363,13 @@ export class EntityManager {
363
363
  if (!joined && !hint.filter) {
364
364
  continue;
365
365
  }
366
+ // Polymorphic to-one relations are already filtered via per-target LEFT JOINs created
367
+ // for the `:ref filter` hint; emitting a populate-filter entry here would auto-join only
368
+ // the first target meta and either drop rows pointing to other targets or reference
369
+ // parent-table-only columns on the child sub-table alias for TPT children.
370
+ if (prop.polymorphic && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
371
+ continue;
372
+ }
366
373
  const filters = QueryHelper.mergePropertyFilters(prop.filters, options.filters);
367
374
  const where = await this.applyFilters(prop.targetMeta.class, {}, filters, 'read', {
368
375
  ...options,
@@ -448,7 +455,23 @@ export class EntityManager {
448
455
  const prop = meta?.properties[field];
449
456
  if (prop && !ref) {
450
457
  hint.children ??= [];
451
- await this.autoJoinRefsForFilters(prop.targetMeta, { ...options, populate: hint.children }, { class: meta.root.class, propName: prop.name });
458
+ const targets = prop.polymorphic && prop.polymorphTargets?.length ? prop.polymorphTargets : [prop.targetMeta];
459
+ for (const targetMeta of targets) {
460
+ const before = hint.children.length;
461
+ await this.autoJoinRefsForFilters(targetMeta, { ...options, populate: hint.children }, { class: meta.root.class, propName: prop.name });
462
+ // For polymorphic relations, `hint.children` is applied to every polymorph target during
463
+ // population, so drop any auto-added hint whose field does not exist on every target —
464
+ // otherwise the shared `:ref` would crash when applied to a target lacking it (GH #7722).
465
+ if (targets.length > 1 && hint.children.length > before) {
466
+ const added = hint.children.splice(before);
467
+ for (const h of added) {
468
+ const f = h.field.split(':')[0];
469
+ if (targets.every(t => f in t.properties)) {
470
+ hint.children.push(h);
471
+ }
472
+ }
473
+ }
474
+ }
452
475
  }
453
476
  }
454
477
  }
@@ -849,6 +872,7 @@ export class EntityManager {
849
872
  em.getHydrator().hydrate(entity, meta, data2, em.#entityFactory, 'full', false, true);
850
873
  }
851
874
  // recompute the data as there might be some values missing (e.g. those with db column defaults)
875
+ resetUntouchedCollections(meta, entity);
852
876
  const snapshot = this.#comparator.prepareEntity(entity);
853
877
  em.#unitOfWork.register(entity, snapshot, { refresh: true });
854
878
  if (em.eventManager.hasListeners(EventType.afterUpsert, meta)) {
@@ -1080,6 +1104,7 @@ export class EntityManager {
1080
1104
  }
1081
1105
  for (const [entity] of entities) {
1082
1106
  // recompute the data as there might be some values missing (e.g. those with db column defaults)
1107
+ resetUntouchedCollections(meta, entity);
1083
1108
  const snapshot = this.#comparator.prepareEntity(entity);
1084
1109
  em.#unitOfWork.register(entity, snapshot, { refresh: true });
1085
1110
  }
@@ -1872,9 +1897,13 @@ export class EntityManager {
1872
1897
  return [];
1873
1898
  }
1874
1899
  const meta = this.metadata.find(entityName);
1875
- // infer populate hints from `fields` when present; merge with explicit `populate` if both are set
1900
+ // infer populate hints from `fields` when present; merge with explicit `populate` if both are set.
1901
+ // `populateArray` is only kept when the entries are user-provided strings — when this method runs a
1902
+ // second time (e.g. from `lockAndPopulate`) the populate is already normalized and merging would be a no-op.
1876
1903
  const fields = options.fields ? this.buildFields(options.fields) : undefined;
1877
- const populateArray = Array.isArray(options.populate) ? options.populate : undefined;
1904
+ const populateArray = Array.isArray(options.populate) && options.populate.every(p => typeof p === 'string')
1905
+ ? options.populate
1906
+ : undefined;
1878
1907
  const hasNestedFields = fields?.some(f => f.includes('.')) ?? false;
1879
1908
  const shouldInfer = fields !== undefined && options.populate === undefined;
1880
1909
  const shouldMerge = fields !== undefined && populateArray !== undefined && populateArray.length > 0 && hasNestedFields;
@@ -1918,8 +1947,11 @@ export class EntityManager {
1918
1947
  }
1919
1948
  else {
1920
1949
  // with an explicit populate, only nested paths (e.g. `item.id`) need to be merged in — they
1921
- // require a JOIN to access sub-fields, whereas bare names are already handled by the driver
1922
- const extras = fromFields.filter(f => f.includes('.'));
1950
+ // require a JOIN to access sub-fields, whereas bare names are already handled by the driver.
1951
+ // skip paths already in user's `populate` (regardless of `:ref` modifier) to avoid producing
1952
+ // both a `:ref` and a non-`:ref` entry for the same relation
1953
+ const existing = new Set(populateArray.map(p => p.split(':')[0]));
1954
+ const extras = fromFields.filter(f => f.includes('.') && !existing.has(f));
1923
1955
  options.populate = [...populateArray, ...extras];
1924
1956
  }
1925
1957
  }
@@ -319,7 +319,7 @@ export class EntityLoader {
319
319
  }
320
320
  }
321
321
  async findChildren(entities, prop, populate, options, ref) {
322
- const children = Utils.unique(this.getChildReferences(entities, prop, options, ref));
322
+ const children = Utils.unique(this.getChildReferences(entities, prop, options, ref, populate));
323
323
  const meta = prop.targetMeta;
324
324
  // When targetKey is set, use it for FK lookup instead of the PK
325
325
  let fk = prop.targetKey ?? Utils.getPrimaryKeyHash(meta.primaryKeys);
@@ -337,10 +337,10 @@ export class EntityLoader {
337
337
  fk = ownerProp.name;
338
338
  }
339
339
  }
340
- if (prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner && !ref) {
340
+ if (prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner && (!ref || (!prop.mapToPk && !populate.filter))) {
341
341
  children.length = 0;
342
342
  fk = meta.properties[prop.mappedBy].name;
343
- children.push(...this.filterByReferences(entities, prop.name, options.refresh));
343
+ children.push(...this.filterByReferences(entities, prop, options, ref));
344
344
  }
345
345
  if (children.length === 0) {
346
346
  return { items: [], partial };
@@ -707,7 +707,7 @@ export class EntityLoader {
707
707
  }
708
708
  return ret;
709
709
  }
710
- getChildReferences(entities, prop, options, ref) {
710
+ getChildReferences(entities, prop, options, ref, populate) {
711
711
  const filtered = this.filterCollections(entities, prop.name, options, ref);
712
712
  if (prop.kind === ReferenceKind.ONE_TO_MANY) {
713
713
  return filtered.map(e => e[prop.name].owner);
@@ -723,7 +723,7 @@ export class EntityLoader {
723
723
  return filtered;
724
724
  }
725
725
  // MANY_TO_ONE or ONE_TO_ONE
726
- return this.filterReferences(entities, prop.name, options, ref);
726
+ return this.filterReferences(entities, prop.name, options, ref, populate);
727
727
  }
728
728
  filterCollections(entities, field, options, ref) {
729
729
  if (options.refresh) {
@@ -749,7 +749,7 @@ export class EntityLoader {
749
749
  }
750
750
  return this.isPropertyLoaded(entity[f], r.join('.'));
751
751
  }
752
- filterReferences(entities, field, options, ref) {
752
+ filterReferences(entities, field, options, ref, populate) {
753
753
  if (ref) {
754
754
  return [];
755
755
  }
@@ -758,13 +758,19 @@ export class EntityLoader {
758
758
  return children.map(e => Reference.unwrapReference(e[field]));
759
759
  }
760
760
  if (options.fields) {
761
+ // `:ref` populate children are satisfied by FK/PK and will be loaded lazily — they don't force a parent reload
762
+ const refChildren = new Set((populate?.children ?? [])
763
+ .map(c => c.field.split(':'))
764
+ .filter(parts => parts[1] === 'ref')
765
+ .map(parts => parts[0]));
761
766
  return children
762
767
  .map(e => Reference.unwrapReference(e[field]))
763
768
  .filter(target => {
764
769
  const wrapped = helper(target);
765
770
  const childFields = options.fields
766
771
  .filter(f => f.startsWith(`${field}.`))
767
- .map(f => f.substring(field.length + 1));
772
+ .map(f => f.substring(field.length + 1))
773
+ .filter(cf => !refChildren.has(cf.split('.')[0]));
768
774
  return !wrapped.__initialized || !childFields.every(cf => this.isPropertyLoaded(target, cf));
769
775
  });
770
776
  }
@@ -772,12 +778,39 @@ export class EntityLoader {
772
778
  .filter(e => !e[field].__helper.__initialized)
773
779
  .map(e => Reference.unwrapReference(e[field]));
774
780
  }
775
- filterByReferences(entities, field, refresh) {
781
+ filterByReferences(entities, prop, options, ref) {
776
782
  /* v8 ignore next */
777
- if (refresh) {
783
+ if (options.refresh) {
778
784
  return entities;
779
785
  }
780
- return entities.filter(e => e[field] !== null && !e[field]?.__helper?.__initialized);
786
+ const field = prop.name;
787
+ return entities.filter(e => {
788
+ const value = e[field];
789
+ if (value === null) {
790
+ return false;
791
+ }
792
+ if (value === undefined || !Utils.isEntity(value, true)) {
793
+ return true;
794
+ }
795
+ const target = Reference.unwrapReference(value);
796
+ const wrapped = helper(target);
797
+ if (!wrapped.__initialized) {
798
+ return true;
799
+ }
800
+ if (ref) {
801
+ return false;
802
+ }
803
+ if (options.fields) {
804
+ const childFields = options.fields
805
+ .filter(f => f.startsWith(`${prop.name}.`))
806
+ .map(f => f.substring(prop.name.length + 1));
807
+ return childFields.length > 0 && !childFields.every(f => this.isPropertyLoaded(target, f));
808
+ }
809
+ return prop.targetMeta.comparableProps.some(targetProp => {
810
+ const inlineEmbedded = targetProp.kind === ReferenceKind.EMBEDDED && !targetProp.object;
811
+ return !inlineEmbedded && !targetProp.lazy && !wrapped.__loadedProperties.has(targetProp.name);
812
+ });
813
+ });
781
814
  }
782
815
  lookupAllRelationships(entityName) {
783
816
  const ret = [];
@@ -605,6 +605,10 @@ export class MetadataDiscovery {
605
605
  prop.pivotEntity = pivotMeta.class;
606
606
  if (prop.inversedBy) {
607
607
  prop.targetMeta.properties[prop.inversedBy].pivotEntity = pivotMeta.class;
608
+ const targetRoot = prop.targetMeta.root;
609
+ if (targetRoot !== prop.targetMeta && targetRoot.properties[prop.inversedBy]) {
610
+ targetRoot.properties[prop.inversedBy].pivotEntity = pivotMeta.class;
611
+ }
608
612
  }
609
613
  // Propagate pivotEntity to ALL inverse collections using mappedBy pointing at this
610
614
  // owner prop. Covers three cases:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/core",
3
- "version": "7.1.0-dev.37",
3
+ "version": "7.1.0-dev.38",
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/utils/Utils.js CHANGED
@@ -141,7 +141,7 @@ export function parseJsonSafe(value) {
141
141
  /** Collection of general-purpose utility methods used throughout the ORM. */
142
142
  export class Utils {
143
143
  static PK_SEPARATOR = '~~~';
144
- static #ORM_VERSION = '7.1.0-dev.37';
144
+ static #ORM_VERSION = '7.1.0-dev.38';
145
145
  /**
146
146
  * Checks if the argument is instance of `Object`. Returns false for arrays.
147
147
  */
@@ -10,3 +10,5 @@ export declare function getWhereCondition<T extends object>(meta: EntityMetadata
10
10
  where: FilterQuery<T>;
11
11
  propIndex: number | false;
12
12
  };
13
+ /** @internal */
14
+ export declare function resetUntouchedCollections<Entity extends object>(meta: EntityMetadata<Entity>, entity: Entity): void;
@@ -1,5 +1,8 @@
1
1
  import { isRaw } from '../utils/RawQueryFragment.js';
2
2
  import { Utils } from './Utils.js';
3
+ import { ReferenceKind } from '../enums.js';
4
+ import { Collection } from '../entity/Collection.js';
5
+ import { helper } from '../entity/wrap.js';
3
6
  function expandEmbeddedProperties(prop, key) {
4
7
  if (prop.object) {
5
8
  return [prop.name];
@@ -52,7 +55,9 @@ export function getOnConflictFields(meta, data, uniqueFields, options) {
52
55
  });
53
56
  }
54
57
  const keys = Object.keys(data).flatMap(f => {
55
- if (!(Array.isArray(uniqueFields) && !uniqueFields.includes(f))) {
58
+ // skip explicitly listed unique fields; for raw onConflictFields we can't introspect the
59
+ // fragment, so merge all data keys (the user is responsible for not corrupting them).
60
+ if (Array.isArray(uniqueFields) && uniqueFields.includes(f)) {
56
61
  return [];
57
62
  }
58
63
  const prop = meta?.properties[f];
@@ -142,3 +147,23 @@ export function getWhereCondition(meta, onConflictFields, data, where) {
142
147
  }
143
148
  return { where, propIndex };
144
149
  }
150
+ /** @internal */
151
+ export function resetUntouchedCollections(meta, entity) {
152
+ // for entities passed in via `em.create()` and then upserted, collection-kind relations
153
+ // were initialized as empty-but-initialized by the hydrator (newEntity=true). once the
154
+ // upsert resolves the entity to a possibly existing row, that state is stale and would
155
+ // cause `em.populate()` to skip those collections. replace them with a fresh
156
+ // uninitialized collection so populate can load them.
157
+ if (helper(entity).__originalEntityData) {
158
+ return;
159
+ }
160
+ for (const prop of meta.relations) {
161
+ if (prop.kind !== ReferenceKind.MANY_TO_MANY && prop.kind !== ReferenceKind.ONE_TO_MANY) {
162
+ continue;
163
+ }
164
+ const collection = entity[prop.name];
165
+ if (Utils.isCollection(collection) && collection.isInitialized() && !collection.isDirty()) {
166
+ Collection.create(entity, prop.name, undefined, false);
167
+ }
168
+ }
169
+ }