@mikro-orm/core 7.0.17-dev.0 → 7.0.17-dev.10

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
@@ -319,13 +319,13 @@ export class EntityManager {
319
319
  }
320
320
  const types = Object.values(meta.root.discriminatorMap).map(cls => this.metadata.get(cls));
321
321
  const children = [];
322
- const lookUpChildren = (ret, type) => {
323
- const children = types.filter(meta2 => meta2.extends === type);
324
- children.forEach(m => lookUpChildren(ret, m.class));
322
+ const lookUpChildren = (ret, parent) => {
323
+ const children = types.filter(meta2 => meta2.extends && this.metadata.find(meta2.extends) === parent);
324
+ children.forEach(m => lookUpChildren(ret, m));
325
325
  ret.push(...children.filter(c => c.discriminatorValue));
326
326
  return children;
327
327
  };
328
- lookUpChildren(children, meta.class);
328
+ lookUpChildren(children, meta);
329
329
  /* v8 ignore next */
330
330
  where[meta.root.discriminatorColumn] =
331
331
  children.length > 0
@@ -452,7 +452,23 @@ export class EntityManager {
452
452
  const prop = meta?.properties[field];
453
453
  if (prop && !ref) {
454
454
  hint.children ??= [];
455
- await this.autoJoinRefsForFilters(prop.targetMeta, { ...options, populate: hint.children }, { class: meta.root.class, propName: prop.name });
455
+ const targets = prop.polymorphic && prop.polymorphTargets?.length ? prop.polymorphTargets : [prop.targetMeta];
456
+ for (const targetMeta of targets) {
457
+ const before = hint.children.length;
458
+ await this.autoJoinRefsForFilters(targetMeta, { ...options, populate: hint.children }, { class: meta.root.class, propName: prop.name });
459
+ // For polymorphic relations, `hint.children` is applied to every polymorph target during
460
+ // population, so drop any auto-added hint whose field does not exist on every target —
461
+ // otherwise the shared `:ref` would crash when applied to a target lacking it (GH #7722).
462
+ if (targets.length > 1 && hint.children.length > before) {
463
+ const added = hint.children.splice(before);
464
+ for (const h of added) {
465
+ const f = h.field.split(':')[0];
466
+ if (targets.every(t => f in t.properties)) {
467
+ hint.children.push(h);
468
+ }
469
+ }
470
+ }
471
+ }
456
472
  }
457
473
  }
458
474
  }
@@ -1727,9 +1743,13 @@ export class EntityManager {
1727
1743
  return [];
1728
1744
  }
1729
1745
  const meta = this.metadata.find(entityName);
1730
- // infer populate hints from `fields` when present; merge with explicit `populate` if both are set
1746
+ // infer populate hints from `fields` when present; merge with explicit `populate` if both are set.
1747
+ // `populateArray` is only kept when the entries are user-provided strings — when this method runs a
1748
+ // second time (e.g. from `lockAndPopulate`) the populate is already normalized and merging would be a no-op.
1731
1749
  const fields = options.fields ? this.buildFields(options.fields) : undefined;
1732
- const populateArray = Array.isArray(options.populate) ? options.populate : undefined;
1750
+ const populateArray = Array.isArray(options.populate) && options.populate.every(p => typeof p === 'string')
1751
+ ? options.populate
1752
+ : undefined;
1733
1753
  const hasNestedFields = fields?.some(f => f.includes('.')) ?? false;
1734
1754
  const shouldInfer = fields !== undefined && options.populate === undefined;
1735
1755
  const shouldMerge = fields !== undefined && populateArray !== undefined && populateArray.length > 0 && hasNestedFields;
@@ -1773,8 +1793,11 @@ export class EntityManager {
1773
1793
  }
