@mikro-orm/core 7.0.2-dev.8 → 7.0.2

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