@mikro-orm/core 7.0.12 → 7.0.13-dev.1

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.
Files changed (206) hide show
  1. package/EntityManager.d.ts +584 -885
  2. package/EntityManager.js +1900 -1927
  3. package/MikroORM.d.ts +74 -103
  4. package/MikroORM.js +178 -177
  5. package/README.md +1 -1
  6. package/cache/CacheAdapter.d.ts +36 -36
  7. package/cache/FileCacheAdapter.d.ts +24 -30
  8. package/cache/FileCacheAdapter.js +78 -80
  9. package/cache/GeneratedCacheAdapter.d.ts +20 -18
  10. package/cache/GeneratedCacheAdapter.js +30 -30
  11. package/cache/MemoryCacheAdapter.d.ts +20 -18
  12. package/cache/MemoryCacheAdapter.js +36 -35
  13. package/cache/NullCacheAdapter.d.ts +16 -16
  14. package/cache/NullCacheAdapter.js +24 -24
  15. package/connections/Connection.d.ts +84 -95
  16. package/connections/Connection.js +168 -165
  17. package/drivers/DatabaseDriver.d.ts +80 -186
  18. package/drivers/DatabaseDriver.js +443 -450
  19. package/drivers/IDatabaseDriver.d.ts +305 -444
  20. package/entity/BaseEntity.d.ts +83 -120
  21. package/entity/BaseEntity.js +43 -43
  22. package/entity/Collection.d.ts +181 -215
  23. package/entity/Collection.js +724 -730
  24. package/entity/EntityAssigner.d.ts +77 -88
  25. package/entity/EntityAssigner.js +230 -231
  26. package/entity/EntityFactory.d.ts +55 -67
  27. package/entity/EntityFactory.js +413 -454
  28. package/entity/EntityHelper.d.ts +23 -35
  29. package/entity/EntityHelper.js +290 -302
  30. package/entity/EntityIdentifier.d.ts +4 -4
  31. package/entity/EntityIdentifier.js +10 -10
  32. package/entity/EntityLoader.d.ts +72 -98
  33. package/entity/EntityLoader.js +786 -815
  34. package/entity/EntityRepository.d.ts +201 -316
  35. package/entity/EntityRepository.js +213 -213
  36. package/entity/PolymorphicRef.d.ts +5 -5
  37. package/entity/PolymorphicRef.js +10 -10
  38. package/entity/Reference.d.ts +83 -127
  39. package/entity/Reference.js +277 -281
  40. package/entity/WrappedEntity.d.ts +72 -115
  41. package/entity/WrappedEntity.js +166 -168
  42. package/entity/defineEntity.d.ts +654 -1359
  43. package/entity/defineEntity.js +518 -527
  44. package/entity/utils.d.ts +3 -13
  45. package/entity/utils.js +73 -71
  46. package/entity/validators.js +43 -43
  47. package/entity/wrap.js +8 -8
  48. package/enums.d.ts +253 -258
  49. package/enums.js +252 -251
  50. package/errors.d.ts +72 -114
  51. package/errors.js +253 -350
  52. package/events/EventManager.d.ts +14 -26
  53. package/events/EventManager.js +77 -79
  54. package/events/EventSubscriber.d.ts +29 -29
  55. package/events/TransactionEventBroadcaster.d.ts +8 -15
  56. package/events/TransactionEventBroadcaster.js +14 -14
  57. package/exceptions.d.ts +40 -23
  58. package/exceptions.js +52 -35
  59. package/hydration/Hydrator.d.ts +17 -42
  60. package/hydration/Hydrator.js +43 -43
  61. package/hydration/ObjectHydrator.d.ts +17 -50
  62. package/hydration/ObjectHydrator.js +418 -483
  63. package/index.d.ts +2 -116
  64. package/index.js +1 -10
  65. package/logging/DefaultLogger.d.ts +32 -34
  66. package/logging/DefaultLogger.js +86 -86
  67. package/logging/Logger.d.ts +41 -41
  68. package/logging/SimpleLogger.d.ts +11 -13
  69. package/logging/SimpleLogger.js +22 -22
  70. package/logging/colors.d.ts +6 -6
  71. package/logging/colors.js +10 -11
  72. package/logging/inspect.js +7 -7
  73. package/metadata/EntitySchema.d.ts +130 -214
  74. package/metadata/EntitySchema.js +412 -411
  75. package/metadata/MetadataDiscovery.d.ts +115 -115
  76. package/metadata/MetadataDiscovery.js +1908 -1987
  77. package/metadata/MetadataProvider.d.ts +26 -29
  78. package/metadata/MetadataProvider.js +97 -95
  79. package/metadata/MetadataStorage.d.ts +32 -38
  80. package/metadata/MetadataStorage.js +118 -118
  81. package/metadata/MetadataValidator.d.ts +39 -39
  82. package/metadata/MetadataValidator.js +340 -385
  83. package/metadata/discover-entities.d.ts +2 -5
  84. package/metadata/discover-entities.js +37 -35
  85. package/metadata/types.d.ts +531 -615
  86. package/naming-strategy/AbstractNamingStrategy.d.ts +39 -54
  87. package/naming-strategy/AbstractNamingStrategy.js +85 -90
  88. package/naming-strategy/EntityCaseNamingStrategy.d.ts +6 -6
  89. package/naming-strategy/EntityCaseNamingStrategy.js +22 -22
  90. package/naming-strategy/MongoNamingStrategy.d.ts +6 -6
  91. package/naming-strategy/MongoNamingStrategy.js +18 -18
  92. package/naming-strategy/NamingStrategy.d.ts +99 -109
  93. package/naming-strategy/UnderscoreNamingStrategy.d.ts +7 -7
  94. package/naming-strategy/UnderscoreNamingStrategy.js +21 -21
  95. package/not-supported.js +4 -7
  96. package/package.json +1 -1
  97. package/platforms/ExceptionConverter.d.ts +1 -1
  98. package/platforms/ExceptionConverter.js +4 -4
  99. package/platforms/Platform.d.ts +303 -312
  100. package/platforms/Platform.js +675 -695
  101. package/serialization/EntitySerializer.d.ts +26 -49
  102. package/serialization/EntitySerializer.js +218 -224
  103. package/serialization/EntityTransformer.d.ts +6 -10
  104. package/serialization/EntityTransformer.js +217 -219
  105. package/serialization/SerializationContext.d.ts +23 -27
  106. package/serialization/SerializationContext.js +105 -105
  107. package/types/ArrayType.d.ts +8 -8
  108. package/types/ArrayType.js +33 -33
  109. package/types/BigIntType.d.ts +10 -17
  110. package/types/BigIntType.js +37 -37
  111. package/types/BlobType.d.ts +3 -3
  112. package/types/BlobType.js +13 -13
  113. package/types/BooleanType.d.ts +4 -4
  114. package/types/BooleanType.js +12 -12
  115. package/types/CharacterType.d.ts +2 -2
  116. package/types/CharacterType.js +6 -6
  117. package/types/DateTimeType.d.ts +5 -5
  118. package/types/DateTimeType.js +15 -15
  119. package/types/DateType.d.ts +5 -5
  120. package/types/DateType.js +15 -15
  121. package/types/DecimalType.d.ts +7 -7
  122. package/types/DecimalType.js +26 -26
  123. package/types/DoubleType.d.ts +3 -3
  124. package/types/DoubleType.js +12 -12
  125. package/types/EnumArrayType.d.ts +5 -5
  126. package/types/EnumArrayType.js +24 -24
  127. package/types/EnumType.d.ts +3 -3
  128. package/types/EnumType.js +11 -11
  129. package/types/FloatType.d.ts +3 -3
  130. package/types/FloatType.js +9 -9
  131. package/types/IntegerType.d.ts +3 -3
  132. package/types/IntegerType.js +9 -9
  133. package/types/IntervalType.d.ts +4 -4
  134. package/types/IntervalType.js +12 -12
  135. package/types/JsonType.d.ts +8 -8
  136. package/types/JsonType.js +32 -32
  137. package/types/MediumIntType.d.ts +1 -1
  138. package/types/MediumIntType.js +3 -3
  139. package/types/SmallIntType.d.ts +3 -3
  140. package/types/SmallIntType.js +9 -9
  141. package/types/StringType.d.ts +4 -4
  142. package/types/StringType.js +12 -12
  143. package/types/TextType.d.ts +3 -3
  144. package/types/TextType.js +9 -9
  145. package/types/TimeType.d.ts +5 -5
  146. package/types/TimeType.js +17 -17
  147. package/types/TinyIntType.d.ts +3 -3
  148. package/types/TinyIntType.js +10 -10
  149. package/types/Type.d.ts +79 -83
  150. package/types/Type.js +82 -82
  151. package/types/Uint8ArrayType.d.ts +4 -4
  152. package/types/Uint8ArrayType.js +21 -21
  153. package/types/UnknownType.d.ts +4 -4
  154. package/types/UnknownType.js +12 -12
  155. package/types/UuidType.d.ts +5 -5
  156. package/types/UuidType.js +19 -19
  157. package/types/index.d.ts +49 -75
  158. package/types/index.js +26 -52
  159. package/typings.d.ts +744 -1265
  160. package/typings.js +233 -244
  161. package/unit-of-work/ChangeSet.d.ts +26 -26
  162. package/unit-of-work/ChangeSet.js +56 -56
  163. package/unit-of-work/ChangeSetComputer.d.ts +12 -12
  164. package/unit-of-work/ChangeSetComputer.js +179 -187
  165. package/unit-of-work/ChangeSetPersister.d.ts +50 -69
  166. package/unit-of-work/ChangeSetPersister.js +446 -469
  167. package/unit-of-work/CommitOrderCalculator.d.ts +40 -40
  168. package/unit-of-work/CommitOrderCalculator.js +88 -89
  169. package/unit-of-work/IdentityMap.d.ts +31 -31
  170. package/unit-of-work/IdentityMap.js +105 -105
  171. package/unit-of-work/UnitOfWork.d.ts +141 -181
  172. package/unit-of-work/UnitOfWork.js +1223 -1237
  173. package/utils/AbstractMigrator.d.ts +92 -112
  174. package/utils/AbstractMigrator.js +287 -285
  175. package/utils/AbstractSchemaGenerator.d.ts +34 -43
  176. package/utils/AbstractSchemaGenerator.js +122 -121
  177. package/utils/AsyncContext.d.ts +3 -3
  178. package/utils/AsyncContext.js +35 -34
  179. package/utils/Configuration.d.ts +808 -852
  180. package/utils/Configuration.js +344 -359
  181. package/utils/Cursor.d.ts +22 -40
  182. package/utils/Cursor.js +127 -135
  183. package/utils/DataloaderUtils.d.ts +43 -58
  184. package/utils/DataloaderUtils.js +198 -203
  185. package/utils/EntityComparator.d.ts +82 -99
  186. package/utils/EntityComparator.js +744 -836
  187. package/utils/NullHighlighter.d.ts +1 -1
  188. package/utils/NullHighlighter.js +3 -3
  189. package/utils/QueryHelper.d.ts +57 -85
  190. package/utils/QueryHelper.js +392 -404
  191. package/utils/RawQueryFragment.d.ts +34 -50
  192. package/utils/RawQueryFragment.js +121 -122
  193. package/utils/RequestContext.d.ts +32 -32
  194. package/utils/RequestContext.js +53 -52
  195. package/utils/TransactionContext.d.ts +16 -16
  196. package/utils/TransactionContext.js +27 -27
  197. package/utils/TransactionManager.d.ts +58 -58
  198. package/utils/TransactionManager.js +200 -202
  199. package/utils/Utils.d.ts +145 -204
  200. package/utils/Utils.js +826 -826
  201. package/utils/clone.js +114 -105
  202. package/utils/env-vars.js +88 -90
  203. package/utils/fs-utils.d.ts +15 -15
  204. package/utils/fs-utils.js +182 -181
  205. package/utils/upsert-utils.d.ts +5 -20
  206. package/utils/upsert-utils.js +116 -114
