@mikro-orm/core 7.1.0-dev.36 → 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
  }
@@ -797,7 +820,10 @@ export class EntityManager {
797
820
  data = QueryHelper.processObjectParams(data);
798
821
  validateParams(data, 'insert data');
799
822
  if (em.eventManager.hasListeners(EventType.beforeUpsert, meta)) {
800
- await em.eventManager.dispatchEvent(EventType.beforeUpsert, { entity: data, em, meta }, meta);
823
+ await em.eventManager.dispatchEvent(EventType.beforeUpsert, { entity: entity ?? data, em, meta }, meta);
824
+ if (entity) {
825
+ data = em.#comparator.prepareEntity(entity);
826
+ }
801
827
  }
802
828
  const ret = await em.driver.nativeUpdate(entityName, where, data, {
803
829
  ctx: em.#transactionContext,
@@ -846,6 +872,7 @@ export class EntityManager {
846
872
  em.getHydrator().hydrate(entity, meta, data2, em.#entityFactory, 'full', false, true);
847
873
  }
848
874
  // recompute the data as there might be some values missing (e.g. those with db column defaults)
875
+ resetUntouchedCollections(meta, entity);
849
876
  const snapshot = this.#comparator.prepareEntity(entity);
850
877
  em.#unitOfWork.register(entity, snapshot, { refresh: true });
851
878
  if (em.eventManager.hasListeners(EventType.afterUpsert, meta)) {
@@ -912,6 +939,7 @@ export class EntityManager {
912
939
  const allWhere = [];
913
940
  const entities = new Map();
914
941
  const entitiesByData = new Map();
942
+ const entitiesByAllDataIdx = new Map();
915
943
  for (let i = 0; i < data.length; i++) {
916
944
  let row = data[i];
917
945
  let where;
@@ -925,6 +953,7 @@ export class EntityManager {
925
953
  }
926
954
  where = helper(entity).getPrimaryKey();
927
955
  em.#entityFactory.assignDefaultValues(entity, meta);
956
+ entitiesByAllDataIdx.set(allData.length, entity);
928
957
  row = em.#comparator.prepareEntity(entity);
929
958
  }
930
959
  else {
@@ -972,6 +1001,9 @@ export class EntityManager {
972
1001
  const entity = entitiesByData.get(dto) ?? dto;
973
1002
  await em.eventManager.dispatchEvent(EventType.beforeUpsert, { entity, em, meta }, meta);
974
1003
  }
1004
+ for (const [idx, entity] of entitiesByAllDataIdx) {
1005
+ allData[idx] = em.#comparator.prepareEntity(entity);
1006
+ }
975
1007
  }
976
1008
  const res = await em.driver.nativeUpdateMany(entityName, allWhere, allData, {
977
1009
  ctx: em.#transactionContext,
@@ -1072,6 +1104,7 @@ export class EntityManager {
1072
1104
  }
1073
1105
  for (const [entity] of entities) {
1074
1106
  // recompute the data as there might be some values missing (e.g. those with db column defaults)
1107
+ resetUntouchedCollections(meta, entity);
1075
1108
  const snapshot = this.#comparator.prepareEntity(entity);
1076
1109
  em.#unitOfWork.register(entity, snapshot, { refresh: true });
1077
1110
  }
@@ -1864,9 +1897,13 @@ export class EntityManager {
1864
1897
  return [];
1865
1898
  }
1866
1899
  const meta = this.metadata.find(entityName);
1867
- // 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.
1868
1903
  const fields = options.fields ? this.buildFields(options.fields) : undefined;
1869
- 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;
1870
1907
  const hasNestedFields = fields?.some(f => f.includes('.')) ?? false;
1871
1908
  const shouldInfer = fields !== undefined && options.populate === undefined;
1872
1909
  const shouldMerge = fields !== undefined && populateArray !== undefined && populateArray.length > 0 && hasNestedFields;
@@ -1910,8 +1947,11 @@ export class EntityManager {
1910
1947
  }
1911
1948
  else {
1912
1949
  // with an explicit populate, only nested paths (e.g. `item.id`) need to be merged in — they
1913
- // require a JOIN to access sub-fields, whereas bare names are already handled by the driver
1914
- 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));
1915
1955
  options.populate = [...populateArray, ...extras];
1916
1956
  }
1917
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 = [];
package/index.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * @module core
4
4
  */
5
5
  export { EntityMetadata, PrimaryKeyProp, EntityRepositoryType, OptionalProps, EagerProps, HiddenProps, Config, EntityName, IndexHints, } from './typings.js';
6
- export type { CompiledFunctions, Constructor, ConnectionType, Dictionary, Primary, IPrimaryKey, ObjectQuery, FilterQuery, IWrappedEntity, InferEntityName, EntityData, Highlighter, MaybePromise, AnyEntity, EntityClass, EntityProperty, PopulateOptions, Populate, Loaded, New, LoadedReference, LoadedCollection, IMigrator, IMigrationGenerator, MigratorEvent, GetRepository, MigrationObject, DeepPartial, PrimaryProperty, Cast, IsUnknown, EntityDictionary, EntityDTO, EntityDTOFlat, EntityDTOProp, SerializeDTO, MigrationDiff, GenerateOptions, FilterObject, IndexFilterQuery, ExtractIndexHints, IndexName, IndexColumns, WithUsingOptions, IMigrationRunner, IEntityGenerator, ISeedManager, SeederObject, IMigratorStorage, RequiredEntityData, CheckCallback, TriggerCallback, IndexCallback, FormulaCallback, FormulaColumns, FormulaTable, SchemaTable, SchemaColumns, EntityDataPropValue, SimpleColumnMeta, Rel, Ref, LazyRef, ScalarRef, EntityRef, ISchemaGenerator, MigrationInfo, MigrateOptions, MigrationResult, MigrationRow, EntityKey, EntityValue, EntityDataValue, FilterKey, EntityType, FromEntityType, Selected, IsSubset, EntityProps, ExpandProperty, ExpandScalar, FilterItemValue, ExpandQuery, Scalar, ExpandHint, FilterValue, MergeLoaded, MergeSelected, TypeConfig, AnyString, ClearDatabaseOptions, CreateSchemaOptions, EnsureDatabaseOptions, UpdateSchemaOptions, DropSchemaOptions, RefreshDatabaseOptions, AutoPath, UnboxArray, MetadataProcessor, ImportsResolver, RequiredNullable, DefineConfig, Opt, Hidden, EntitySchemaWithMeta, InferEntity, CheckConstraint, TriggerDef, GeneratedColumnCallback, FilterDef, EntityCtor, Subquery, PopulateHintOptions, Prefixes, } from './typings.js';
6
+ export type { CompiledFunctions, Constructor, ConnectionType, Dictionary, Primary, IPrimaryKey, ObjectQuery, FilterQuery, IWrappedEntity, InferEntityName, EntityData, Highlighter, MaybePromise, AnyEntity, EntityClass, EntityProperty, PopulateOptions, Populate, Loaded, New, LoadedReference, LoadedCollection, IMigrator, IMigrationGenerator, MigratorEvent, GetRepository, MigrationObject, DeepPartial, PrimaryProperty, Cast, IsUnknown, EntityDictionary, EntityDTO, EntityDTOFlat, EntityDTOProp, SerializeDTO, MigrationDiff, GenerateOptions, FilterObject, IndexFilterQuery, ExtractIndexHints, IndexName, IndexColumns, WithUsingOptions, IMigrationRunner, IEntityGenerator, ISeedManager, SeederObject, IMigratorStorage, RequiredEntityData, CheckCallback, TriggerCallback, IndexCallback, FormulaCallback, FormulaColumns, FormulaTable, SchemaTable, SchemaColumns, SchemaColumnRef, EntityDataPropValue, SimpleColumnMeta, Rel, Ref, LazyRef, ScalarRef, EntityRef, ISchemaGenerator, MigrationInfo, MigrateOptions, MigrationResult, MigrationRow, EntityKey, EntityValue, EntityDataValue, FilterKey, EntityType, FromEntityType, Selected, IsSubset, EntityProps, ExpandProperty, ExpandScalar, FilterItemValue, ExpandQuery, Scalar, ExpandHint, FilterValue, MergeLoaded, MergeSelected, TypeConfig, AnyString, ClearDatabaseOptions, CreateSchemaOptions, EnsureDatabaseOptions, UpdateSchemaOptions, DropSchemaOptions, RefreshDatabaseOptions, AutoPath, UnboxArray, MetadataProcessor, ImportsResolver, RequiredNullable, DefineConfig, Opt, Hidden, EntitySchemaWithMeta, InferEntity, CheckConstraint, TriggerDef, GeneratedColumnCallback, FilterDef, EntityCtor, Subquery, PopulateHintOptions, Prefixes, } from './typings.js';
7
7
  export * from './enums.js';
8
8
  export * from './errors.js';
9
9
  export * from './exceptions.js';
@@ -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:
@@ -1030,10 +1034,14 @@ export class MetadataDiscovery {
1030
1034
  meta.propertyOrder.set(prop.name, (order += 0.01));
1031
1035
  });
1032
1036
  }
1033
- meta.indexes = Utils.unique([...base.indexes, ...meta.indexes]);
1034
- meta.uniques = Utils.unique([...base.uniques, ...meta.uniques]);
1035
- meta.checks = Utils.unique([...base.checks, ...meta.checks]);
1036
- meta.triggers = Utils.unique([...base.triggers, ...meta.triggers]);
1037
+ // TPT children have their own tables that don't contain the parent's columns,
1038
+ // so propagating parent indexes/uniques/checks/triggers would target missing columns.
1039
+ if (meta.inheritanceType !== 'tpt' || !meta.tptParent) {
1040
+ meta.indexes = Utils.unique([...base.indexes, ...meta.indexes]);
1041
+ meta.uniques = Utils.unique([...base.uniques, ...meta.uniques]);
1042
+ meta.checks = Utils.unique([...base.checks, ...meta.checks]);
1043
+ meta.triggers = Utils.unique([...base.triggers, ...meta.triggers]);
1044
+ }
1037
1045
  const pks = Object.values(meta.properties)
1038
1046
  .filter(p => p.primary)
1039
1047
  .map(p => p.name);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/core",
3
- "version": "7.1.0-dev.36",
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/typings.d.ts CHANGED
@@ -673,21 +673,39 @@ export type SchemaTable = {
673
673
  export type FormulaColumns<T> = Record<PropertyName<T>, string> & {
674
674
  toString(): string;
675
675
  };
676
+ /**
677
+ * Column reference for schema callbacks. Behaves like a string (coercible via `toString()`),
678
+ * with arbitrary nested property access for embedded sub-columns (e.g. `cols.address.city`).
679
+ */
680
+ export type SchemaColumnRef = string & {
681
+ readonly [key: string]: SchemaColumnRef;
682
+ };
676
683
  /**
677
684
  * Column mapping for schema callbacks (indexes, checks, generated columns).
678
685
  * Maps property names to field names. For TPT entities, only includes properties
679
686
  * that belong to the current table (not inherited properties from parent tables).
687
+ * Embedded properties expose their sub-columns via nested access (e.g. `cols.address.city`),
688
+ * while coercing to the embedded property's own column name via `toString()` (matches the
689
+ * pre-7712 behaviour, regardless of any custom `prefix`).
690
+ */
691
+ export type SchemaColumns<T> = Record<PropertyName<T>, SchemaColumnRef>;
692
+ /**
693
+ * Callback for custom index expressions. Receives column mappings, table info, and the index name.
694
+ * The runtime `columns` object includes nested entries for embedded properties (see `SchemaColumnRef`);
695
+ * cast to `SchemaColumns<T>` if you need typed nested access.
680
696
  */
681
- export type SchemaColumns<T> = Record<PropertyName<T>, string>;
682
- /** Callback for custom index expressions. Receives column mappings, table info, and the index name. */
683
697
  export type IndexCallback<T> = (columns: Record<PropertyName<T>, string>, table: SchemaTable, indexName: string) => string | Raw;
684
698
  /** Callback for computed (formula) properties. Receives column mappings and table info, returns a SQL expression. */
685
699
  export type FormulaCallback<T> = (columns: FormulaColumns<T>, table: FormulaTable) => string | Raw;
686
700
  /** Callback for CHECK constraint expressions. Receives column mappings and table info. */
687
- export type CheckCallback<T> = (columns: Record<PropertyName<T>, string>, table: SchemaTable) => string | Raw;
701
+ export type CheckCallback<T> = (columns: SchemaColumns<T>, table: SchemaTable) => string | Raw;
688
702
  /** Callback for trigger body expressions. Receives column mappings and table info. */
689
703
  export type TriggerCallback<T> = (columns: Record<PropertyName<T>, string>, table: SchemaTable) => string | Raw;
690
- /** Callback for generated (computed) column expressions. Receives column mappings and table info. */
704
+ /**
705
+ * Callback for generated (computed) column expressions. Receives column mappings and table info.
706
+ * The runtime `columns` object includes nested entries for embedded properties (see `SchemaColumnRef`);
707
+ * cast to `SchemaColumns<T>` if you need typed nested access.
708
+ */
691
709
  export type GeneratedColumnCallback<T> = (columns: Record<PropertyName<T>, string>, table: SchemaTable) => string | Raw;
692
710
  /** Definition of a CHECK constraint on a table or property. */
693
711
  export interface CheckConstraint<T = any> {
@@ -842,8 +860,12 @@ export declare class EntityMetadata<Entity = any, Class extends EntityCtor<Entit
842
860
  /**
843
861
  * Creates a column mapping for schema callbacks (indexes, checks, generated columns).
844
862
  * For TPT entities, only includes properties that belong to the current table (ownProps).
863
+ * Embedded properties expose their sub-columns via nested access (e.g. `cols.address.city`),
864
+ * while still coercing to the embedded column prefix when used as a string (GH #7712).
845
865
  */
846
866
  createSchemaColumnMappingObject(): SchemaColumns<Entity>;
867
+ /** @internal Recursively builds a column mapping for an embedded property's sub-columns. */
868
+ private static buildEmbeddedColumnMapping;
847
869
  get tableName(): string;
848
870
  set tableName(name: string);
849
871
  get uniqueName(): string;
package/typings.js CHANGED
@@ -133,16 +133,37 @@ export class EntityMetadata {
133
133
  /**
134
134
  * Creates a column mapping for schema callbacks (indexes, checks, generated columns).
135
135
  * For TPT entities, only includes properties that belong to the current table (ownProps).
136
+ * Embedded properties expose their sub-columns via nested access (e.g. `cols.address.city`),
137
+ * while still coercing to the embedded column prefix when used as a string (GH #7712).
136
138
  */
137
139
  createSchemaColumnMappingObject() {
138
140
  // For TPT entities, only include properties that belong to this entity's table
139
141
  const props = this.inheritanceType === 'tpt' && this.ownProps ? this.ownProps : Object.values(this.properties);
140
- return props.reduce((o, prop) => {
141
- if (prop.fieldNames) {
142
- o[prop.name] = prop.fieldNames[0];
142
+ const result = {};
143
+ for (const prop of props) {
144
+ if (!prop.fieldNames) {
145
+ continue;
143
146
  }
144
- return o;
145
- }, {});
147
+ if (prop.kind === ReferenceKind.EMBEDDED && prop.embeddedProps) {
148
+ result[prop.name] = EntityMetadata.buildEmbeddedColumnMapping(prop);
149
+ continue;
150
+ }
151
+ result[prop.name] = prop.fieldNames[0];
152
+ }
153
+ return result;
154
+ }
155
+ /** @internal Recursively builds a column mapping for an embedded property's sub-columns. */
156
+ static buildEmbeddedColumnMapping(embeddedProp) {
157
+ const result = {};
158
+ for (const [name, sub] of Object.entries(embeddedProp.embeddedProps)) {
159
+ result[name] =
160
+ sub.kind === ReferenceKind.EMBEDDED && sub.embeddedProps
161
+ ? EntityMetadata.buildEmbeddedColumnMapping(sub)
162
+ : sub.fieldNames[0];
163
+ }
164
+ const prefix = embeddedProp.fieldNames[0];
165
+ Object.defineProperty(result, 'toString', { value: () => prefix, enumerable: false });
166
+ return result;
146
167
  }
147
168
  get tableName() {
148
169
  return this.collection;
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.36';
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
+ }