1774
1794
  else {
1775
1795
  // with an explicit populate, only nested paths (e.g. `item.id`) need to be merged in — they
1776
- // require a JOIN to access sub-fields, whereas bare names are already handled by the driver
1777
- const extras = fromFields.filter(f => f.includes('.'));
1796
+ // require a JOIN to access sub-fields, whereas bare names are already handled by the driver.
1797
+ // skip paths already in user's `populate` (regardless of `:ref` modifier) to avoid producing
1798
+ // both a `:ref` and a non-`:ref` entry for the same relation
1799
+ const existing = new Set(populateArray.map(p => p.split(':')[0]));
1800
+ const extras = fromFields.filter(f => f.includes('.') && !existing.has(f));
1778
1801
  options.populate = [...populateArray, ...extras];
1779
1802
  }
1780
1803
  }
@@ -161,13 +161,15 @@ export class EntityFactory {
161
161
  return diff2[key] === undefined;
162
162
  })
163
163
  .forEach(key => delete diff2[key]);
164
- // but always add collection properties, formulas, generated columns, and version properties if they
165
- // are part of the `data`, as these are excluded from `comparableProps` and won't appear in the diff
164
+ // but always add collection properties, formulas, generated columns, version properties, and the
165
+ // user-declared STI discriminator if they are part of the `data`, as these are excluded from
166
+ // `comparableProps` and won't appear in the diff
166
167
  Utils.keys(data)
167
168
  .filter(key => meta.properties[key]?.formula ||
168
169
  meta.properties[key]?.version ||
169
170
  (meta.properties[key]?.generated && !meta.properties[key]?.primary) ||
170
- [ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(meta.properties[key]?.kind))
171
+ [ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(meta.properties[key]?.kind) ||
172
+ (key === meta.root.discriminatorColumn && meta.properties[key]?.userDefined !== false))
171
173
  .forEach(key => (diff2[key] = data[key]));
172
174
  // rehydrated with the new values, skip those changed by user
173
175
  // use full hydration if the entity is already initialized, even if the caller used `initialized: false`
@@ -316,7 +316,7 @@ export class EntityLoader {
316
316
  }
317
317
  }
318
318
  async findChildren(entities, prop, populate, options, ref) {
319
- const children = Utils.unique(this.getChildReferences(entities, prop, options, ref));
319
+ const children = Utils.unique(this.getChildReferences(entities, prop, options, ref, populate));
320
320
  const meta = prop.targetMeta;
321
321
  // When targetKey is set, use it for FK lookup instead of the PK
322
322
  let fk = prop.targetKey ?? Utils.getPrimaryKeyHash(meta.primaryKeys);
@@ -334,10 +334,10 @@ export class EntityLoader {
334
334
  fk = ownerProp.name;
335
335
  }
336
336
  }