@@ -1,850 +1,821 @@
1
1
  import { QueryHelper } from '../utils/QueryHelper.js';
2
2
  import { Utils } from '../utils/Utils.js';
3
3
  import { ValidationError } from '../errors.js';
4
- import { LoadStrategy, PopulatePath, ReferenceKind } from '../enums.js';
4
+ import { LoadStrategy, PopulatePath, ReferenceKind, } from '../enums.js';
5
5
  import { Reference } from './Reference.js';
6
6
  import { helper } from './wrap.js';
7
7
  import { expandDotPaths } from './utils.js';
8
8
  import { Raw } from '../utils/RawQueryFragment.js';
9
9
  /** Responsible for batch-loading entity relations using either select-in or joined loading strategies. */
10
10
  export class EntityLoader {
11
- #metadata;
12
- #driver;
13
- #em;
14
- constructor(em) {
15
- this.#em = em;
16
- this.#metadata = this.#em.getMetadata();
17
- this.#driver = this.#em.getDriver();
18
- }
19
- /**
20
- * Loads specified relations in batch.
21
- * This will execute one query for each relation, that will populate it on all the specified entities.
22
- */
23
- async populate(entityName, entities, populate, options) {
24
- if (entities.length === 0 || Utils.isEmpty(populate)) {
25
- return this.setSerializationContext(entities, populate, options);
26
- }
27
- const meta = this.#metadata.find(entityName);
28
- if (entities.some(e => !e.__helper)) {
29
- const entity = entities.find(e => !Utils.isEntity(e));
30
- throw ValidationError.notDiscoveredEntity(entity, meta, 'populate');
31
- }
32
- const references = entities.filter(e => !helper(e).isInitialized());
33
- const visited = (options.visited ??= new Set());
34
- options.where ??= {};
35
- options.orderBy ??= {};
36
- options.lookup ??= true;
37
- options.validate ??= true;
38
- options.refresh ??= false;
39
- options.convertCustomTypes ??= true;
40
- if (references.length > 0) {
41
- await this.populateScalar(meta, references, { ...options, populateWhere: undefined });
42
- }
43
- populate = this.normalizePopulate(entityName, populate, options.strategy, options.lookup, options.exclude);
44
- const invalid = populate.find(({ field }) => !this.#em.canPopulate(entityName, field));
45
- /* v8 ignore next */
46
- if (options.validate && invalid) {
47
- throw ValidationError.invalidPropertyName(entityName, invalid.field);
48
- }
49
- this.setSerializationContext(entities, populate, options);
50
- const addedToVisited = [];
51
- for (const entity of entities) {
52
- if (visited.has(entity)) {
53
- continue;
54
- }
55
- addedToVisited.push(entity);
56
- visited.add(entity);
57
- }
58
- for (const pop of populate) {
59
- await this.populateField(entityName, entities, pop, options);
11
+ #metadata;
12
+ #driver;
13
+ #em;
14
+ constructor(em) {
15
+ this.#em = em;
16
+ this.#metadata = this.#em.getMetadata();
17
+ this.#driver = this.#em.getDriver();
18
+ }
19
+ /**
20
+ * Loads specified relations in batch.
21
+ * This will execute one query for each relation, that will populate it on all the specified entities.
22
+ */
23
+ async populate(entityName, entities, populate, options) {
24
+ if (entities.length === 0 || Utils.isEmpty(populate)) {
25
+ return this.setSerializationContext(entities, populate, options);
26
+ }
27
+ const meta = this.#metadata.find(entityName);
28
+ if (entities.some(e => !e.__helper)) {
29
+ const entity = entities.find(e => !Utils.isEntity(e));
30
+ throw ValidationError.notDiscoveredEntity(entity, meta, 'populate');
31
+ }
32
+ const references = entities.filter(e => !helper(e).isInitialized());
33
+ const visited = (options.visited ??= new Set());
34
+ options.where ??= {};
35
+ options.orderBy ??= {};
36
+ options.lookup ??= true;
37
+ options.validate ??= true;
38
+ options.refresh ??= false;
39
+ options.convertCustomTypes ??= true;
40
+ if (references.length > 0) {
41
+ await this.populateScalar(meta, references, { ...options, populateWhere: undefined });
42
+ }
43
+ populate = this.normalizePopulate(entityName, populate, options.strategy, options.lookup, options.exclude);
44
+ const invalid = populate.find(({ field }) => !this.#em.canPopulate(entityName, field));
45
+ /* v8 ignore next */
46
+ if (options.validate && invalid) {
47
+ throw ValidationError.invalidPropertyName(entityName, invalid.field);
48
+ }
49
+ this.setSerializationContext(entities, populate, options);
50
+ const addedToVisited = [];
51
+ for (const entity of entities) {
52
+ if (visited.has(entity)) {
53
+ continue;
54
+ }
55
+ addedToVisited.push(entity);
56
+ visited.add(entity);
57
+ }
58
+ for (const pop of populate) {
59
+ await this.populateField(entityName, entities, pop, options);
60
+ }
61
+ // Child-specific relations exist only on child metadata, so the parent-scoped populate loop above skips them (GH #7453).
62
+ if (Array.isArray(populate) &&
63
+ populate.some(p => p.all) &&
64
+ meta.inheritanceType === 'tpt' &&
65
+ meta.tptChildren?.length) {
66
+ const byType = new Map();
67
+ for (const entity of entities) {
68
+ const entityMeta = helper(entity).__meta;
69
+ if (entityMeta !== meta) {
70
+ const group = byType.get(entityMeta);
71
+ if (group) {
72
+ group.push(entity);
73
+ }
74
+ else {
75
+ byType.set(entityMeta, [entity]);
76
+ }
77
+ }
78
+ }
79
+ for (const [childMeta, group] of byType) {
80
+ await this.populate(childMeta.class, group, true, {
81
+ ...options,
82
+ lookup: false,
83
+ validate: false,
84
+ });
85
+ }
86
+ }
87
+ for (const entity of addedToVisited) {
88
+ visited.delete(entity);
89
+ }
60
90
  }
61
- // Child-specific relations exist only on child metadata, so the parent-scoped populate loop above skips them (GH #7453).
62
- if (
63
- Array.isArray(populate) &&
64
- populate.some(p => p.all) &&
65
- meta.inheritanceType === 'tpt' &&
66
- meta.tptChildren?.length
67
- ) {
68
- const byType = new Map();
69
- for (const entity of entities) {
70
- const entityMeta = helper(entity).__meta;
71
- if (entityMeta !== meta) {
72
- const group = byType.get(entityMeta);
73
- if (group) {
74
- group.push(entity);
75
- } else {
76
- byType.set(entityMeta, [entity]);
77
- }
78
- }
79
- }
80
- for (const [childMeta, group] of byType) {
81
- await this.populate(childMeta.class, group, true, {
82
- ...options,
83
- lookup: false,
84
- validate: false,
91
+ /** Normalizes populate hints into a structured array of PopulateOptions, expanding dot paths and eager relations. */
92
+ normalizePopulate(entityName, populate, strategy, lookup = true, exclude) {
93
+ const meta = this.#metadata.find(entityName);
94
+ let normalized = Utils.asArray(populate).map(field => {
95
+ // oxfmt-ignore
96
+ return typeof field === 'boolean' || field.field === PopulatePath.ALL ? { all: !!field, field: meta.primaryKeys[0] } : field;
85
97
  });
86
- }
87
- }
88
- for (const entity of addedToVisited) {
89
- visited.delete(entity);
90
- }
91
- }
92
- /** Normalizes populate hints into a structured array of PopulateOptions, expanding dot paths and eager relations. */
93
- normalizePopulate(entityName, populate, strategy, lookup = true, exclude) {
94
- const meta = this.#metadata.find(entityName);
95
- let normalized = Utils.asArray(populate).map(field => {
96
- // oxfmt-ignore
97
- return typeof field === 'boolean' || field.field === PopulatePath.ALL ? { all: !!field, field: meta.primaryKeys[0] } : field;
98
- });
99
- if (normalized.some(p => p.all)) {
100
- normalized = this.lookupAllRelationships(entityName);
101
- }
102
- // convert nested `field` with dot syntax to PopulateOptions with `children` array
103
- expandDotPaths(meta, normalized, true);
104
- if (lookup && populate !== false) {
105
- normalized = this.lookupEagerLoadedRelationships(entityName, normalized, strategy, '', [], exclude);
106
- // convert nested `field` with dot syntax produced by eager relations
107
- expandDotPaths(meta, normalized, true);
108
- }
109
- // merge same fields
110
- return this.mergeNestedPopulate(normalized);
111
- }
112
- setSerializationContext(entities, populate, options) {
113
- for (const entity of entities) {
114
- helper(entity).setSerializationContext({
115
- populate,
116
- fields: options.fields,
117
- exclude: options.exclude,
118
- });
119
- }
120
- }
121
- /**
122
- * Merge multiple populates for the same entity with different children. Also skips `*` fields, those can come from
123
- * partial loading hints (`fields`) that are used to infer the `populate` hint if missing.
124
- */
125
- mergeNestedPopulate(populate) {
126
- const tmp = populate.reduce((ret, item) => {
127
- /* v8 ignore next */
128
- if (item.field === PopulatePath.ALL) {
129
- return ret;
130
- }
131
- if (!ret[item.field]) {
132
- ret[item.field] = item;
133
- return ret;
134
- }
135
- if (!ret[item.field].children && item.children) {
136
- ret[item.field].children = item.children;
137
- } else if (ret[item.field].children && item.children) {
138
- ret[item.field].children.push(...item.children);
139
- }
140
- return ret;
141
- }, {});
142
- return Object.values(tmp).map(item => {
143
- if (item.children) {
144
- item.children = this.mergeNestedPopulate(item.children);
145
- }
146
- return item;
147
- });
148
- }
149
- /**
150
- * preload everything in one call (this will update already existing references in IM)
151
- */
152
- async populateMany(entityName, entities, populate, options) {
153
- const [field, ref] = populate.field.split(':', 2);
154
- const meta = this.#metadata.find(entityName);
155
- const prop = meta.properties[field];
156
- if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.owner && !this.#driver.getPlatform().usesPivotTable()) {
157
- const filtered = entities.filter(e => !e[prop.name]?.isInitialized());
158
- if (filtered.length > 0) {
159
- await this.populateScalar(meta, filtered, { ...options, fields: [prop.name] });
160
- }
161
- }
162
- if (prop.kind === ReferenceKind.SCALAR && prop.lazy) {
163
- const filtered = entities.filter(
164
- e => options.refresh || (prop.ref ? !e[prop.name]?.isInitialized() : e[prop.name] === undefined),
165
- );
166
- if (options.ignoreLazyScalarProperties || filtered.length === 0) {
167
- return entities;
168
- }
169
- await this.populateScalar(meta, filtered, { ...options, fields: [prop.name] });
170
- return entities;
171
- }
172
- if (prop.kind === ReferenceKind.EMBEDDED) {
173
- return [];
174
- }
175
- const filtered = this.filterCollections(entities, field, options, ref);
176
- const innerOrderBy = Utils.asArray(options.orderBy)
177
- .filter(
178
- orderBy =>
179
- (Array.isArray(orderBy[prop.name]) && orderBy[prop.name].length > 0) || Utils.isObject(orderBy[prop.name]),
180
- )
181
- .flatMap(orderBy => orderBy[prop.name]);
182
- const where = await this.extractChildCondition(options, prop);
183
- if (prop.kind === ReferenceKind.MANY_TO_MANY && this.#driver.getPlatform().usesPivotTable()) {
184
- const pivotOrderBy = QueryHelper.mergeOrderBy(innerOrderBy, prop.orderBy, prop.targetMeta?.orderBy);
185
- const res = await this.findChildrenFromPivotTable(filtered, prop, options, pivotOrderBy, populate, !!ref);
186
- return Utils.flatten(res);
187
- }
188
- if (prop.polymorphic && prop.polymorphTargets) {
189
- return this.populatePolymorphic(entities, prop, options, !!ref);
190
- }
191
- const { items, partial } = await this.findChildren(
192
- options.filtered ?? entities,
193
- prop,
194
- populate,
195
- {
196
- ...options,
197
- where,
198
- orderBy: innerOrderBy,
199
- },
200
- !!(ref || prop.mapToPk),
201
- );
202
- const customOrder = innerOrderBy.length > 0 || !!prop.orderBy || !!prop.targetMeta?.orderBy;
203
- this.initializeCollections(filtered, prop, field, items, customOrder, partial);
204
- return items;
205
- }
206
- async populateScalar(meta, filtered, options) {
207
- const pk = Utils.getPrimaryKeyHash(meta.primaryKeys);
208
- const ids = Utils.unique(filtered.map(e => Utils.getPrimaryKeyValues(e, meta, true)));
209
- const where = this.mergePrimaryCondition(ids, pk, options, meta, this.#metadata, this.#driver.getPlatform());
210
- const { filters, convertCustomTypes, lockMode, strategy, populateWhere, connectionType, logging, fields } = options;
211
- await this.#em.find(meta.class, where, {
212
- filters,
213
- convertCustomTypes,
214
- lockMode,
215
- strategy,
216
- populateWhere,
217
- connectionType,
218
- logging,
219
- fields: fields,
220
- populate: [],
221
- });
222
- }
223
- async populatePolymorphic(entities, prop, options, ref) {
224
- const ownerMeta = this.#metadata.get(entities[0].constructor);
225
- // Separate entities: those with loaded refs vs those needing FK load
226
- const toPopulate = [];
227
- const needsFkLoad = [];
228
- for (const entity of entities) {
229
- const refValue = entity[prop.name];
230
- if (refValue && helper(refValue).hasPrimaryKey()) {
231
- if (
232
- (ref && !options.refresh) || // :ref hint - already have reference
233
- (!ref && helper(refValue).__initialized && !options.refresh) // already loaded
234
- ) {
235
- continue;
236
- }
237
- toPopulate.push(entity);
238
- } else if (refValue == null && !helper(entity).__loadedProperties.has(prop.name)) {
239
- // FK columns weren't loaded (partial loading) — need to re-fetch them.
240
- // If the property IS in __loadedProperties, the FK was loaded and is genuinely null.
241
- needsFkLoad.push(entity);
242
- }
243
- }
244
- // Load FK columns using populateScalar pattern
245
- if (needsFkLoad.length > 0) {
246
- await this.populateScalar(ownerMeta, needsFkLoad, {
247
- ...options,
248
- fields: [...ownerMeta.primaryKeys, prop.name],
249
- });
250
- // After loading FKs, add to toPopulate if not using :ref hint
251
- if (!ref) {
252
- for (const entity of needsFkLoad) {
253
- const refValue = entity[prop.name];
254
- if (refValue && helper(refValue).hasPrimaryKey()) {
255
- toPopulate.push(entity);
256
- }
257
- }
258
- }
259
- }
260
- if (toPopulate.length === 0) {
261
- return [];
262
- }
263
- // Group references by target class for batch loading
264
- const groups = new Map();
265
- for (const entity of toPopulate) {
266
- const refValue = Reference.unwrapReference(entity[prop.name]);
267
- const discriminator = QueryHelper.findDiscriminatorValue(prop.discriminatorMap, helper(refValue).__meta.class);
268
- const group = groups.get(discriminator) ?? [];
269
- group.push(refValue);
270
- groups.set(discriminator, group);
271
- }
272
- // Load each group concurrently - identity map handles merging with existing references
273
- const allItems = [];
274
- await Promise.all(
275
- [...groups].map(async ([discriminator, children]) => {
276
- const targetMeta = this.#metadata.find(prop.discriminatorMap[discriminator]);
277
- await this.populateScalar(targetMeta, children, options);
278
- allItems.push(...children);
279
- }),
280
- );
281
- return allItems;
282
- }
283
- initializeCollections(filtered, prop, field, children, customOrder, partial) {
284
- if (prop.kind === ReferenceKind.ONE_TO_MANY) {
285
- this.initializeOneToMany(filtered, children, prop, field, partial);
286
- }
287
- if (prop.kind === ReferenceKind.MANY_TO_MANY && !this.#driver.getPlatform().usesPivotTable()) {
288
- this.initializeManyToMany(filtered, children, prop, field, customOrder, partial);
289
- }
290
- }
291
- initializeOneToMany(filtered, children, prop, field, partial) {
292
- const mapToPk = prop.targetMeta.properties[prop.mappedBy].mapToPk;
293
- const map = {};
294
- for (const entity of filtered) {
295
- const key = helper(entity).getSerializedPrimaryKey();
296
- map[key] = [];
297
- }
298
- for (const child of children) {
299
- const pk = child.__helper.__data[prop.mappedBy] ?? child[prop.mappedBy];
300
- if (pk) {
301
- const key = helper(mapToPk ? this.#em.getReference(prop.targetMeta.class, pk) : pk).getSerializedPrimaryKey();
302
- map[key]?.push(child);
303
- }
304
- }
305
- for (const entity of filtered) {
306
- const key = helper(entity).getSerializedPrimaryKey();
307
- entity[field].hydrate(map[key], undefined, partial);
308
- }
309
- }
310
- initializeManyToMany(filtered, children, prop, field, customOrder, partial) {
311
- if (prop.mappedBy) {
312
- for (const entity of filtered) {
313
- const items = children.filter(child => child[prop.mappedBy].contains(entity, false));
314
- entity[field].hydrate(items, true, partial);
315
- }
316
- } else {
317
- // owning side of M:N without pivot table needs to be reordered
318
- for (const entity of filtered) {
319
- const order = !customOrder ? [...entity[prop.name].getItems(false)] : []; // copy order of references
320
- const items = children.filter(child => entity[prop.name].contains(child, false));
321
- if (!customOrder) {
322
- items.sort((a, b) => order.indexOf(a) - order.indexOf(b));
323
- }
324
- entity[field].hydrate(items, true, partial);
325
- }
326
- }
327
- }
328
- async findChildren(entities, prop, populate, options, ref) {
329
- const children = Utils.unique(this.getChildReferences(entities, prop, options, ref));
330
- const meta = prop.targetMeta;
331
- // When targetKey is set, use it for FK lookup instead of the PK
332
- let fk = prop.targetKey ?? Utils.getPrimaryKeyHash(meta.primaryKeys);
333
- let schema = options.schema;
334
- const partial = !Utils.isEmpty(prop.where) || !Utils.isEmpty(options.where);
335
- let polymorphicOwnerProp;
336
- if (prop.kind === ReferenceKind.ONE_TO_MANY || (prop.kind === ReferenceKind.MANY_TO_MANY && !prop.owner)) {
337
- const ownerProp = meta.properties[prop.mappedBy];
338
- if (ownerProp.polymorphic && ownerProp.fieldNames.length >= 2) {
339
- const idColumns = ownerProp.fieldNames.slice(1);
340
- fk = idColumns.length === 1 ? idColumns[0] : idColumns;
341
- polymorphicOwnerProp = ownerProp;
342
- } else {
343
- fk = ownerProp.name;
344
- }
345
- }
346
- if (prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner && !ref) {
347
- children.length = 0;
348
- fk = meta.properties[prop.mappedBy].name;
349
- children.push(...this.filterByReferences(entities, prop.name, options.refresh));
350
- }
351
- if (children.length === 0) {
352
- return { items: [], partial };
353
- }
354
- if (!schema && [ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind)) {
355
- schema = children.find(e => e.__helper.__schema)?.__helper.__schema;
356
- }
357
- const ids = Utils.unique(children.map(e => (prop.targetKey ? e[prop.targetKey] : e.__helper.getPrimaryKey())));
358
- let where;
359
- if (polymorphicOwnerProp && Array.isArray(fk)) {
360
- const conditions = ids.map(id => {
361
- const pkValues = Object.values(id);
362
- return Object.fromEntries(fk.map((col, idx) => [col, pkValues[idx]]));
363
- });
364
- where = conditions.length === 1 ? conditions[0] : { $or: conditions };
365
- } else {
366
- where = this.mergePrimaryCondition(ids, fk, options, meta, this.#metadata, this.#driver.getPlatform());
367
- }
368
- if (polymorphicOwnerProp) {
369
- const parentMeta = this.#metadata.find(entities[0].constructor);
370
- const discriminatorValue =
371
- QueryHelper.findDiscriminatorValue(polymorphicOwnerProp.discriminatorMap, parentMeta.class) ??
372
- parentMeta.tableName;
373
- const discriminatorColumn = polymorphicOwnerProp.fieldNames[0];
374
- where = { $and: [where, { [discriminatorColumn]: discriminatorValue }] };
375
- }
376
- const fields = this.buildFields(options.fields, prop, ref);
377
- /* eslint-disable prefer-const */
378
- let {
379
- refresh,
380
- filters,
381
- convertCustomTypes,
382
- lockMode,
383
- strategy,
384
- populateWhere = 'infer',
385
- connectionType,
386
- logging,
387
- } = options;
388
- /* eslint-enable prefer-const */
389
- if (typeof populateWhere === 'object') {
390
- populateWhere = await this.extractChildCondition({ where: populateWhere }, prop);
391
- }
392
- if (!Utils.isEmpty(prop.where) || Raw.hasObjectFragments(prop.where)) {
393
- where = { $and: [where, prop.where] };
394
- }
395
- const orderBy = QueryHelper.mergeOrderBy(options.orderBy, prop.orderBy);
396
- const items = await this.#em.find(meta.class, where, {
397
- filters,
398
- convertCustomTypes,
399
- lockMode,
400
- populateWhere,
401
- logging,
402
- orderBy,
403
- populate: populate.children ?? populate.all ?? [],
404
- exclude: Array.isArray(options.exclude)
405
- ? Utils.extractChildElements(options.exclude, prop.name)
406
- : options.exclude,
407
- strategy,
408
- fields,
409
- schema,
410
- connectionType,
411
- // @ts-ignore not a public option, will be propagated to the populate call
412
- refresh: refresh && !children.every(item => options.visited.has(item)),
413
- // @ts-ignore not a public option, will be propagated to the populate call
414
- visited: options.visited,
415
- });
416
- // For targetKey relations, wire up loaded entities to parent references
417
- // This is needed because the references were created under alternate key,
418
- // but loaded entities are stored under PK, so they don't automatically merge
419
- if (prop.targetKey && [ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind)) {
420
- const itemsByKey = new Map();
421
- for (const item of items) {
422
- itemsByKey.set('' + item[prop.targetKey], item);
423
- }
424
- for (const entity of entities) {
425
- const ref = entity[prop.name];
426
- /* v8 ignore next */
427
- if (!ref) {
428
- continue;
98
+ if (normalized.some(p => p.all)) {
99
+ normalized = this.lookupAllRelationships(entityName);
429
100
  }
430
- // oxfmt-ignore
431
- const keyValue = '' + (Reference.isReference(ref) ? ref.unwrap()[prop.targetKey] : ref[prop.targetKey]);
432
- const loadedItem = itemsByKey.get(keyValue);
433
- if (loadedItem) {
434
- entity[prop.name] = Reference.isReference(ref) ? Reference.create(loadedItem) : loadedItem;
101
+ // convert nested `field` with dot syntax to PopulateOptions with `children` array
102
+ expandDotPaths(meta, normalized, true);
103
+ if (lookup && populate !== false) {
104
+ normalized = this.lookupEagerLoadedRelationships(entityName, normalized, strategy, '', [], exclude);
105
+ // convert nested `field` with dot syntax produced by eager relations
106
+ expandDotPaths(meta, normalized, true);
107
+ }
108
+ // merge same fields
109
+ return this.mergeNestedPopulate(normalized);
110
+ }
111
+ setSerializationContext(entities, populate, options) {
112
+ for (const entity of entities) {
113
+ helper(entity).setSerializationContext({
114
+ populate,
115
+ fields: options.fields,
116
+ exclude: options.exclude,
117
+ });
435
118
  }
436
- }
437
119
  }
438
- if ([ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind) && items.length !== children.length) {
439
- const nullVal = this.#em.config.get('forceUndefined') ? undefined : null;
440
- const itemsMap = new Set();
441
- const childrenMap = new Set();
442
- // Use targetKey value if set, otherwise use serialized PK
443
- const getKey = e => (prop.targetKey ? '' + e[prop.targetKey] : helper(e).getSerializedPrimaryKey());
444
- for (const item of items) {
445
- /* v8 ignore next */
446
- itemsMap.add(getKey(item));
447
- }
448
- for (const child of children) {
449
- childrenMap.add(getKey(child));
450
- }
451
- const isInverseOneToOne = prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner;
452
- for (const entity of entities) {
453
- const ref = entity[prop.name];
454
- if (ref == null) {
455
- continue;
456
- }
457
- const refKey = getKey(ref);
458
- // For 1:1 inverse, `children` contains parent entities, so `childrenMap`
459
- // has parent PKs — match against the entity's own PK, not the referenced entity's PK.
460
- const childKey = isInverseOneToOne ? getKey(entity) : refKey;
461
- if (childrenMap.has(childKey) && !itemsMap.has(refKey)) {
462
- entity[prop.name] = nullVal;
463
- helper(entity).__originalEntityData[prop.name] = null;
464
- }
465
- }
120
+ /**
121
+ * Merge multiple populates for the same entity with different children. Also skips `*` fields, those can come from
122
+ * partial loading hints (`fields`) that are used to infer the `populate` hint if missing.
123
+ */
124
+ mergeNestedPopulate(populate) {
125
+ const tmp = populate.reduce((ret, item) => {
126
+ /* v8 ignore next */
127
+ if (item.field === PopulatePath.ALL) {
128
+ return ret;
129
+ }
130
+ if (!ret[item.field]) {
131
+ ret[item.field] = item;
132
+ return ret;
133
+ }
134
+ if (!ret[item.field].children && item.children) {
135
+ ret[item.field].children = item.children;
136
+ }
137
+ else if (ret[item.field].children && item.children) {
138
+ ret[item.field].children.push(...item.children);
139
+ }
140
+ return ret;
141
+ }, {});
142
+ return Object.values(tmp).map(item => {
143
+ if (item.children) {
144
+ item.children = this.mergeNestedPopulate(item.children);
145
+ }
146
+ return item;
147
+ });
466
148
  }
467
- for (const item of items) {
468
- if (ref && !helper(item).__onLoadFired) {
469
- helper(item).__initialized = false;
470
- this.#em.getUnitOfWork().unmarkAsLoaded(item);
471
- }
149
+ /**
150
+ * preload everything in one call (this will update already existing references in IM)
151
+ */
152
+ async populateMany(entityName, entities, populate, options) {
153
+ const [field, ref] = populate.field.split(':', 2);
154
+ const meta = this.#metadata.find(entityName);
155
+ const prop = meta.properties[field];
156
+ if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.owner && !this.#driver.getPlatform().usesPivotTable()) {
157
+ const filtered = entities.filter(e => !e[prop.name]?.isInitialized());
158
+ if (filtered.length > 0) {
159
+ await this.populateScalar(meta, filtered, { ...options, fields: [prop.name] });
160
+ }
161
+ }
162
+ if (prop.kind === ReferenceKind.SCALAR && prop.lazy) {
163
+ const filtered = entities.filter(e => options.refresh ||
164
+ (prop.ref ? !e[prop.name]?.isInitialized() : e[prop.name] === undefined));
165
+ if (options.ignoreLazyScalarProperties || filtered.length === 0) {
166
+ return entities;
167
+ }
168
+ await this.populateScalar(meta, filtered, { ...options, fields: [prop.name] });
169
+ return entities;
170
+ }
171
+ if (prop.kind === ReferenceKind.EMBEDDED) {
172
+ return [];
173
+ }
174
+ const filtered = this.filterCollections(entities, field, options, ref);
175
+ const innerOrderBy = Utils.asArray(options.orderBy)
176
+ .filter(orderBy => (Array.isArray(orderBy[prop.name]) && orderBy[prop.name].length > 0) ||
177
+ Utils.isObject(orderBy[prop.name]))
178
+ .flatMap(orderBy => orderBy[prop.name]);
179
+ const where = await this.extractChildCondition(options, prop);
180
+ if (prop.kind === ReferenceKind.MANY_TO_MANY && this.#driver.getPlatform().usesPivotTable()) {
181
+ const pivotOrderBy = QueryHelper.mergeOrderBy(innerOrderBy, prop.orderBy, prop.targetMeta?.orderBy);
182
+ const res = await this.findChildrenFromPivotTable(filtered, prop, options, pivotOrderBy, populate, !!ref);
183
+ return Utils.flatten(res);
184
+ }
185
+ if (prop.polymorphic && prop.polymorphTargets) {
186
+ return this.populatePolymorphic(entities, prop, options, !!ref);
187
+ }
188
+ const { items, partial } = await this.findChildren(options.filtered ?? entities, prop, populate, {
189
+ ...options,
190
+ where,
191
+ orderBy: innerOrderBy,
192
+ }, !!(ref || prop.mapToPk));
193
+ const customOrder = innerOrderBy.length > 0 || !!prop.orderBy || !!prop.targetMeta?.orderBy;
194
+ this.initializeCollections(filtered, prop, field, items, customOrder, partial);
195
+ return items;
196
+ }
197
+ async populateScalar(meta, filtered, options) {
198
+ const pk = Utils.getPrimaryKeyHash(meta.primaryKeys);
199
+ const ids = Utils.unique(filtered.map(e => Utils.getPrimaryKeyValues(e, meta, true)));
200
+ const where = this.mergePrimaryCondition(ids, pk, options, meta, this.#metadata, this.#driver.getPlatform());
201
+ const { filters, convertCustomTypes, lockMode, strategy, populateWhere, connectionType, logging, fields } = options;
202
+ await this.#em.find(meta.class, where, {
203
+ filters,
204
+ convertCustomTypes,
205
+ lockMode,
206
+ strategy,
207
+ populateWhere,
208
+ connectionType,
209
+ logging,
210
+ fields: fields,
211
+ populate: [],
212
+ });
472
213
  }
473
- return { items, partial };
474
- }
475
- mergePrimaryCondition(ids, pk, options, meta, metadata, platform) {
476
- const cond1 = QueryHelper.processWhere({
477
- where: { [pk]: { $in: ids } },
478
- entityName: meta.class,
479
- metadata,
480
- platform,
481
- convertCustomTypes: !options.convertCustomTypes,
482
- });
483
- const where = { ...options.where };
484
- Utils.dropUndefinedProperties(where);
485
- return where[pk] ? { $and: [cond1, where] } : { ...cond1, ...where };
486
- }
487
- async populateField(entityName, entities, populate, options) {
488
- const field = populate.field.split(':')[0];
489
- const prop = this.#metadata.find(entityName).properties[field];
490
- if (prop.kind === ReferenceKind.SCALAR && !prop.lazy) {
491
- return;
214
+ async populatePolymorphic(entities, prop, options, ref) {
215
+ const ownerMeta = this.#metadata.get(entities[0].constructor);
216
+ // Separate entities: those with loaded refs vs those needing FK load
217
+ const toPopulate = [];
218
+ const needsFkLoad = [];
219
+ for (const entity of entities) {
220
+ const refValue = entity[prop.name];
221
+ if (refValue && helper(refValue).hasPrimaryKey()) {
222
+ if ((ref && !options.refresh) || // :ref hint - already have reference
223
+ (!ref && helper(refValue).__initialized && !options.refresh) // already loaded
224
+ ) {
225
+ continue;
226
+ }
227
+ toPopulate.push(entity);
228
+ }
229
+ else if (refValue == null && !helper(entity).__loadedProperties.has(prop.name)) {
230
+ // FK columns weren't loaded (partial loading) — need to re-fetch them.
231
+ // If the property IS in __loadedProperties, the FK was loaded and is genuinely null.
232
+ needsFkLoad.push(entity);
233
+ }
234
+ }
235
+ // Load FK columns using populateScalar pattern
236
+ if (needsFkLoad.length > 0) {
237
+ await this.populateScalar(ownerMeta, needsFkLoad, {
238
+ ...options,
239
+ fields: [...ownerMeta.primaryKeys, prop.name],
240
+ });
241
+ // After loading FKs, add to toPopulate if not using :ref hint
242
+ if (!ref) {
243
+ for (const entity of needsFkLoad) {
244
+ const refValue = entity[prop.name];
245
+ if (refValue && helper(refValue).hasPrimaryKey()) {
246
+ toPopulate.push(entity);
247
+ }
248
+ }
249
+ }
250
+ }
251
+ if (toPopulate.length === 0) {
252
+ return [];
253
+ }
254
+ // Group references by target class for batch loading
255
+ const groups = new Map();
256
+ for (const entity of toPopulate) {
257
+ const refValue = Reference.unwrapReference(entity[prop.name]);
258
+ const discriminator = QueryHelper.findDiscriminatorValue(prop.discriminatorMap, helper(refValue).__meta.class);
259
+ const group = groups.get(discriminator) ?? [];
260
+ group.push(refValue);
261
+ groups.set(discriminator, group);
262
+ }
263
+ // Load each group concurrently - identity map handles merging with existing references
264
+ const allItems = [];
265
+ await Promise.all([...groups].map(async ([discriminator, children]) => {
266
+ const targetMeta = this.#metadata.find(prop.discriminatorMap[discriminator]);
267
+ await this.populateScalar(targetMeta, children, options);
268
+ allItems.push(...children);
269
+ }));
270
+ return allItems;
271
+ }
272
+ initializeCollections(filtered, prop, field, children, customOrder, partial) {
273
+ if (prop.kind === ReferenceKind.ONE_TO_MANY) {
274
+ this.initializeOneToMany(filtered, children, prop, field, partial);
275
+ }
276
+ if (prop.kind === ReferenceKind.MANY_TO_MANY && !this.#driver.getPlatform().usesPivotTable()) {
277
+ this.initializeManyToMany(filtered, children, prop, field, customOrder, partial);
278
+ }
492
279
  }
493
- options = { ...options, filters: QueryHelper.mergePropertyFilters(prop.filters, options.filters) };
494
- const populated = await this.populateMany(entityName, entities, populate, options);
495
- if (!populate.children && !populate.all) {
496
- return;
280
+ initializeOneToMany(filtered, children, prop, field, partial) {
281
+ const mapToPk = prop.targetMeta.properties[prop.mappedBy].mapToPk;
282
+ const map = {};
283
+ for (const entity of filtered) {
284
+ const key = helper(entity).getSerializedPrimaryKey();
285
+ map[key] = [];
286
+ }
287
+ for (const child of children) {
288
+ const pk = child.__helper.__data[prop.mappedBy] ?? child[prop.mappedBy];
289
+ if (pk) {
290
+ const key = helper(mapToPk ? this.#em.getReference(prop.targetMeta.class, pk) : pk).getSerializedPrimaryKey();
291
+ map[key]?.push(child);
292
+ }
293
+ }
294
+ for (const entity of filtered) {
295
+ const key = helper(entity).getSerializedPrimaryKey();
296
+ entity[field].hydrate(map[key], undefined, partial);
297
+ }
497
298
  }
498
- const children = [];
499
- for (const entity of entities) {
500
- const ref = entity[field];
501
- if (Utils.isEntity(ref)) {
502
- children.push(ref);
503
- } else if (Reference.isReference(ref)) {
504
- children.push(ref.unwrap());
505
- } else if (Utils.isCollection(ref)) {
506
- children.push(...ref.getItems());
507
- } else if (ref && prop.kind === ReferenceKind.EMBEDDED) {
508
- children.push(...Utils.asArray(ref));
509
- }
299
+ initializeManyToMany(filtered, children, prop, field, customOrder, partial) {
300
+ if (prop.mappedBy) {
301
+ for (const entity of filtered) {
302
+ const items = children.filter(child => child[prop.mappedBy].contains(entity, false));
303
+ entity[field].hydrate(items, true, partial);
304
+ }
305
+ }
306
+ else {
307
+ // owning side of M:N without pivot table needs to be reordered
308
+ for (const entity of filtered) {
309
+ const order = !customOrder ? [...entity[prop.name].getItems(false)] : []; // copy order of references
310
+ const items = children.filter(child => entity[prop.name].contains(child, false));
311
+ if (!customOrder) {
312
+ items.sort((a, b) => order.indexOf(a) - order.indexOf(b));
313
+ }
314
+ entity[field].hydrate(items, true, partial);
315
+ }
316
+ }
510
317
  }
511
- if (populated.length === 0 && !populate.children) {
512
- // Populate child-specific relations for TPT entities already in the identity map (GH #7529).
513
- // Pass a sentinel hint so the existing TPT child handling in `populate()` does the real work.
514
- if (populate.all && prop.targetMeta?.inheritanceType === 'tpt') {
318
+ async findChildren(entities, prop, populate, options, ref) {
319
+ const children = Utils.unique(this.getChildReferences(entities, prop, options, ref));
320
+ const meta = prop.targetMeta;
321
+ // When targetKey is set, use it for FK lookup instead of the PK
322
+ let fk = prop.targetKey ?? Utils.getPrimaryKeyHash(meta.primaryKeys);
323
+ let schema = options.schema;
324
+ const partial = !Utils.isEmpty(prop.where) || !Utils.isEmpty(options.where);
325
+ let polymorphicOwnerProp;
326
+ if (prop.kind === ReferenceKind.ONE_TO_MANY || (prop.kind === ReferenceKind.MANY_TO_MANY && !prop.owner)) {
327
+ const ownerProp = meta.properties[prop.mappedBy];
328
+ if (ownerProp.polymorphic && ownerProp.fieldNames.length >= 2) {
329
+ const idColumns = ownerProp.fieldNames.slice(1);
330
+ fk = idColumns.length === 1 ? idColumns[0] : idColumns;
331
+ polymorphicOwnerProp = ownerProp;
332
+ }
333
+ else {
334
+ fk = ownerProp.name;
335
+ }
336
+ }
337
+ if (prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner && !ref) {
338
+ children.length = 0;
339
+ fk = meta.properties[prop.mappedBy].name;
340
+ children.push(...this.filterByReferences(entities, prop.name, options.refresh));
341
+ }
342
+ if (children.length === 0) {
343
+ return { items: [], partial };
344
+ }
345
+ if (!schema && [ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind)) {
346
+ schema = children.find(e => e.__helper.__schema)?.__helper.__schema;
347
+ }
348
+ const ids = Utils.unique(children.map(e => (prop.targetKey ? e[prop.targetKey] : e.__helper.getPrimaryKey())));
349
+ let where;
350
+ if (polymorphicOwnerProp && Array.isArray(fk)) {
351
+ const conditions = ids.map(id => {
352
+ const pkValues = Object.values(id);
353
+ return Object.fromEntries(fk.map((col, idx) => [col, pkValues[idx]]));
354
+ });
355
+ where = (conditions.length === 1 ? conditions[0] : { $or: conditions });
356
+ }
357
+ else {
358
+ where = this.mergePrimaryCondition(ids, fk, options, meta, this.#metadata, this.#driver.getPlatform());
359
+ }
360
+ if (polymorphicOwnerProp) {
361
+ const parentMeta = this.#metadata.find(entities[0].constructor);
362
+ const discriminatorValue = QueryHelper.findDiscriminatorValue(polymorphicOwnerProp.discriminatorMap, parentMeta.class) ??
363
+ parentMeta.tableName;
364
+ const discriminatorColumn = polymorphicOwnerProp.fieldNames[0];
365
+ where = { $and: [where, { [discriminatorColumn]: discriminatorValue }] };
366
+ }
367
+ const fields = this.buildFields(options.fields, prop, ref);
368
+ /* eslint-disable prefer-const */
369
+ let { refresh, filters, convertCustomTypes, lockMode, strategy, populateWhere = 'infer', connectionType, logging, } = options;
370
+ /* eslint-enable prefer-const */
371
+ if (typeof populateWhere === 'object') {
372
+ populateWhere = await this.extractChildCondition({ where: populateWhere }, prop);
373
+ }
374
+ if (!Utils.isEmpty(prop.where) || Raw.hasObjectFragments(prop.where)) {
375
+ where = { $and: [where, prop.where] };
376
+ }
377
+ const orderBy = QueryHelper.mergeOrderBy(options.orderBy, prop.orderBy);
378
+ const items = await this.#em.find(meta.class, where, {
379
+ filters,
380
+ convertCustomTypes,
381
+ lockMode,
382
+ populateWhere,
383
+ logging,
384
+ orderBy,
385
+ populate: populate.children ?? populate.all ?? [],
386
+ exclude: Array.isArray(options.exclude)
387
+ ? Utils.extractChildElements(options.exclude, prop.name)
388
+ : options.exclude,
389
+ strategy,
390
+ fields,
391
+ schema,
392
+ connectionType,
393
+ // @ts-ignore not a public option, will be propagated to the populate call
394
+ refresh: refresh && !children.every(item => options.visited.has(item)),
395
+ // @ts-ignore not a public option, will be propagated to the populate call
396
+ visited: options.visited,
397
+ });
398
+ // For targetKey relations, wire up loaded entities to parent references
399
+ // This is needed because the references were created under alternate key,
400
+ // but loaded entities are stored under PK, so they don't automatically merge
401
+ if (prop.targetKey && [ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind)) {
402
+ const itemsByKey = new Map();
403
+ for (const item of items) {
404
+ itemsByKey.set('' + item[prop.targetKey], item);
405
+ }
406
+ for (const entity of entities) {
407
+ const ref = entity[prop.name];
408
+ /* v8 ignore next */
409
+ if (!ref) {
410
+ continue;
411
+ }
412
+ // oxfmt-ignore
413
+ const keyValue = '' + (Reference.isReference(ref) ? ref.unwrap()[prop.targetKey] : ref[prop.targetKey]);
414
+ const loadedItem = itemsByKey.get(keyValue);
415
+ if (loadedItem) {
416
+ entity[prop.name] = (Reference.isReference(ref) ? Reference.create(loadedItem) : loadedItem);
417
+ }
418
+ }
419
+ }
420
+ if ([ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind) && items.length !== children.length) {
421
+ const nullVal = this.#em.config.get('forceUndefined') ? undefined : null;
422
+ const itemsMap = new Set();
423
+ const childrenMap = new Set();
424
+ // Use targetKey value if set, otherwise use serialized PK
425
+ const getKey = (e) => (prop.targetKey ? '' + e[prop.targetKey] : helper(e).getSerializedPrimaryKey());
426
+ for (const item of items) {
427
+ /* v8 ignore next */
428
+ itemsMap.add(getKey(item));
429
+ }
430
+ for (const child of children) {
431
+ childrenMap.add(getKey(child));
432
+ }
433
+ const isInverseOneToOne = prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner;
434
+ for (const entity of entities) {
435
+ const ref = entity[prop.name];
436
+ if (ref == null) {
437
+ continue;
438
+ }
439
+ const refKey = getKey(ref);
440
+ // For 1:1 inverse, `children` contains parent entities, so `childrenMap`
441
+ // has parent PKs — match against the entity's own PK, not the referenced entity's PK.
442
+ const childKey = isInverseOneToOne ? getKey(entity) : refKey;
443
+ if (childrenMap.has(childKey) && !itemsMap.has(refKey)) {
444
+ entity[prop.name] = nullVal;
445
+ helper(entity).__originalEntityData[prop.name] = null;
446
+ }
447
+ }
448
+ }
449
+ for (const item of items) {
450
+ if (ref && !helper(item).__onLoadFired) {
451
+ helper(item).__initialized = false;
452
+ this.#em.getUnitOfWork().unmarkAsLoaded(item);
453
+ }
454
+ }
455
+ return { items, partial };
456
+ }
457
+ mergePrimaryCondition(ids, pk, options, meta, metadata, platform) {
458
+ const cond1 = QueryHelper.processWhere({
459
+ where: { [pk]: { $in: ids } },
460
+ entityName: meta.class,
461
+ metadata,
462
+ platform,
463
+ convertCustomTypes: !options.convertCustomTypes,
464
+ });
465
+ const where = { ...options.where };
466
+ Utils.dropUndefinedProperties(where);
467
+ return where[pk] ? { $and: [cond1, where] } : { ...cond1, ...where };
468
+ }
469
+ async populateField(entityName, entities, populate, options) {
470
+ const field = populate.field.split(':')[0];
471
+ const prop = this.#metadata.find(entityName).properties[field];
472
+ if (prop.kind === ReferenceKind.SCALAR && !prop.lazy) {
473
+ return;
474
+ }
475
+ options = { ...options, filters: QueryHelper.mergePropertyFilters(prop.filters, options.filters) };
476
+ const populated = await this.populateMany(entityName, entities, populate, options);
477
+ if (!populate.children && !populate.all) {
478
+ return;
479
+ }
480
+ const children = [];
481
+ for (const entity of entities) {
482
+ const ref = entity[field];
483
+ if (Utils.isEntity(ref)) {
484
+ children.push(ref);
485
+ }
486
+ else if (Reference.isReference(ref)) {
487
+ children.push(ref.unwrap());
488
+ }
489
+ else if (Utils.isCollection(ref)) {
490
+ children.push(...ref.getItems());
491
+ }
492
+ else if (ref && prop.kind === ReferenceKind.EMBEDDED) {
493
+ children.push(...Utils.asArray(ref));
494
+ }
495
+ }
496
+ if (populated.length === 0 && !populate.children) {
497
+ // Populate child-specific relations for TPT entities already in the identity map (GH #7529).
498
+ // Pass a sentinel hint so the existing TPT child handling in `populate()` does the real work.
499
+ if (populate.all && prop.targetMeta?.inheritanceType === 'tpt') {
500
+ const visited = options.visited;
501
+ const pending = Utils.unique(children).filter(c => helper(c).__meta !== prop.targetMeta && !visited.has(c));
502
+ if (pending.length > 0) {
503
+ const hint = [
504
+ { field: prop.targetMeta.primaryKeys[0], strategy: LoadStrategy.SELECT_IN, all: true },
505
+ ];
506
+ await this.populate(prop.targetMeta.class, pending, hint, {
507
+ ...options,
508
+ lookup: false,
509
+ validate: false,
510
+ });
511
+ }
512
+ }
513
+ return;
514
+ }
515
+ const fields = this.buildFields(options.fields, prop);
516
+ const innerOrderBy = Utils.asArray(options.orderBy)
517
+ .filter(orderBy => Utils.isObject(orderBy[prop.name]))
518
+ .map(orderBy => orderBy[prop.name]);
519
+ const { refresh, filters, ignoreLazyScalarProperties, populateWhere, connectionType, logging, schema } = options;
520
+ // oxfmt-ignore
521
+ const exclude = Array.isArray(options.exclude) ? Utils.extractChildElements(options.exclude, prop.name) : options.exclude;
515
522
  const visited = options.visited;
516
- const pending = Utils.unique(children).filter(c => helper(c).__meta !== prop.targetMeta && !visited.has(c));
517
- if (pending.length > 0) {
518
- const hint = [{ field: prop.targetMeta.primaryKeys[0], strategy: LoadStrategy.SELECT_IN, all: true }];
519
- await this.populate(prop.targetMeta.class, pending, hint, {
520
- ...options,
521
- lookup: false,
522
- validate: false,
523
- });
523
+ for (const entity of entities) {
524
+ visited.delete(entity);
525
+ }
526
+ const unique = Utils.unique(children);
527
+ const filtered = unique.filter(e => !visited.has(e));
528
+ for (const entity of entities) {
529
+ visited.add(entity);
530
+ }
531
+ if (!prop.targetMeta) {
532
+ return;
533
+ }
534
+ const populateChildren = async (targetMeta, items) => {
535
+ await this.populate(targetMeta.class, items, populate.children ?? populate.all, {
536
+ where: (await this.extractChildCondition(options, prop, false)),
537
+ orderBy: innerOrderBy,
538
+ fields,
539
+ exclude,
540
+ validate: false,
541
+ lookup: false,
542
+ filters,
543
+ ignoreLazyScalarProperties,
544
+ populateWhere,
545
+ connectionType,
546
+ logging,
547
+ schema,
548
+ // @ts-ignore not a public option, will be propagated to the populate call
549
+ refresh: refresh && !filtered.every(item => options.visited.has(item)),
550
+ // @ts-ignore not a public option, will be propagated to the populate call
551
+ visited: options.visited,
552
+ // @ts-ignore not a public option
553
+ filtered,
554
+ });
555
+ };
556
+ if (prop.polymorphic && prop.polymorphTargets) {
557
+ await Promise.all(prop.polymorphTargets.map(async (targetMeta) => {
558
+ const targetChildren = unique.filter(child => {
559
+ const childMeta = helper(child).__meta;
560
+ // Use root.className for TPT entities: a Dog (root: Animal) should match
561
+ // targetMeta Animal. For non-TPT entities, root === self.
562
+ return childMeta.root.className === targetMeta.root.className;
563
+ });
564
+ if (targetChildren.length > 0) {
565
+ await populateChildren(targetMeta, targetChildren);
566
+ }
567
+ }));
568
+ }
569
+ else {
570
+ await populateChildren(prop.targetMeta, unique);
524
571
  }
525
- }
526
- return;
527
- }
528
- const fields = this.buildFields(options.fields, prop);
529
- const innerOrderBy = Utils.asArray(options.orderBy)
530
- .filter(orderBy => Utils.isObject(orderBy[prop.name]))
531
- .map(orderBy => orderBy[prop.name]);
532
- const { refresh, filters, ignoreLazyScalarProperties, populateWhere, connectionType, logging, schema } = options;
533
- // oxfmt-ignore
534
- const exclude = Array.isArray(options.exclude) ? Utils.extractChildElements(options.exclude, prop.name) : options.exclude;
535
- const visited = options.visited;
536
- for (const entity of entities) {
537
- visited.delete(entity);
538
- }
539
- const unique = Utils.unique(children);
540
- const filtered = unique.filter(e => !visited.has(e));
541
- for (const entity of entities) {
542
- visited.add(entity);
543
- }
544
- if (!prop.targetMeta) {
545
- return;
546
- }
547
- const populateChildren = async (targetMeta, items) => {
548
- await this.populate(targetMeta.class, items, populate.children ?? populate.all, {
549
- where: await this.extractChildCondition(options, prop, false),
550
- orderBy: innerOrderBy,
551
- fields,
552
- exclude,
553
- validate: false,
554
- lookup: false,
555
- filters,
556
- ignoreLazyScalarProperties,
557
- populateWhere,
558
- connectionType,
559
- logging,
560
- schema,
561
- // @ts-ignore not a public option, will be propagated to the populate call
562
- refresh: refresh && !filtered.every(item => options.visited.has(item)),
563
- // @ts-ignore not a public option, will be propagated to the populate call
564
- visited: options.visited,
565
- // @ts-ignore not a public option
566
- filtered,
567
- });
568
- };
569
- if (prop.polymorphic && prop.polymorphTargets) {
570
- await Promise.all(
571
- prop.polymorphTargets.map(async targetMeta => {
572
- const targetChildren = unique.filter(child => {
573
- const childMeta = helper(child).__meta;
574
- // Use root.className for TPT entities: a Dog (root: Animal) should match
575
- // targetMeta Animal. For non-TPT entities, root === self.
576
- return childMeta.root.className === targetMeta.root.className;
577
- });
578
- if (targetChildren.length > 0) {
579
- await populateChildren(targetMeta, targetChildren);
580
- }
581
- }),
582
- );
583
- } else {
584
- await populateChildren(prop.targetMeta, unique);
585
- }
586
- }
587
- /** @internal */
588
- async findChildrenFromPivotTable(filtered, prop, options, orderBy, populate, pivotJoin) {
589
- const ids = filtered.map(e => e.__helper.__primaryKeys);
590
- const refresh = options.refresh;
591
- let where = await this.extractChildCondition(options, prop, true);
592
- const fields = this.buildFields(options.fields, prop);
593
- // oxfmt-ignore
594
- const exclude = Array.isArray(options.exclude) ? Utils.extractChildElements(options.exclude, prop.name) : options.exclude;
595
- const populateFilter = options.populateFilter?.[prop.name];
596
- const options2 = { ...options, fields, exclude, populateFilter };
597
- ['limit', 'offset', 'first', 'last', 'before', 'after', 'overfetch'].forEach(prop => delete options2[prop]);
598
- options2.populate = populate?.children ?? [];
599
- if (!Utils.isEmpty(prop.where)) {
600
- where = { $and: [where, prop.where] };
601
572
  }
602
- const map = await this.#driver.loadFromPivotTable(
603
- prop,
604
- ids,
605
- where,
606
- orderBy,
607
- this.#em.getTransactionContext(),
608
- options2,
609
- pivotJoin,
610
- );
611
- const children = [];
612
- for (let i = 0; i < filtered.length; i++) {
613
- const entity = filtered[i];
614
- const items = map[Utils.getPrimaryKeyHash(ids[i])].map(item => {
615
- if (pivotJoin) {
616
- return this.#em.getReference(prop.targetMeta.class, item, {
617
- convertCustomTypes: true,
618
- schema: options.schema ?? this.#em.config.get('schema'),
619
- });
620
- }
621
- const entity = this.#em.getEntityFactory().create(prop.targetMeta.class, item, {
622
- refresh,
623
- merge: true,
624
- convertCustomTypes: true,
625
- schema: options.schema ?? this.#em.config.get('schema'),
573
+ /** @internal */
574
+ async findChildrenFromPivotTable(filtered, prop, options, orderBy, populate, pivotJoin) {
575
+ const ids = filtered.map(e => e.__helper.__primaryKeys);
576
+ const refresh = options.refresh;
577
+ let where = await this.extractChildCondition(options, prop, true);
578
+ const fields = this.buildFields(options.fields, prop);
579
+ // oxfmt-ignore
580
+ const exclude = Array.isArray(options.exclude) ? Utils.extractChildElements(options.exclude, prop.name) : options.exclude;
581
+ const populateFilter = options.populateFilter?.[prop.name];
582
+ const options2 = { ...options, fields, exclude, populateFilter };
583
+ ['limit', 'offset', 'first', 'last', 'before', 'after', 'overfetch'].forEach(prop => delete options2[prop]);
584
+ options2.populate = populate?.children ?? [];
585
+ if (!Utils.isEmpty(prop.where)) {
586
+ where = { $and: [where, prop.where] };
587
+ }
588
+ const map = await this.#driver.loadFromPivotTable(prop, ids, where, orderBy, this.#em.getTransactionContext(), options2, pivotJoin);
589
+ const children = [];
590
+ for (let i = 0; i < filtered.length; i++) {
591
+ const entity = filtered[i];
592
+ const items = map[Utils.getPrimaryKeyHash(ids[i])].map(item => {
593
+ if (pivotJoin) {
594
+ return this.#em.getReference(prop.targetMeta.class, item, {
595
+ convertCustomTypes: true,
596
+ schema: options.schema ?? this.#em.config.get('schema'),
597
+ });
598
+ }
599
+ const entity = this.#em.getEntityFactory().create(prop.targetMeta.class, item, {
600
+ refresh,
601
+ merge: true,
602
+ convertCustomTypes: true,
603
+ schema: options.schema ?? this.#em.config.get('schema'),
604
+ });
605
+ return this.#em.getUnitOfWork().register(entity, item, { refresh, loaded: true });
606
+ });
607
+ entity[prop.name].hydrate(items, true);
608
+ children.push(items);
609
+ }
610
+ return children;
611
+ }
612
+ async extractChildCondition(options, prop, filters = false) {
613
+ const where = options.where;
614
+ const subCond = Utils.isPlainObject(where[prop.name]) ? where[prop.name] : {};
615
+ const meta2 = prop.targetMeta;
616
+ const pk = Utils.getPrimaryKeyHash(meta2.primaryKeys);
617
+ ['$and', '$or'].forEach(op => {
618
+ if (where[op]) {
619
+ const child = where[op]
620
+ .map((cond) => cond[prop.name])
621
+ .filter((sub) => sub != null &&
622
+ !(Utils.isPlainObject(sub) && Utils.getObjectQueryKeys(sub).every(key => Utils.isOperator(key, false))))
623
+ .map((cond) => {
624
+ if (Utils.isPrimaryKey(cond)) {
625
+ return { [pk]: cond };
626
+ }
627
+ return cond;
628
+ });
629
+ if (child.length > 0) {
630
+ subCond[op] = child;
631
+ }
632
+ }
626
633
  });
627
- return this.#em.getUnitOfWork().register(entity, item, { refresh, loaded: true });
628
- });
629
- entity[prop.name].hydrate(items, true);
630
- children.push(items);
634
+ const operators = Object.keys(subCond).filter(key => Utils.isOperator(key, false));
635
+ if (operators.length > 0) {
636
+ operators.forEach(op => {
637
+ subCond[pk] ??= {};
638
+ subCond[pk][op] = subCond[op];
639
+ delete subCond[op];
640
+ });
641
+ }
642
+ if (filters) {
643
+ return this.#em.applyFilters(meta2.class, subCond, options.filters, 'read', options);
644
+ }
645
+ return subCond;
631
646
  }
632
- return children;
633
- }
634
- async extractChildCondition(options, prop, filters = false) {
635
- const where = options.where;
636
- const subCond = Utils.isPlainObject(where[prop.name]) ? where[prop.name] : {};
637
- const meta2 = prop.targetMeta;
638
- const pk = Utils.getPrimaryKeyHash(meta2.primaryKeys);
639
- ['$and', '$or'].forEach(op => {
640
- if (where[op]) {
641
- const child = where[op]
642
- .map(cond => cond[prop.name])
643
- .filter(
644
- sub =>
645
- sub != null &&
646
- !(Utils.isPlainObject(sub) && Utils.getObjectQueryKeys(sub).every(key => Utils.isOperator(key, false))),
647
- )
648
- .map(cond => {
649
- if (Utils.isPrimaryKey(cond)) {
650
- return { [pk]: cond };
651
- }
652
- return cond;
653
- });
654
- if (child.length > 0) {
655
- subCond[op] = child;
656
- }
657
- }
658
- });
659
- const operators = Object.keys(subCond).filter(key => Utils.isOperator(key, false));
660
- if (operators.length > 0) {
661
- operators.forEach(op => {
662
- subCond[pk] ??= {};
663
- subCond[pk][op] = subCond[op];
664
- delete subCond[op];
665
- });
647
+ buildFields(fields = [], prop, ref) {
648
+ if (ref) {
649
+ fields = prop.targetMeta.primaryKeys.map(targetPkName => `${prop.name}.${targetPkName}`);
650
+ }
651
+ const ret = fields.reduce((ret, f) => {
652
+ if (Utils.isPlainObject(f)) {
653
+ Utils.keys(f)
654
+ .filter(ff => ff === prop.name)
655
+ .forEach(ff => ret.push(...f[ff]));
656
+ }
657
+ else if (f.toString().includes('.')) {
658
+ const parts = f.toString().split('.');
659
+ const propName = parts.shift();
660
+ const childPropName = parts.join('.');
661
+ /* v8 ignore next */
662
+ if (propName === prop.name) {
663
+ ret.push(childPropName);
664
+ }
665
+ }
666
+ return ret;
667
+ }, []);
668
+ // we need to automatically select the FKs too, e.g. for 1:m relations to be able to wire them with the items
669
+ if (prop.kind === ReferenceKind.ONE_TO_MANY || prop.kind === ReferenceKind.MANY_TO_MANY) {
670
+ const owner = prop.targetMeta.properties[prop.mappedBy];
671
+ // when the owning FK is lazy, we need to explicitly select it even without user-provided fields,
672
+ // otherwise the driver will exclude it and we won't be able to map children to their parent collections
673
+ if (owner && !ret.includes(owner.name) && (ret.length > 0 || owner.lazy)) {
674
+ ret.push(owner.name);
675
+ }
676
+ }
677
+ if (ret.length === 0) {
678
+ return undefined;
679
+ }
680
+ return ret;
666
681
  }
667
- if (filters) {
668
- return this.#em.applyFilters(meta2.class, subCond, options.filters, 'read', options);
682
+ getChildReferences(entities, prop, options, ref) {
683
+ const filtered = this.filterCollections(entities, prop.name, options, ref);
684
+ if (prop.kind === ReferenceKind.ONE_TO_MANY) {
685
+ return filtered.map(e => e[prop.name].owner);
686
+ }
687
+ if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.owner) {
688
+ return filtered.reduce((a, b) => {
689
+ a.push(...b[prop.name].getItems());
690
+ return a;
691
+ }, []);
692
+ }
693
+ if (prop.kind === ReferenceKind.MANY_TO_MANY) {
694
+ // inverse side
695
+ return filtered;
696
+ }
697
+ // MANY_TO_ONE or ONE_TO_ONE
698
+ return this.filterReferences(entities, prop.name, options, ref);
669
699
  }
670
- return subCond;
671
- }
672
- buildFields(fields = [], prop, ref) {
673
- if (ref) {
674
- fields = prop.targetMeta.primaryKeys.map(targetPkName => `${prop.name}.${targetPkName}`);
700
+ filterCollections(entities, field, options, ref) {
701
+ if (options.refresh) {
702
+ return entities.filter(e => e[field]);
703
+ }
704
+ return entities.filter(e => Utils.isCollection(e[field]) && !e[field].isInitialized(!ref));
675
705
  }
676
- const ret = fields.reduce((ret, f) => {
677
- if (Utils.isPlainObject(f)) {
678
- Utils.keys(f)
679
- .filter(ff => ff === prop.name)
680
- .forEach(ff => ret.push(...f[ff]));
681
- } else if (f.toString().includes('.')) {
682
- const parts = f.toString().split('.');
683
- const propName = parts.shift();
684
- const childPropName = parts.join('.');
706
+ isPropertyLoaded(entity, field) {
707
+ if (!entity || field === '*') {
708
+ return true;
709
+ }
710
+ const wrapped = helper(entity);
711
+ if (!field.includes('.')) {
712
+ return wrapped.__loadedProperties.has(field);
713
+ }
714
+ const [f, ...r] = field.split('.');
685
715
  /* v8 ignore next */
686
- if (propName === prop.name) {
687
- ret.push(childPropName);
688
- }
689
- }
690
- return ret;
691
- }, []);
692
- // we need to automatically select the FKs too, e.g. for 1:m relations to be able to wire them with the items
693
- if (prop.kind === ReferenceKind.ONE_TO_MANY || prop.kind === ReferenceKind.MANY_TO_MANY) {
694
- const owner = prop.targetMeta.properties[prop.mappedBy];
695
- // when the owning FK is lazy, we need to explicitly select it even without user-provided fields,
696
- // otherwise the driver will exclude it and we won't be able to map children to their parent collections
697
- if (owner && !ret.includes(owner.name) && (ret.length > 0 || owner.lazy)) {
698
- ret.push(owner.name);
699
- }
700
- }
701
- if (ret.length === 0) {
702
- return undefined;
703
- }
704
- return ret;
705
- }
706
- getChildReferences(entities, prop, options, ref) {
707
- const filtered = this.filterCollections(entities, prop.name, options, ref);
708
- if (prop.kind === ReferenceKind.ONE_TO_MANY) {
709
- return filtered.map(e => e[prop.name].owner);
710
- }
711
- if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.owner) {
712
- return filtered.reduce((a, b) => {
713
- a.push(...b[prop.name].getItems());
714
- return a;
715
- }, []);
716
- }
717
- if (prop.kind === ReferenceKind.MANY_TO_MANY) {
718
- // inverse side
719
- return filtered;
720
- }
721
- // MANY_TO_ONE or ONE_TO_ONE
722
- return this.filterReferences(entities, prop.name, options, ref);
723
- }
724
- filterCollections(entities, field, options, ref) {
725
- if (options.refresh) {
726
- return entities.filter(e => e[field]);
727
- }
728
- return entities.filter(e => Utils.isCollection(e[field]) && !e[field].isInitialized(!ref));
729
- }
730
- isPropertyLoaded(entity, field) {
731
- if (!entity || field === '*') {
732
- return true;
733
- }
734
- const wrapped = helper(entity);
735
- if (!field.includes('.')) {
736
- return wrapped.__loadedProperties.has(field);
737
- }
738
- const [f, ...r] = field.split('.');
739
- /* v8 ignore next */
740
- if (!wrapped.__loadedProperties.has(f) || !wrapped.__meta.properties[f]?.targetMeta) {
741
- return false;
742
- }
743
- if ([ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(wrapped.__meta.properties[f].kind)) {
744
- return entity[f].getItems(false).every(item => this.isPropertyLoaded(item, r.join('.')));
745
- }
746
- return this.isPropertyLoaded(entity[f], r.join('.'));
747
- }
748
- filterReferences(entities, field, options, ref) {
749
- if (ref) {
750
- return [];
716
+ if (!wrapped.__loadedProperties.has(f) || !wrapped.__meta.properties[f]?.targetMeta) {
717
+ return false;
718
+ }
719
+ if ([ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(wrapped.__meta.properties[f].kind)) {
720
+ return entity[f].getItems(false).every((item) => this.isPropertyLoaded(item, r.join('.')));
721
+ }
722
+ return this.isPropertyLoaded(entity[f], r.join('.'));
751
723
  }
752
- const children = entities.filter(e => Utils.isEntity(e[field], true));
753
- if (options.refresh) {
754
- return children.map(e => Reference.unwrapReference(e[field]));
724
+ filterReferences(entities, field, options, ref) {
725
+ if (ref) {
726
+ return [];
727
+ }
728
+ const children = entities.filter(e => Utils.isEntity(e[field], true));
729
+ if (options.refresh) {
730
+ return children.map(e => Reference.unwrapReference(e[field]));
731
+ }
732
+ if (options.fields) {
733
+ return children
734
+ .map(e => Reference.unwrapReference(e[field]))
735
+ .filter(target => {
736
+ const wrapped = helper(target);
737
+ const childFields = options.fields
738
+ .filter(f => f.startsWith(`${field}.`))
739
+ .map(f => f.substring(field.length + 1));
740
+ return !wrapped.__initialized || !childFields.every(cf => this.isPropertyLoaded(target, cf));
741
+ });
742
+ }
743
+ return children
744
+ .filter(e => !e[field].__helper.__initialized)
745
+ .map(e => Reference.unwrapReference(e[field]));
755
746
  }
756
- if (options.fields) {
757
- return children
758
- .map(e => Reference.unwrapReference(e[field]))
759
- .filter(target => {
760
- const wrapped = helper(target);
761
- const childFields = options.fields
762
- .filter(f => f.startsWith(`${field}.`))
763
- .map(f => f.substring(field.length + 1));
764
- return !wrapped.__initialized || !childFields.every(cf => this.isPropertyLoaded(target, cf));
747
+ filterByReferences(entities, field, refresh) {
748
+ /* v8 ignore next */
749
+ if (refresh) {
750
+ return entities;
751
+ }
752
+ return entities.filter(e => e[field] !== null && !e[field]?.__helper?.__initialized);
753
+ }
754
+ lookupAllRelationships(entityName) {
755
+ const ret = [];
756
+ const meta = this.#metadata.find(entityName);
757
+ meta.relations.forEach(prop => {
758
+ ret.push({
759
+ field: this.getRelationName(meta, prop),
760
+ // force select-in strategy when populating all relations as otherwise we could cause infinite loops when self-referencing
761
+ strategy: LoadStrategy.SELECT_IN,
762
+ // no need to look up populate children recursively as we just pass `all: true` here
763
+ all: true,
764
+ });
765
765
  });
766
+ // For TPT parents with child types, keep an all:true sentinel so the populate
767
+ // loop doesn't exit early and the TPT child relation population can run after it.
768
+ if (ret.length === 0 && meta.inheritanceType === 'tpt' && meta.tptChildren?.length) {
769
+ ret.push({ field: meta.primaryKeys[0], strategy: LoadStrategy.SELECT_IN, all: true });
770
+ }
771
+ return ret;
766
772
  }
767
- return children.filter(e => !e[field].__helper.__initialized).map(e => Reference.unwrapReference(e[field]));
768
- }
769
- filterByReferences(entities, field, refresh) {
770
- /* v8 ignore next */
771
- if (refresh) {
772
- return entities;
773
- }
774
- return entities.filter(e => e[field] !== null && !e[field]?.__helper?.__initialized);
775
- }
776
- lookupAllRelationships(entityName) {
777
- const ret = [];
778
- const meta = this.#metadata.find(entityName);
779
- meta.relations.forEach(prop => {
780
- ret.push({
781
- field: this.getRelationName(meta, prop),
782
- // force select-in strategy when populating all relations as otherwise we could cause infinite loops when self-referencing
783
- strategy: LoadStrategy.SELECT_IN,
784
- // no need to look up populate children recursively as we just pass `all: true` here
785
- all: true,
786
- });
787
- });
788
- // For TPT parents with child types, keep an all:true sentinel so the populate
789
- // loop doesn't exit early and the TPT child relation population can run after it.
790
- if (ret.length === 0 && meta.inheritanceType === 'tpt' && meta.tptChildren?.length) {
791
- ret.push({ field: meta.primaryKeys[0], strategy: LoadStrategy.SELECT_IN, all: true });
792
- }
793
- return ret;
794
- }
795
- getRelationName(meta, prop) {
796
- if (!prop.embedded) {
797
- return prop.name;
798
- }
799
- return `${this.getRelationName(meta, meta.properties[prop.embedded[0]])}.${prop.embedded[1]}`;
800
- }
801
- lookupEagerLoadedRelationships(entityName, populate, strategy, prefix = '', visited = [], exclude) {
802
- const meta = this.#metadata.find(entityName);
803
- if (!meta && !prefix) {
804
- return populate;
773
+ getRelationName(meta, prop) {
774
+ if (!prop.embedded) {
775
+ return prop.name;
776
+ }
777
+ return `${this.getRelationName(meta, meta.properties[prop.embedded[0]])}.${prop.embedded[1]}`;
805
778
  }
806
- if (!meta || visited.includes(meta)) {
807
- return [];
779
+ lookupEagerLoadedRelationships(entityName, populate, strategy, prefix = '', visited = [], exclude) {
780
+ const meta = this.#metadata.find(entityName);
781
+ if (!meta && !prefix) {
782
+ return populate;
783
+ }
784
+ if (!meta || visited.includes(meta)) {
785
+ return [];
786
+ }
787
+ visited.push(meta);
788
+ const ret = prefix === '' ? [...populate] : [];
789
+ meta.relations
790
+ .filter(prop => {
791
+ const field = this.getRelationName(meta, prop);
792
+ const prefixed = prefix ? `${prefix}.${field}` : field;
793
+ const isExcluded = exclude?.includes(prefixed);
794
+ const eager = prop.eager && !populate.some(p => p.field === `${prop.name}:ref`);
795
+ const populated = populate.some(p => p.field === prop.name);
796
+ const disabled = populate.some(p => p.field === prop.name && p.all === false);
797
+ return !disabled && !isExcluded && (eager || populated);
798
+ })
799
+ .forEach(prop => {
800
+ const field = this.getRelationName(meta, prop);
801
+ const prefixed = prefix ? `${prefix}.${field}` : field;
802
+ const nestedPopulate = populate
803
+ .filter(p => p.field === prop.name)
804
+ .flatMap(p => p.children)
805
+ .filter(Boolean);
806
+ const nested = this.lookupEagerLoadedRelationships(prop.targetMeta.class, nestedPopulate, strategy, prefixed, visited.slice(), exclude);
807
+ if (nested.length > 0) {
808
+ ret.push(...nested);
809
+ }
810
+ else {
811
+ const selfReferencing = [meta.tableName, ...visited.map(m => m.tableName)].includes(prop.targetMeta.tableName) && prop.eager;
812
+ ret.push({
813
+ field: prefixed,
814
+ // enforce select-in strategy for self-referencing relations
815
+ strategy: selfReferencing ? LoadStrategy.SELECT_IN : (strategy ?? prop.strategy),
816
+ });
817
+ }
818
+ });
819
+ return ret;
808
820
  }
809
- visited.push(meta);
810
- const ret = prefix === '' ? [...populate] : [];
811
- meta.relations
812
- .filter(prop => {
813
- const field = this.getRelationName(meta, prop);
814
- const prefixed = prefix ? `${prefix}.${field}` : field;
815
- const isExcluded = exclude?.includes(prefixed);
816
- const eager = prop.eager && !populate.some(p => p.field === `${prop.name}:ref`);
817
- const populated = populate.some(p => p.field === prop.name);
818
- const disabled = populate.some(p => p.field === prop.name && p.all === false);
819
- return !disabled && !isExcluded && (eager || populated);
820
- })
821
- .forEach(prop => {
822
- const field = this.getRelationName(meta, prop);
823
- const prefixed = prefix ? `${prefix}.${field}` : field;
824
- const nestedPopulate = populate
825
- .filter(p => p.field === prop.name)
826
- .flatMap(p => p.children)
827
- .filter(Boolean);
828
- const nested = this.lookupEagerLoadedRelationships(
829
- prop.targetMeta.class,
830
- nestedPopulate,
831
- strategy,
832
- prefixed,
833
- visited.slice(),
834
- exclude,
835
- );
836
- if (nested.length > 0) {
837
- ret.push(...nested);
838
- } else {
839
- const selfReferencing =
840
- [meta.tableName, ...visited.map(m => m.tableName)].includes(prop.targetMeta.tableName) && prop.eager;
841
- ret.push({
842
- field: prefixed,
843
- // enforce select-in strategy for self-referencing relations
844
- strategy: selfReferencing ? LoadStrategy.SELECT_IN : (strategy ?? prop.strategy),
845
- });
846
- }
847
- });
848
- return ret;
849
- }
850
821
  }