@mikro-orm/core 7.0.8-dev.7 → 7.0.8

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