337
- if (prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner && !ref) {
337
+ if (prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner && (!ref || (!prop.mapToPk && !populate.filter))) {
338
338
  children.length = 0;
339
339
  fk = meta.properties[prop.mappedBy].name;
340
- children.push(...this.filterByReferences(entities, prop.name, options.refresh));
340
+ children.push(...this.filterByReferences(entities, prop, options, ref));
341
341
  }
342
342
  if (children.length === 0) {
343
343
  return { items: [], partial };
@@ -682,7 +682,7 @@ export class EntityLoader {
682
682
  }
683
683
  return ret;
684
684
  }
685
- getChildReferences(entities, prop, options, ref) {
685
+ getChildReferences(entities, prop, options, ref, populate) {
686
686
  const filtered = this.filterCollections(entities, prop.name, options, ref);
687
687
  if (prop.kind === ReferenceKind.ONE_TO_MANY) {
688
688
  return filtered.map(e => e[prop.name].owner);
@@ -698,7 +698,7 @@ export class EntityLoader {
698
698
  return filtered;
699
699
  }
700
700
  // MANY_TO_ONE or ONE_TO_ONE
701
- return this.filterReferences(entities, prop.name, options, ref);
701
+ return this.filterReferences(entities, prop.name, options, ref, populate);
702
702
  }
703
703
  filterCollections(entities, field, options, ref) {
704
704
  if (options.refresh) {
@@ -724,7 +724,7 @@ export class EntityLoader {
724
724
  }
725
725
  return this.isPropertyLoaded(entity[f], r.join('.'));
726
726
  }
727
- filterReferences(entities, field, options, ref) {
727
+ filterReferences(entities, field, options, ref, populate) {
728
728
  if (ref) {
729
729
  return [];
730
730
  }
@@ -733,13 +733,19 @@ export class EntityLoader {
733
733
  return children.map(e => Reference.unwrapReference(e[field]));
734
734
  }
735
735
  if (options.fields) {
736
+ // `:ref` populate children are satisfied by FK/PK and will be loaded lazily — they don't force a parent reload
737
+ const refChildren = new Set((populate?.children ?? [])
738
+ .map(c => c.field.split(':'))
739
+ .filter(parts => parts[1] === 'ref')
740
+ .map(parts => parts[0]));
736
741
  return children
737
742
  .map(e => Reference.unwrapReference(e[field]))
738
743
  .filter(target => {
739
744
  const wrapped = helper(target);
740
745
  const childFields = options.fields
741
746
  .filter(f => f.startsWith(`${field}.`))
742
- .map(f => f.substring(field.length + 1));
747
+ .map(f => f.substring(field.length + 1))
748
+ .filter(cf => !refChildren.has(cf.split('.')[0]));
743
749
  return !wrapped.__initialized || !childFields.every(cf => this.isPropertyLoaded(target, cf));
744
750
  });
745
751
  }
@@ -747,12 +753,39 @@ export class EntityLoader {
747
753
  .filter(e => !e[field].__helper.__initialized)
748
754
  .map(e => Reference.unwrapReference(e[field]));
749
755
  }
750
- filterByReferences(entities, field, refresh) {
756
+ filterByReferences(entities, prop, options, ref) {
751
757
  /* v8 ignore next */
752
- if (refresh) {
758
+ if (options.refresh) {
753
759
  return entities;
754
760
  }
755
- return entities.filter(e => e[field] !== null && !e[field]?.__helper?.__initialized);
761
+ const field = prop.name;
762
+ return entities.filter(e => {
763
+ const value = e[field];
764
+ if (value === null) {
765
+ return false;
766
+ }
767
+ if (value === undefined || !Utils.isEntity(value, true)) {
768
+ return true;
769
+ }
770
+ const target = Reference.unwrapReference(value);
771
+ const wrapped = helper(target);
772
+ if (!wrapped.__initialized) {
773
+ return true;
774
+ }
775
+ if (ref) {
776
+ return false;
777
+ }
778
+ if (options.fields) {
779
+ const childFields = options.fields
780
+ .filter(f => f.startsWith(`${prop.name}.`))
781
+ .map(f => f.substring(prop.name.length + 1));
782
+ return childFields.length > 0 && !childFields.every(f => this.isPropertyLoaded(target, f));
783
+ }
784
+ return prop.targetMeta.comparableProps.some(targetProp => {
785
+ const inlineEmbedded = targetProp.kind === ReferenceKind.EMBEDDED && !targetProp.object;
786
+ return !inlineEmbedded && !targetProp.lazy && !wrapped.__loadedProperties.has(targetProp.name);
787
+ });
788
+ });
756
789
  }
757
790
  lookupAllRelationships(entityName) {
758
791
  const ret = [];
@@ -587,6 +587,10 @@ export class MetadataDiscovery {
587
587
  prop.pivotEntity = pivotMeta.class;
588
588
  if (prop.inversedBy) {
589
589
  prop.targetMeta.properties[prop.inversedBy].pivotEntity = pivotMeta.class;
590
+ const targetRoot = prop.targetMeta.root;
591
+ if (targetRoot !== prop.targetMeta && targetRoot.properties[prop.inversedBy]) {
592
+ targetRoot.properties[prop.inversedBy].pivotEntity = pivotMeta.class;
593
+ }
590
594
  }
591
595
  return pivotMeta;
592
596
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/core",
3
- "version": "7.0.17-dev.0",
3
+ "version": "7.0.17-dev.10",
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.0.17-dev.0';
144
+ static #ORM_VERSION = '7.0.17-dev.10';
145
145
  /**
146
146
  * Checks if the argument is instance of `Object`. Returns false for arrays.
147
147
  */