@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,4 +1,4 @@
1
- import { EntityMetadata, } from '../typings.js';
1
+ import { EntityMetadata } from '../typings.js';
2
2
  import { compareArrays, Utils } from '../utils/Utils.js';
3
3
  import { QueryHelper } from '../utils/QueryHelper.js';
4
4
  import { MetadataValidator } from './MetadataValidator.js';
@@ -11,1872 +11,1963 @@ import { t, Type } from '../types/index.js';
11
11
  import { colors } from '../logging/colors.js';
12
12
  import { raw, Raw } from '../utils/RawQueryFragment.js';
13
13
  import { BaseEntity } from '../entity/BaseEntity.js';
14
+ /** Discovers, validates, and processes entity metadata from configured sources. */
14
15
  export class MetadataDiscovery {
15
- #namingStrategy;
16
- #metadataProvider;
17
- #logger;
18
- #schemaHelper;
19
- #validator = new MetadataValidator();
20
- #discovered = [];
21
- #metadata;
22
- #platform;
23
- #config;
24
- constructor(metadata, platform, config) {
25
- this.#metadata = metadata;
26
- this.#platform = platform;
27
- this.#config = config;
28
- this.#namingStrategy = this.#config.getNamingStrategy();
29
- this.#metadataProvider = this.#config.getMetadataProvider();
30
- this.#logger = this.#config.getLogger();
31
- this.#schemaHelper = this.#platform.getSchemaHelper();
32
- }
33
- async discover(preferTs = true) {
34
- this.#discovered.length = 0;
35
- const startTime = Date.now();
36
- const suffix = this.#metadataProvider.constructor === MetadataProvider
37
- ? ''
38
- : `, using ${colors.cyan(this.#metadataProvider.constructor.name)}`;
39
- this.#logger.log('discovery', `ORM entity discovery started${suffix}`);
40
- await this.findEntities(preferTs);
41
- for (const meta of this.#discovered) {
42
- /* v8 ignore next */
43
- await this.#config.get('discovery').onMetadata?.(meta, this.#platform);
44
- }
45
- this.processDiscoveredEntities(this.#discovered);
46
- const diff = Date.now() - startTime;
47
- this.#logger.log('discovery', `- entity discovery finished, found ${colors.green('' + this.#discovered.length)} entities, took ${colors.green(`${diff} ms`)}`);
48
- const storage = this.mapDiscoveredEntities();
49
- /* v8 ignore next */
50
- await this.#config.get('discovery').afterDiscovered?.(storage, this.#platform);
51
- return storage;
52
- }
53
- discoverSync() {
54
- this.#discovered.length = 0;
55
- const startTime = Date.now();
56
- const suffix = this.#metadataProvider.constructor === MetadataProvider
57
- ? ''
58
- : `, using ${colors.cyan(this.#metadataProvider.constructor.name)}`;
59
- this.#logger.log('discovery', `ORM entity discovery started${suffix} in sync mode`);
60
- const refs = this.#config.get('entities');
61
- this.discoverReferences(refs);
62
- for (const meta of this.#discovered) {
63
- /* v8 ignore next */
64
- void this.#config.get('discovery').onMetadata?.(meta, this.#platform);
65
- }
66
- this.processDiscoveredEntities(this.#discovered);
67
- const diff = Date.now() - startTime;
68
- this.#logger.log('discovery', `- entity discovery finished, found ${colors.green('' + this.#discovered.length)} entities, took ${colors.green(`${diff} ms`)}`);
69
- const storage = this.mapDiscoveredEntities();
70
- /* v8 ignore next */
71
- void this.#config.get('discovery').afterDiscovered?.(storage, this.#platform);
72
- return storage;
73
- }
74
- mapDiscoveredEntities() {
75
- const discovered = new MetadataStorage();
76
- this.#discovered
77
- .filter(meta => meta.root.name)
78
- .sort((a, b) => b.root.name.localeCompare(a.root.name))
79
- .forEach(meta => {
80
- this.#platform.validateMetadata(meta);
81
- discovered.set(meta.class, meta);
82
- });
83
- for (const meta of discovered) {
84
- meta.root = discovered.get(meta.root.class);
85
- if (meta.inheritanceType === 'tpt') {
86
- this.computeTPTOwnProps(meta);
87
- }
88
- }
89
- return discovered;
90
- }
91
- initAccessors(meta) {
92
- for (const prop of Object.values(meta.properties)) {
93
- if (!prop.accessor || meta.properties[prop.accessor]) {
94
- continue;
95
- }
96
- const desc = Object.getOwnPropertyDescriptor(meta.prototype, prop.name);
97
- if (desc?.get || desc?.set) {
98
- this.initRelation(prop);
99
- this.initFieldName(prop);
100
- const accessor = prop.name;
101
- prop.name = typeof prop.accessor === 'string' ? prop.accessor : prop.name;
102
- if (prop.accessor === true) {
103
- prop.getter = prop.setter = true;
104
- }
105
- else {
106
- prop.getter = prop.setter = false;
107
- }
108
- prop.accessor = accessor;
109
- prop.serializedName ??= accessor;
110
- Utils.renameKey(meta.properties, accessor, prop.name);
111
- }
112
- else {
113
- const name = prop.name;
114
- if (prop.kind === ReferenceKind.SCALAR || prop.kind === ReferenceKind.EMBEDDED) {
115
- prop.name = prop.accessor;
116
- }
117
- this.initRelation(prop);
118
- this.initFieldName(prop);
119
- prop.serializedName ??= prop.accessor;
120
- prop.name = name;
121
- }
122
- }
123
- }
124
- processDiscoveredEntities(discovered) {
125
- for (const meta of discovered) {
126
- let i = 1;
127
- Object.values(meta.properties).forEach(prop => meta.propertyOrder.set(prop.name, i++));
128
- Object.values(meta.properties).forEach(prop => this.initPolyEmbeddables(prop, discovered));
129
- this.initAccessors(meta);
130
- }
131
- // ignore base entities (not annotated with @Entity)
132
- const filtered = discovered.filter(meta => meta.root.name);
133
- // sort so we discover entities first to get around issues with nested embeddables
134
- filtered.sort((a, b) => (!a.embeddable === !b.embeddable ? 0 : a.embeddable ? 1 : -1));
135
- filtered.forEach(meta => this.initSingleTableInheritance(meta, filtered));
136
- filtered.forEach(meta => this.initTPTRelationships(meta, filtered));
137
- filtered.forEach(meta => this.defineBaseEntityProperties(meta));
138
- filtered.forEach(meta => {
139
- const newMeta = EntitySchema.fromMetadata(meta).init().meta;
140
- return this.#metadata.set(newMeta.class, newMeta);
141
- });
142
- filtered.forEach(meta => this.initAutoincrement(meta));
143
- const forEachProp = (cb) => {
144
- filtered.forEach(meta => Object.values(meta.properties).forEach(prop => cb(meta, prop)));
145
- };
146
- forEachProp((m, p) => this.initFactoryField(m, p));
147
- forEachProp((m, p) => this.initPolymorphicRelation(m, p, filtered));
148
- forEachProp((_m, p) => this.initRelation(p));
149
- forEachProp((m, p) => this.initEmbeddables(m, p));
150
- forEachProp((_m, p) => this.initFieldName(p));
151
- filtered.forEach(meta => this.finalizeTPTInheritance(meta, filtered));
152
- filtered.forEach(meta => this.computeTPTOwnProps(meta));
153
- forEachProp((m, p) => this.initVersionProperty(m, p));
154
- forEachProp((m, p) => this.initCustomType(m, p));
155
- forEachProp((m, p) => this.initGeneratedColumn(m, p));
156
- filtered.forEach(meta => this.initAutoincrement(meta)); // once again after we init custom types
157
- filtered.forEach(meta => this.initCheckConstraints(meta));
158
- forEachProp((_m, p) => {
159
- this.initDefaultValue(p);
160
- this.inferTypeFromDefault(p);
161
- this.initRelation(p);
162
- this.initColumnType(p);
163
- });
164
- forEachProp((m, p) => this.initIndexes(m, p));
165
- filtered.forEach(meta => this.autoWireBidirectionalProperties(meta));
166
- for (const meta of filtered) {
167
- discovered.push(...this.processEntity(meta));
168
- }
169
- discovered.forEach(meta => meta.sync(true));
170
- this.#metadataProvider.combineCache();
171
- return discovered.map(meta => {
172
- meta = this.#metadata.get(meta.class);
173
- meta.sync(true);
174
- this.findReferencingProperties(meta, filtered);
175
- if (meta.inheritanceType === 'tpt') {
176
- this.computeTPTOwnProps(meta);
177
- }
178
- return meta;
179
- });
180
- }
181
- async findEntities(preferTs) {
182
- const { entities, entitiesTs, baseDir } = this.#config.getAll();
183
- const targets = preferTs && entitiesTs.length > 0 ? entitiesTs : entities;
184
- const processed = [];
185
- const paths = [];
186
- for (const entity of targets) {
187
- if (typeof entity === 'string') {
188
- paths.push(entity);
189
- }
190
- else {
191
- processed.push(entity);
192
- }
193
- }
194
- if (paths.length > 0) {
195
- const { discoverEntities } = await import('@mikro-orm/core/file-discovery');
196
- processed.push(...(await discoverEntities(paths, { baseDir })));
197
- }
198
- return this.discoverReferences(processed);
199
- }
200
- discoverMissingTargets() {
201
- const unwrap = (type) => type
202
- .replace(/Array<(.*)>/, '$1') // unwrap array
203
- .replace(/\[]$/, '') // remove array suffix
204
- .replace(/\((.*)\)/, '$1'); // unwrap union types
205
- const missing = [];
206
- this.#discovered.forEach(meta => Object.values(meta.properties).forEach(prop => {
207
- if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.pivotEntity) {
208
- const pivotEntity = prop.pivotEntity;
209
- const target = typeof pivotEntity === 'function' && !pivotEntity.prototype
210
- ? pivotEntity()
211
- : pivotEntity;
212
- if (!this.#discovered.find(m => m.className === Utils.className(target))) {
213
- missing.push(target);
214
- }
215
- }
216
- if (prop.kind !== ReferenceKind.SCALAR) {
217
- const target = typeof prop.entity === 'function' && !prop.entity.prototype ? prop.entity() : prop.type;
218
- if (!unwrap(prop.type)
219
- .split(/ ?\| ?/)
220
- .every(type => this.#discovered.find(m => m.className === type))) {
221
- missing.push(...Utils.asArray(target));
222
- }
223
- }
224
- }));
225
- if (missing.length > 0) {
226
- this.tryDiscoverTargets(missing);
227
- }
228
- }
229
- tryDiscoverTargets(targets) {
230
- for (const target of targets) {
231
- const schema = target instanceof EntitySchema ? target : undefined;
232
- const isDiscoverable = typeof target === 'function' || schema;
233
- if (isDiscoverable && target.name) {
234
- // Get the actual class for EntitySchema, or use target directly for classes
235
- const targetClass = schema ? schema.meta.class : target;
236
- if (!this.#metadata.has(targetClass)) {
237
- this.discoverReferences([target], false);
238
- this.discoverMissingTargets();
239
- }
240
- }
241
- }
242
- }
243
- discoverReferences(refs, validate = true) {
244
- const found = [];
245
- for (const entity of refs) {
246
- if (typeof entity === 'string') {
247
- throw new Error('Folder based discovery requires the async `MikroORM.init()` method.');
248
- }
249
- const schema = this.getSchema(entity);
250
- const meta = schema.init().meta;
251
- this.#metadata.set(meta.class, meta);
252
- found.push(schema);
253
- }
254
- // discover parents (base entities) automatically
255
- for (const meta of this.#metadata) {
256
- let parent = meta.extends;
257
- if (parent instanceof EntitySchema && !this.#metadata.has(parent.init().meta.class)) {
258
- this.discoverReferences([parent], false);
259
- }
260
- if (typeof parent === 'function' && parent.name && !this.#metadata.has(parent)) {
261
- this.discoverReferences([parent], false);
262
- }
263
- /* v8 ignore next */
264
- if (!meta.class) {
265
- continue;
266
- }
267
- parent = Object.getPrototypeOf(meta.class);
268
- // Skip if parent is the auto-generated base class for the same entity (from setClass usage)
269
- if (parent.name !== '' &&
270
- parent.name !== meta.className &&
271
- !this.#metadata.has(parent) &&
272
- parent !== BaseEntity) {
273
- this.discoverReferences([parent], false);
274
- }
275
- }
276
- for (const schema of found) {
277
- this.discoverEntity(schema);
278
- }
279
- this.discoverMissingTargets();
280
- if (validate) {
281
- this.#validator.validateDiscovered(this.#discovered, this.#config.get('discovery'));
282
- }
283
- return this.#discovered.filter(meta => found.find(m => m.name === meta.className));
284
- }
285
- reset(entityName) {
286
- const exists = this.#discovered.findIndex(m => m.class === entityName || m.className === Utils.className(entityName));
287
- if (exists !== -1) {
288
- this.#metadata.reset(this.#discovered[exists].class);
289
- this.#discovered.splice(exists, 1);
290
- }
291
- }
292
- getSchema(entity) {
293
- if (EntitySchema.REGISTRY.has(entity)) {
294
- entity = EntitySchema.REGISTRY.get(entity);
295
- }
296
- if (entity instanceof EntitySchema) {
297
- const meta = Utils.copy(entity.meta, false);
298
- return EntitySchema.fromMetadata(meta);
299
- }
300
- const path = entity[MetadataStorage.PATH_SYMBOL];
301
- if (path) {
302
- const meta = Utils.copy(MetadataStorage.getMetadata(entity.name, path), false);
303
- meta.path = path;
304
- this.#metadata.set(entity, meta);
305
- }
306
- const exists = this.#metadata.has(entity);
307
- const meta = this.#metadata.get(entity, true);
308
- meta.abstract ??= !(exists && meta.name);
309
- const schema = EntitySchema.fromMetadata(meta);
310
- schema.setClass(entity);
311
- return schema;
312
- }
313
- getRootEntity(meta) {
314
- const base = meta.extends && this.#metadata.find(meta.extends);
315
- if (!base || base === meta) {
316
- // make sure we do not fall into infinite loop
317
- return meta;
318
- }
319
- const root = this.getRootEntity(base);
320
- // For STI or TPT, use the root entity.
321
- // Check both `inheritanceType` (set during discovery) and raw `inheritance` option (set before discovery).
322
- if (root.discriminatorColumn || root.inheritanceType || root.inheritance === 'tpt') {
323
- return root;
324
- }
325
- return meta;
326
- }
327
- discoverEntity(schema) {
328
- const meta = schema.meta;
329
- const path = meta.path;
330
- this.#logger.log('discovery', `- processing entity ${colors.cyan(meta.className)}${colors.grey(path ? ` (${path})` : '')}`);
331
- const root = this.getRootEntity(meta);
332
- schema.meta.path = meta.path;
333
- const cache = this.#metadataProvider.getCachedMetadata(meta, root);
334
- if (cache) {
335
- this.#logger.log('discovery', `- using cached metadata for entity ${colors.cyan(meta.className)}`);
336
- this.#discovered.push(meta);
337
- return;
338
- }
339
- // infer default value from property initializer early, as the metadata provider might use some defaults, e.g. string for reflect-metadata
340
- for (const prop of meta.props) {
341
- this.inferDefaultValue(meta, prop);
342
- }
343
- // if the definition is using EntitySchema we still want it to go through the metadata provider to validate no types are missing
344
- this.#metadataProvider.loadEntityMetadata(meta);
345
- if (!meta.tableName && meta.name) {
346
- const entityName = root.discriminatorColumn ? root.name : meta.name;
347
- meta.tableName = this.#namingStrategy.classToTableName(entityName);
348
- }
349
- this.#metadataProvider.saveToCache(meta);
350
- meta.root = root;
351
- this.#discovered.push(meta);
352
- }
353
- initNullability(prop) {
354
- if (prop.kind === ReferenceKind.ONE_TO_ONE) {
355
- return Utils.defaultValue(prop, 'nullable', prop.optional || !prop.owner);
356
- }
357
- return Utils.defaultValue(prop, 'nullable', prop.optional);
358
- }
359
- applyNamingStrategy(meta, prop) {
360
- if (!prop.fieldNames) {
361
- this.initFieldName(prop);
362
- }
363
- if (prop.kind === ReferenceKind.MANY_TO_MANY) {
364
- this.initManyToManyFields(meta, prop);
365
- }
366
- if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
367
- this.initManyToOneFields(prop);
368
- }
369
- if (prop.kind === ReferenceKind.ONE_TO_MANY) {
370
- this.initOneToManyFields(prop);
371
- }
372
- }
373
- initOwnColumns(meta) {
374
- meta.sync();
375
- for (const prop of meta.props) {
376
- if (!prop.joinColumns ||
377
- !prop.columnTypes ||
378
- prop.ownColumns ||
379
- ![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
380
- continue;
381
- }
382
- // For polymorphic relations, ownColumns should include all fieldNames
383
- // (discriminator + ID columns) since they are all owned by this relation
384
- if (prop.polymorphic) {
385
- prop.ownColumns = prop.fieldNames;
386
- continue;
387
- }
388
- if (prop.joinColumns.length > 1) {
389
- prop.ownColumns = prop.joinColumns.filter(col => {
390
- return !meta.props.find(p => p.name !== prop.name && (!p.fieldNames || p.fieldNames.includes(col)));
391
- });
392
- }
393
- if (!prop.ownColumns || prop.ownColumns.length === 0) {
394
- prop.ownColumns = prop.joinColumns;
395
- }
396
- if (prop.joinColumns.length !== prop.columnTypes.length) {
397
- prop.columnTypes = prop.joinColumns.flatMap(field => {
398
- const matched = meta.props.find(p => p.fieldNames?.includes(field));
399
- /* v8 ignore next */
400
- if (!matched) {
401
- throw MetadataError.fromWrongForeignKey(meta, prop, 'columnTypes');
402
- }
403
- return matched.columnTypes;
404
- });
405
- }
406
- if (prop.joinColumns.length !== prop.referencedColumnNames.length) {
407
- throw MetadataError.fromWrongForeignKey(meta, prop, 'referencedColumnNames');
408
- }
409
- }
410
- }
411
- initFieldName(prop, object = false) {
412
- if (prop.fieldNames && prop.fieldNames.length > 0) {
413
- return;
414
- }
16
+ #namingStrategy;
17
+ #metadataProvider;
18
+ #logger;
19
+ #schemaHelper;
20
+ #validator = new MetadataValidator();
21
+ #discovered = [];
22
+ #metadata;
23
+ #platform;
24
+ #config;
25
+ constructor(metadata, platform, config) {
26
+ this.#metadata = metadata;
27
+ this.#platform = platform;
28
+ this.#config = config;
29
+ this.#namingStrategy = this.#config.getNamingStrategy();
30
+ this.#metadataProvider = this.#config.getMetadataProvider();
31
+ this.#logger = this.#config.getLogger();
32
+ this.#schemaHelper = this.#platform.getSchemaHelper();
33
+ }
34
+ /** Discovers all entities asynchronously and returns the populated MetadataStorage. */
35
+ async discover(preferTs = true) {
36
+ this.#discovered.length = 0;
37
+ const startTime = Date.now();
38
+ const suffix =
39
+ this.#metadataProvider.constructor === MetadataProvider
40
+ ? ''
41
+ : `, using ${colors.cyan(this.#metadataProvider.constructor.name)}`;
42
+ this.#logger.log('discovery', `ORM entity discovery started${suffix}`);
43
+ await this.findEntities(preferTs);
44
+ for (const meta of this.#discovered) {
45
+ /* v8 ignore next */
46
+ await this.#config.get('discovery').onMetadata?.(meta, this.#platform);
47
+ }
48
+ this.processDiscoveredEntities(this.#discovered);
49
+ const diff = Date.now() - startTime;
50
+ this.#logger.log(
51
+ 'discovery',
52
+ `- entity discovery finished, found ${colors.green('' + this.#discovered.length)} entities, took ${colors.green(`${diff} ms`)}`,
53
+ );
54
+ const storage = this.mapDiscoveredEntities();
55
+ /* v8 ignore next */
56
+ await this.#config.get('discovery').afterDiscovered?.(storage, this.#platform);
57
+ return storage;
58
+ }
59
+ /** Discovers all entities synchronously and returns the populated MetadataStorage. */
60
+ discoverSync() {
61
+ this.#discovered.length = 0;
62
+ const startTime = Date.now();
63
+ const suffix =
64
+ this.#metadataProvider.constructor === MetadataProvider
65
+ ? ''
66
+ : `, using ${colors.cyan(this.#metadataProvider.constructor.name)}`;
67
+ this.#logger.log('discovery', `ORM entity discovery started${suffix} in sync mode`);
68
+ const refs = this.#config.get('entities');
69
+ this.discoverReferences(refs);
70
+ for (const meta of this.#discovered) {
71
+ /* v8 ignore next */
72
+ void this.#config.get('discovery').onMetadata?.(meta, this.#platform);
73
+ }
74
+ this.processDiscoveredEntities(this.#discovered);
75
+ const diff = Date.now() - startTime;
76
+ this.#logger.log(
77
+ 'discovery',
78
+ `- entity discovery finished, found ${colors.green('' + this.#discovered.length)} entities, took ${colors.green(`${diff} ms`)}`,
79
+ );
80
+ const storage = this.mapDiscoveredEntities();
81
+ /* v8 ignore next */
82
+ void this.#config.get('discovery').afterDiscovered?.(storage, this.#platform);
83
+ return storage;
84
+ }
85
+ mapDiscoveredEntities() {
86
+ const discovered = new MetadataStorage();
87
+ this.#discovered
88
+ .filter(meta => meta.root.name)
89
+ .sort((a, b) => b.root.name.localeCompare(a.root.name))
90
+ .forEach(meta => {
91
+ this.#platform.validateMetadata(meta);
92
+ discovered.set(meta.class, meta);
93
+ });
94
+ for (const meta of discovered) {
95
+ meta.root = discovered.get(meta.root.class);
96
+ if (meta.inheritanceType === 'tpt') {
97
+ this.computeTPTOwnProps(meta);
98
+ }
99
+ }
100
+ return discovered;
101
+ }
102
+ initAccessors(meta) {
103
+ for (const prop of Object.values(meta.properties)) {
104
+ if (!prop.accessor || meta.properties[prop.accessor]) {
105
+ continue;
106
+ }
107
+ const desc = Object.getOwnPropertyDescriptor(meta.prototype, prop.name);
108
+ if (desc?.get || desc?.set) {
109
+ this.initRelation(prop);
110
+ this.initFieldName(prop);
111
+ const accessor = prop.name;
112
+ prop.name = typeof prop.accessor === 'string' ? prop.accessor : prop.name;
113
+ if (prop.accessor === true) {
114
+ prop.getter = prop.setter = true;
115
+ } else {
116
+ prop.getter = prop.setter = false;
117
+ }
118
+ prop.accessor = accessor;
119
+ prop.serializedName ??= accessor;
120
+ Utils.renameKey(meta.properties, accessor, prop.name);
121
+ } else {
122
+ const name = prop.name;
415
123
  if (prop.kind === ReferenceKind.SCALAR || prop.kind === ReferenceKind.EMBEDDED) {
416
- prop.fieldNames = [this.#namingStrategy.propertyToColumnName(prop.name, object)];
417
- }
418
- else if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && !prop.polymorphic) {
419
- prop.fieldNames = this.initManyToOneFieldName(prop, prop.name);
420
- }
421
- else if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.owner) {
422
- prop.fieldNames = this.initManyToManyFieldName(prop, prop.name);
423
- }
424
- }
425
- initManyToOneFieldName(prop, name, tableName) {
426
- const meta2 = prop.targetMeta;
427
- const ret = [];
428
- for (const primaryKey of meta2.primaryKeys) {
429
- this.initFieldName(meta2.properties[primaryKey]);
430
- for (const fieldName of meta2.properties[primaryKey].fieldNames) {
431
- ret.push(this.#namingStrategy.joinKeyColumnName(name, fieldName, meta2.compositePK, tableName));
432
- }
433
- }
434
- return ret;
435
- }
436
- initManyToManyFieldName(prop, name) {
437
- const meta2 = prop.targetMeta;
438
- return meta2.primaryKeys.map(() => this.#namingStrategy.propertyToColumnName(name));
439
- }
440
- initManyToManyFields(meta, prop) {
441
- const meta2 = prop.targetMeta;
442
- Utils.defaultValue(prop, 'fixedOrder', !!prop.fixedOrderColumn);
443
- const pivotMeta = this.#metadata.find(prop.pivotEntity);
444
- const props = Object.values(pivotMeta?.properties ?? {});
445
- const pks = props.filter(p => p.primary);
446
- const fks = props.filter(p => p.kind === ReferenceKind.MANY_TO_ONE);
447
- if (pivotMeta) {
448
- pivotMeta.pivotTable = true;
449
- prop.pivotTable = pivotMeta.tableName;
450
- if (pks.length === 1) {
451
- prop.fixedOrder = true;
452
- prop.fixedOrderColumn = pks[0].name;
453
- }
454
- }
455
- if (pivotMeta && (pks.length === 2 || fks.length >= 2)) {
456
- const owner = prop.mappedBy ? meta2.properties[prop.mappedBy] : prop;
457
- const [first, second] = this.ensureCorrectFKOrderInPivotEntity(pivotMeta, owner);
458
- prop.joinColumns ??= first.fieldNames;
459
- prop.inverseJoinColumns ??= second.fieldNames;
460
- }
461
- if (!prop.pivotTable && prop.owner && this.#platform.usesPivotTable()) {
462
- prop.pivotTable = this.#namingStrategy.joinTableName(meta.className, meta2.tableName, prop.name, meta.tableName);
463
- }
464
- if (prop.mappedBy) {
465
- const prop2 = meta2.properties[prop.mappedBy];
466
- this.initManyToManyFields(meta2, prop2);
467
- prop.pivotTable = prop2.pivotTable;
468
- prop.pivotEntity = prop2.pivotEntity;
469
- prop.fixedOrder = prop2.fixedOrder;
470
- prop.fixedOrderColumn = prop2.fixedOrderColumn;
471
- prop.joinColumns = prop2.inverseJoinColumns;
472
- prop.inverseJoinColumns = prop2.joinColumns;
473
- prop.polymorphic = prop2.polymorphic;
474
- prop.discriminator = prop2.discriminator;
475
- prop.discriminatorColumn = prop2.discriminatorColumn;
476
- prop.discriminatorValue = prop2.discriminatorValue;
477
- }
478
- prop.referencedColumnNames ??= Utils.flatten(meta.primaryKeys.map(primaryKey => meta.properties[primaryKey].fieldNames));
479
- // For polymorphic M:N, use discriminator base name for FK column (e.g., taggable_id instead of post_id)
480
- if (prop.polymorphic && prop.discriminator) {
481
- prop.joinColumns ??= prop.referencedColumnNames.map(referencedColumnName => this.#namingStrategy.joinKeyColumnName(prop.discriminator, referencedColumnName, prop.referencedColumnNames.length > 1));
482
- }
483
- else {
484
- const ownerTableName = this.isExplicitTableName(meta.root) ? meta.root.tableName : undefined;
485
- prop.joinColumns ??= prop.referencedColumnNames.map(referencedColumnName => this.#namingStrategy.joinKeyColumnName(meta.root.className, referencedColumnName, meta.compositePK, ownerTableName));
486
- }
487
- const inverseTableName = this.isExplicitTableName(meta2.root) ? meta2.root.tableName : undefined;
488
- prop.inverseJoinColumns ??= this.initManyToOneFieldName(prop, meta2.root.className, inverseTableName);
489
- }
490
- isExplicitTableName(meta) {
491
- return meta.tableName !== this.#namingStrategy.classToTableName(meta.className);
492
- }
493
- initManyToOneFields(prop) {
494
- if (prop.polymorphic && prop.polymorphTargets) {
495
- const fieldNames1 = prop.targetMeta.getPrimaryProps().flatMap(pk => pk.fieldNames);
496
- const idColumns = fieldNames1.map(fieldName => this.#namingStrategy.joinKeyColumnName(prop.discriminator, fieldName, fieldNames1.length > 1));
497
- prop.fieldNames ??= [prop.discriminatorColumn, ...idColumns];
498
- prop.joinColumns ??= idColumns;
499
- prop.referencedColumnNames ??= fieldNames1;
500
- prop.referencedTableName ??= prop.targetMeta.tableName;
501
- return;
502
- }
503
- const meta2 = prop.targetMeta;
504
- let fieldNames;
505
- // If targetKey is specified, use that property's field names instead of PKs
506
- if (prop.targetKey) {
507
- const targetProp = meta2.properties[prop.targetKey];
508
- fieldNames = targetProp.fieldNames;
509
- }
510
- else {
511
- fieldNames = Utils.flatten(meta2.primaryKeys.map(primaryKey => meta2.properties[primaryKey].fieldNames));
512
- }
513
- Utils.defaultValue(prop, 'referencedTableName', meta2.tableName);
514
- if (!prop.joinColumns) {
515
- prop.joinColumns = fieldNames.map(fieldName => this.#namingStrategy.joinKeyColumnName(prop.name, fieldName, fieldNames.length > 1));
516
- }
517
- if (!prop.referencedColumnNames) {
518
- prop.referencedColumnNames = fieldNames;
519
- }
520
- // Relations to composite PK targets need cascade update by default,
521
- // since composite PKs are more likely to have mutable components
522
- if (meta2.compositePK) {
523
- prop.updateRule ??= 'cascade';
524
- }
525
- // Nullable relations default to 'set null' on delete - when the referenced
526
- // entity is deleted, set the FK to null rather than failing
527
- if (prop.nullable) {
528
- prop.deleteRule ??= 'set null';
529
- }
530
- }
531
- initOneToManyFields(prop) {
532
- const meta2 = prop.targetMeta;
533
- if (!prop.joinColumns) {
534
- prop.joinColumns = [this.#namingStrategy.joinColumnName(prop.name)];
535
- }
536
- if (!prop.referencedColumnNames) {
537
- meta2.getPrimaryProps().forEach(pk => this.applyNamingStrategy(meta2, pk));
538
- prop.referencedColumnNames = Utils.flatten(meta2.getPrimaryProps().map(pk => pk.fieldNames));
539
- }
540
- }
541
- processEntity(meta) {
542
- const pks = Object.values(meta.properties).filter(prop => prop.primary);
543
- meta.primaryKeys = pks.map(prop => prop.name);
544
- meta.compositePK = pks.length > 1;
545
- // FK used as PK, we need to cascade - applies to both single FK-as-PK
546
- // and composite PKs where all PKs are FKs (e.g., pivot-like entities)
547
- const fkPks = pks.filter(pk => pk.kind !== ReferenceKind.SCALAR);
548
- if (fkPks.length > 0 && fkPks.length === pks.length) {
549
- for (const pk of fkPks) {
550
- pk.deleteRule ??= 'cascade';
551
- pk.updateRule ??= 'cascade';
552
- }
553
- }
554
- meta.forceConstructor ??= this.shouldForceConstructorUsage(meta);
555
- this.#validator.validateEntityDefinition(this.#metadata, meta.class, this.#config.get('discovery'));
556
- for (const prop of Object.values(meta.properties)) {
557
- this.initNullability(prop);
558
- this.applyNamingStrategy(meta, prop);
559
- this.initDefaultValue(prop);
560
- this.inferTypeFromDefault(prop);
561
- this.initVersionProperty(meta, prop);
562
- this.initCustomType(meta, prop);
563
- this.initColumnType(prop);
564
- this.initRelation(prop);
565
- }
566
- this.initOwnColumns(meta);
567
- meta.simplePK =
568
- pks.length === 1 && pks[0].kind === ReferenceKind.SCALAR && !pks[0].customType && pks[0].runtimeType !== 'Date';
569
- meta.serializedPrimaryKey ??= meta.props.find(prop => prop.serializedPrimaryKey)?.name;
570
- if (meta.serializedPrimaryKey && meta.serializedPrimaryKey !== meta.primaryKeys[0]) {
571
- meta.properties[meta.serializedPrimaryKey].persist ??= false;
572
- }
573
- if (this.#platform.usesPivotTable()) {
574
- return Object.values(meta.properties)
575
- .filter(prop => prop.kind === ReferenceKind.MANY_TO_MANY && prop.owner && prop.pivotTable)
576
- .map(prop => {
577
- const pivotMeta = this.definePivotTableEntity(meta, prop);
578
- prop.pivotEntity = pivotMeta.class;
579
- if (prop.inversedBy) {
580
- prop.targetMeta.properties[prop.inversedBy].pivotEntity = pivotMeta.class;
581
- }
582
- return pivotMeta;
583
- });
584
- }
585
- return [];
586
- }
587
- findReferencingProperties(meta, metadata) {
588
- for (const meta2 of metadata) {
589
- for (const prop2 of meta2.relations) {
590
- if (prop2.kind !== ReferenceKind.SCALAR && prop2.type === meta.className) {
591
- meta.referencingProperties.push({ meta: meta2, prop: prop2 });
592
- }
593
- }
594
- }
595
- }
596
- initFactoryField(meta, prop) {
597
- ['mappedBy', 'inversedBy', 'pivotEntity'].forEach(type => {
598
- const value = prop[type];
599
- if (value instanceof Function) {
600
- const meta2 = prop.targetMeta ?? this.#metadata.get(prop.target);
601
- prop[type] = value(meta2.properties)?.name;
602
- if (type === 'pivotEntity' && value) {
603
- prop[type] = value(meta2.properties);
604
- }
605
- if (prop[type] == null) {
606
- throw MetadataError.fromWrongReference(meta, prop, type);
607
- }
608
- }
124
+ prop.name = prop.accessor;
125
+ }
126
+ this.initRelation(prop);
127
+ this.initFieldName(prop);
128
+ prop.serializedName ??= prop.accessor;
129
+ prop.name = name;
130
+ }
131
+ }
132
+ }
133
+ /** Processes discovered entities: initializes relations, embeddables, indexes, and inheritance. */
134
+ processDiscoveredEntities(discovered) {
135
+ for (const meta of discovered) {
136
+ let i = 1;
137
+ Object.values(meta.properties).forEach(prop => meta.propertyOrder.set(prop.name, i++));
138
+ Object.values(meta.properties).forEach(prop => this.initPolyEmbeddables(prop, discovered));
139
+ this.initAccessors(meta);
140
+ }
141
+ // ignore base entities (not annotated with @Entity)
142
+ const filtered = discovered.filter(meta => meta.root.name);
143
+ // sort so we discover entities first to get around issues with nested embeddables
144
+ filtered.sort((a, b) => (!a.embeddable === !b.embeddable ? 0 : a.embeddable ? 1 : -1));
145
+ filtered.forEach(meta => this.initSingleTableInheritance(meta, filtered));
146
+ filtered.forEach(meta => this.initTPTRelationships(meta, filtered));
147
+ filtered.forEach(meta => this.defineBaseEntityProperties(meta));
148
+ filtered.forEach(meta => {
149
+ const newMeta = EntitySchema.fromMetadata(meta).init().meta;
150
+ return this.#metadata.set(newMeta.class, newMeta);
151
+ });
152
+ filtered.forEach(meta => this.initAutoincrement(meta));
153
+ const forEachProp = cb => {
154
+ filtered.forEach(meta => Object.values(meta.properties).forEach(prop => cb(meta, prop)));
155
+ };
156
+ forEachProp((m, p) => this.initFactoryField(m, p));
157
+ forEachProp((m, p) => this.initPolymorphicRelation(m, p, filtered));
158
+ forEachProp((_m, p) => this.initRelation(p));
159
+ forEachProp((m, p) => this.initEmbeddables(m, p));
160
+ forEachProp((_m, p) => this.initFieldName(p));
161
+ filtered.forEach(meta => this.finalizeTPTInheritance(meta, filtered));
162
+ filtered.forEach(meta => this.computeTPTOwnProps(meta));
163
+ forEachProp((m, p) => this.initVersionProperty(m, p));
164
+ forEachProp((m, p) => this.initCustomType(m, p));
165
+ forEachProp((m, p) => this.initGeneratedColumn(m, p));
166
+ filtered.forEach(meta => this.initAutoincrement(meta)); // once again after we init custom types
167
+ filtered.forEach(meta => this.initCheckConstraints(meta));
168
+ forEachProp((_m, p) => {
169
+ this.initDefaultValue(p);
170
+ this.inferTypeFromDefault(p);
171
+ this.initRelation(p);
172
+ this.initColumnType(p);
173
+ });
174
+ forEachProp((m, p) => this.initIndexes(m, p));
175
+ filtered.forEach(meta => this.autoWireBidirectionalProperties(meta));
176
+ for (const meta of filtered) {
177
+ discovered.push(...this.processEntity(meta));
178
+ }
179
+ discovered.forEach(meta => meta.sync(true));
180
+ this.#metadataProvider.combineCache();
181
+ return discovered.map(meta => {
182
+ meta = this.#metadata.get(meta.class);
183
+ meta.sync(true);
184
+ this.findReferencingProperties(meta, filtered);
185
+ if (meta.inheritanceType === 'tpt') {
186
+ this.computeTPTOwnProps(meta);
187
+ }
188
+ return meta;
189
+ });
190
+ }
191
+ async findEntities(preferTs) {
192
+ const { entities, entitiesTs, baseDir } = this.#config.getAll();
193
+ const targets = preferTs && entitiesTs.length > 0 ? entitiesTs : entities;
194
+ const processed = [];
195
+ const paths = [];
196
+ for (const entity of targets) {
197
+ if (typeof entity === 'string') {
198
+ paths.push(entity);
199
+ } else {
200
+ processed.push(entity);
201
+ }
202
+ }
203
+ if (paths.length > 0) {
204
+ const { discoverEntities } = await import('@mikro-orm/core/file-discovery');
205
+ processed.push(...(await discoverEntities(paths, { baseDir })));
206
+ }
207
+ return this.discoverReferences(processed);
208
+ }
209
+ discoverMissingTargets() {
210
+ const unwrap = type =>
211
+ type
212
+ .replace(/Array<(.*)>/, '$1') // unwrap array
213
+ .replace(/\[]$/, '') // remove array suffix
214
+ .replace(/\((.*)\)/, '$1'); // unwrap union types
215
+ const missing = [];
216
+ this.#discovered.forEach(meta =>
217
+ Object.values(meta.properties).forEach(prop => {
218
+ if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.pivotEntity) {
219
+ const pivotEntity = prop.pivotEntity;
220
+ const target = typeof pivotEntity === 'function' && !pivotEntity.prototype ? pivotEntity() : pivotEntity;
221
+ if (!this.#discovered.find(m => m.className === Utils.className(target))) {
222
+ missing.push(target);
223
+ }
224
+ }
225
+ if (prop.kind !== ReferenceKind.SCALAR) {
226
+ const target = typeof prop.entity === 'function' && !prop.entity.prototype ? prop.entity() : prop.type;
227
+ if (
228
+ !unwrap(prop.type)
229
+ .split(/ ?\| ?/)
230
+ .every(type => this.#discovered.find(m => m.className === type))
231
+ ) {
232
+ missing.push(...Utils.asArray(target));
233
+ }
234
+ }
235
+ }),
236
+ );
237
+ if (missing.length > 0) {
238
+ this.tryDiscoverTargets(missing);
239
+ }
240
+ }
241
+ tryDiscoverTargets(targets) {
242
+ for (const target of targets) {
243
+ const schema = EntitySchema.is(target) ? target : undefined;
244
+ const isDiscoverable = typeof target === 'function' || schema;
245
+ if (isDiscoverable && target.name) {
246
+ // Get the actual class for EntitySchema, or use target directly for classes
247
+ const targetClass = schema ? schema.meta.class : target;
248
+ if (!this.#metadata.has(targetClass)) {
249
+ this.discoverReferences([target], false);
250
+ this.discoverMissingTargets();
251
+ }
252
+ }
253
+ }
254
+ }
255
+ discoverReferences(refs, validate = true) {
256
+ const found = [];
257
+ for (const entity of refs) {
258
+ if (typeof entity === 'string') {
259
+ throw new Error('Folder based discovery requires the async `MikroORM.init()` method.');
260
+ }
261
+ const schema = this.getSchema(entity);
262
+ const meta = schema.init().meta;
263
+ this.#metadata.set(meta.class, meta);
264
+ found.push(schema);
265
+ }
266
+ // discover parents (base entities) automatically
267
+ for (const meta of this.#metadata) {
268
+ let parent = meta.extends;
269
+ if (EntitySchema.is(parent) && !this.#metadata.has(parent.init().meta.class)) {
270
+ this.discoverReferences([parent], false);
271
+ }
272
+ if (typeof parent === 'function' && parent.name && !this.#metadata.has(parent)) {
273
+ this.discoverReferences([parent], false);
274
+ }
275
+ /* v8 ignore next */
276
+ if (!meta.class) {
277
+ continue;
278
+ }
279
+ parent = Object.getPrototypeOf(meta.class);
280
+ // Skip if parent is the auto-generated base class for the same entity (from setClass usage)
281
+ if (
282
+ parent.name !== '' &&
283
+ parent.name !== meta.className &&
284
+ !this.#metadata.has(parent) &&
285
+ parent !== BaseEntity
286
+ ) {
287
+ this.discoverReferences([parent], false);
288
+ }
289
+ }
290
+ for (const schema of found) {
291
+ this.discoverEntity(schema);
292
+ }
293
+ this.discoverMissingTargets();
294
+ if (validate) {
295
+ this.#validator.validateDiscovered(this.#discovered, this.#config.get('discovery'));
296
+ }
297
+ return this.#discovered.filter(meta => found.find(m => m.name === meta.className));
298
+ }
299
+ reset(entityName) {
300
+ const exists = this.#discovered.findIndex(
301
+ m => m.class === entityName || m.className === Utils.className(entityName),
302
+ );
303
+ if (exists !== -1) {
304
+ this.#metadata.reset(this.#discovered[exists].class);
305
+ this.#discovered.splice(exists, 1);
306
+ }
307
+ }
308
+ getSchema(entity) {
309
+ if (EntitySchema.REGISTRY.has(entity)) {
310
+ entity = EntitySchema.REGISTRY.get(entity);
311
+ }
312
+ if (EntitySchema.is(entity)) {
313
+ const meta = Utils.copy(entity.meta, false);
314
+ return EntitySchema.fromMetadata(meta);
315
+ }
316
+ // After the EntitySchema check, entity must be an EntityClass
317
+ const cls = entity;
318
+ const path = cls[MetadataStorage.PATH_SYMBOL];
319
+ if (path) {
320
+ const meta = Utils.copy(MetadataStorage.getMetadata(cls.name, path), false);
321
+ meta.path = path;
322
+ this.#metadata.set(cls, meta);
323
+ }
324
+ const exists = this.#metadata.has(cls);
325
+ const meta = this.#metadata.get(cls, true);
326
+ meta.abstract ??= !(exists && meta.name);
327
+ const schema = EntitySchema.fromMetadata(meta);
328
+ schema.setClass(cls);
329
+ return schema;
330
+ }
331
+ getRootEntity(meta) {
332
+ const base = meta.extends && this.#metadata.find(meta.extends);
333
+ if (!base || base === meta) {
334
+ // make sure we do not fall into infinite loop
335
+ return meta;
336
+ }
337
+ const root = this.getRootEntity(base);
338
+ // For STI or TPT, use the root entity.
339
+ // Check both `inheritanceType` (set during discovery) and raw `inheritance` option (set before discovery).
340
+ if (root.discriminatorColumn || root.inheritanceType || root.inheritance === 'tpt') {
341
+ return root;
342
+ }
343
+ return meta;
344
+ }
345
+ discoverEntity(schema) {
346
+ const meta = schema.meta;
347
+ const path = meta.path;
348
+ this.#logger.log(
349
+ 'discovery',
350
+ `- processing entity ${colors.cyan(meta.className)}${colors.grey(path ? ` (${path})` : '')}`,
351
+ );
352
+ const root = this.getRootEntity(meta);
353
+ schema.meta.path = meta.path;
354
+ const cache = this.#metadataProvider.getCachedMetadata(meta, root);
355
+ if (cache) {
356
+ this.#logger.log('discovery', `- using cached metadata for entity ${colors.cyan(meta.className)}`);
357
+ this.#discovered.push(meta);
358
+ return;
359
+ }
360
+ // infer default value from property initializer early, as the metadata provider might use some defaults, e.g. string for reflect-metadata
361
+ for (const prop of meta.props) {
362
+ this.inferDefaultValue(meta, prop);
363
+ }
364
+ // if the definition is using EntitySchema we still want it to go through the metadata provider to validate no types are missing
365
+ this.#metadataProvider.loadEntityMetadata(meta);
366
+ if (!meta.tableName && meta.name) {
367
+ const entityName = root.discriminatorColumn ? root.name : meta.name;
368
+ meta.tableName = this.#namingStrategy.classToTableName(entityName);
369
+ }
370
+ this.#metadataProvider.saveToCache(meta);
371
+ meta.root = root;
372
+ this.#discovered.push(meta);
373
+ }
374
+ initNullability(prop) {
375
+ if (prop.kind === ReferenceKind.ONE_TO_ONE) {
376
+ return Utils.defaultValue(prop, 'nullable', prop.optional || !prop.owner);
377
+ }
378
+ return Utils.defaultValue(prop, 'nullable', prop.optional);
379
+ }
380
+ applyNamingStrategy(meta, prop) {
381
+ if (!prop.fieldNames) {
382
+ this.initFieldName(prop);
383
+ }
384
+ if (prop.kind === ReferenceKind.MANY_TO_MANY) {
385
+ this.initManyToManyFields(meta, prop);
386
+ }
387
+ if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
388
+ this.initManyToOneFields(prop);
389
+ }
390
+ if (prop.kind === ReferenceKind.ONE_TO_MANY) {
391
+ this.initOneToManyFields(prop);
392
+ }
393
+ }
394
+ initOwnColumns(meta) {
395
+ meta.sync();
396
+ for (const prop of meta.props) {
397
+ if (
398
+ !prop.joinColumns ||
399
+ !prop.columnTypes ||
400
+ prop.ownColumns ||
401
+ ![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)
402
+ ) {
403
+ continue;
404
+ }
405
+ // For polymorphic relations, ownColumns should include all fieldNames
406
+ // (discriminator + ID columns) since they are all owned by this relation
407
+ if (prop.polymorphic) {
408
+ prop.ownColumns = prop.fieldNames;
409
+ continue;
410
+ }
411
+ if (prop.joinColumns.length > 1) {
412
+ prop.ownColumns = prop.joinColumns.filter(col => {
413
+ return !meta.props.find(p => p.name !== prop.name && (!p.fieldNames || p.fieldNames.includes(col)));
609
414
  });
610
- }
611
- ensureCorrectFKOrderInPivotEntity(meta, owner) {
612
- const pks = Object.values(meta.properties).filter(p => p.primary);
613
- const fks = Object.values(meta.properties).filter(p => p.kind === ReferenceKind.MANY_TO_ONE);
614
- let first, second;
615
- if (pks.length === 2) {
616
- [first, second] = pks;
617
- }
618
- else if (fks.length >= 2) {
619
- [first, second] = fks;
620
- }
621
- else {
622
- /* v8 ignore next */
623
- return [];
624
- }
625
- // wrong FK order, first FK needs to point to the owning side
626
- // (note that we can detect this only if the FKs target different types)
627
- if (owner.type === first.type && first.type !== second.type) {
628
- delete meta.properties[first.name];
629
- meta.removeProperty(first.name, false);
630
- meta.addProperty(first);
631
- [first, second] = [second, first];
632
- }
633
- return [first, second];
634
- }
635
- definePivotTableEntity(meta, prop) {
636
- const pivotMeta = prop.pivotEntity
637
- ? this.#metadata.find(prop.pivotEntity)
638
- : this.#metadata.getByClassName(prop.pivotTable, false);
639
- // ensure inverse side exists so we can join it when populating via pivot tables
640
- if (!prop.inversedBy && prop.targetMeta) {
641
- const inverseName = `${meta.className}_${prop.name}__inverse`;
642
- prop.inversedBy = inverseName;
643
- const inverseProp = {
644
- name: inverseName,
645
- kind: ReferenceKind.MANY_TO_MANY,
646
- type: meta.className,
647
- target: meta.class,
648
- targetMeta: meta,
649
- mappedBy: prop.name,
650
- pivotEntity: prop.pivotEntity,
651
- pivotTable: prop.pivotTable,
652
- persist: false,
653
- hydrate: false,
654
- };
655
- this.applyNamingStrategy(prop.targetMeta, inverseProp);
656
- this.initCustomType(prop.targetMeta, inverseProp);
657
- prop.targetMeta.properties[inverseName] = inverseProp;
658
- }
659
- if (pivotMeta) {
660
- prop.pivotEntity = pivotMeta.class;
661
- this.ensureCorrectFKOrderInPivotEntity(pivotMeta, prop);
662
- if (prop.polymorphic && prop.discriminatorValue) {
663
- pivotMeta.polymorphicDiscriminatorMap ??= {};
664
- pivotMeta.polymorphicDiscriminatorMap[prop.discriminatorValue] = meta.class;
665
- // For composite PK entities sharing a polymorphic pivot table,
666
- // we need to add columns for each entity type's PKs
667
- this.addPolymorphicPivotColumns(pivotMeta, meta, prop);
668
- // Add virtual M:1 relation for this polymorphic owner (for join loading)
669
- const ownerRelationName = `${prop.discriminator}_${meta.tableName}`;
670
- if (!pivotMeta.properties[ownerRelationName]) {
671
- pivotMeta.properties[ownerRelationName] = this.definePolymorphicOwnerRelation(prop, ownerRelationName, meta);
672
- }
673
- }
674
- return pivotMeta;
675
- }
676
- let tableName = prop.pivotTable;
677
- let schemaName;
678
- if (prop.pivotTable.includes('.')) {
679
- [schemaName, tableName] = prop.pivotTable.split('.');
680
- }
681
- schemaName ??= meta.schema;
682
- const targetMeta = prop.targetMeta;
683
- const targetType = targetMeta.className;
684
- const pivotMeta2 = new EntityMetadata({
685
- name: prop.pivotTable,
686
- className: prop.pivotTable,
687
- collection: tableName,
688
- schema: schemaName,
689
- pivotTable: true,
415
+ }
416
+ if (!prop.ownColumns || prop.ownColumns.length === 0) {
417
+ prop.ownColumns = prop.joinColumns;
418
+ }
419
+ if (prop.joinColumns.length !== prop.columnTypes.length) {
420
+ prop.columnTypes = prop.joinColumns.flatMap(field => {
421
+ const matched = meta.props.find(p => p.fieldNames?.includes(field));
422
+ /* v8 ignore next */
423
+ if (!matched) {
424
+ throw MetadataError.fromWrongForeignKey(meta, prop, 'columnTypes');
425
+ }
426
+ return matched.columnTypes;
427
+ });
428
+ }
429
+ if (prop.joinColumns.length !== prop.referencedColumnNames.length) {
430
+ throw MetadataError.fromWrongForeignKey(meta, prop, 'referencedColumnNames');
431
+ }
432
+ }
433
+ }
434
+ initFieldName(prop, object = false) {
435
+ if (prop.fieldNames && prop.fieldNames.length > 0) {
436
+ return;
437
+ }
438
+ if (prop.kind === ReferenceKind.SCALAR || prop.kind === ReferenceKind.EMBEDDED) {
439
+ prop.fieldNames = [this.#namingStrategy.propertyToColumnName(prop.name, object)];
440
+ } else if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && !prop.polymorphic) {
441
+ prop.fieldNames = this.initManyToOneFieldName(prop, prop.name);
442
+ } else if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.owner) {
443
+ prop.fieldNames = this.initManyToManyFieldName(prop, prop.name);
444
+ }
445
+ }
446
+ initManyToOneFieldName(prop, name, tableName) {
447
+ const meta2 = prop.targetMeta;
448
+ const ret = [];
449
+ for (const primaryKey of meta2.primaryKeys) {
450
+ this.initFieldName(meta2.properties[primaryKey]);
451
+ for (const fieldName of meta2.properties[primaryKey].fieldNames) {
452
+ ret.push(this.#namingStrategy.joinKeyColumnName(name, fieldName, meta2.compositePK, tableName));
453
+ }
454
+ }
455
+ return ret;
456
+ }
457
+ initManyToManyFieldName(prop, name) {
458
+ const meta2 = prop.targetMeta;
459
+ return meta2.primaryKeys.map(() => this.#namingStrategy.propertyToColumnName(name));
460
+ }
461
+ initManyToManyFields(meta, prop) {
462
+ const meta2 = prop.targetMeta;
463
+ Utils.defaultValue(prop, 'fixedOrder', !!prop.fixedOrderColumn);
464
+ const pivotMeta = this.#metadata.find(prop.pivotEntity);
465
+ const props = Object.values(pivotMeta?.properties ?? {});
466
+ const pks = props.filter(p => p.primary);
467
+ const fks = props.filter(p => p.kind === ReferenceKind.MANY_TO_ONE);
468
+ if (pivotMeta) {
469
+ pivotMeta.pivotTable = true;
470
+ prop.pivotTable = pivotMeta.tableName;
471
+ if (pks.length === 1) {
472
+ prop.fixedOrder = true;
473
+ prop.fixedOrderColumn = pks[0].name;
474
+ }
475
+ }
476
+ if (pivotMeta && (pks.length === 2 || fks.length >= 2)) {
477
+ const owner = prop.mappedBy ? meta2.properties[prop.mappedBy] : prop;
478
+ const [first, second] = this.ensureCorrectFKOrderInPivotEntity(pivotMeta, owner);
479
+ prop.joinColumns ??= first.fieldNames;
480
+ prop.inverseJoinColumns ??= second.fieldNames;
481
+ }
482
+ if (!prop.pivotTable && prop.owner && this.#platform.usesPivotTable()) {
483
+ prop.pivotTable = this.#namingStrategy.joinTableName(meta.className, meta2.tableName, prop.name, meta.tableName);
484
+ }
485
+ if (prop.mappedBy) {
486
+ const prop2 = meta2.properties[prop.mappedBy];
487
+ this.initManyToManyFields(meta2, prop2);
488
+ prop.pivotTable = prop2.pivotTable;
489
+ prop.pivotEntity = prop2.pivotEntity;
490
+ prop.fixedOrder = prop2.fixedOrder;
491
+ prop.fixedOrderColumn = prop2.fixedOrderColumn;
492
+ prop.joinColumns = prop2.inverseJoinColumns;
493
+ prop.inverseJoinColumns = prop2.joinColumns;
494
+ prop.polymorphic = prop2.polymorphic;
495
+ prop.discriminator = prop2.discriminator;
496
+ prop.discriminatorColumn = prop2.discriminatorColumn;
497
+ prop.discriminatorValue = prop2.discriminatorValue;
498
+ }
499
+ prop.referencedColumnNames ??= Utils.flatten(
500
+ meta.primaryKeys.map(primaryKey => meta.properties[primaryKey].fieldNames),
501
+ );
502
+ // For polymorphic M:N, use discriminator base name for FK column (e.g., taggable_id instead of post_id)
503
+ if (prop.polymorphic && prop.discriminator) {
504
+ prop.joinColumns ??= prop.referencedColumnNames.map(referencedColumnName =>
505
+ this.#namingStrategy.joinKeyColumnName(
506
+ prop.discriminator,
507
+ referencedColumnName,
508
+ prop.referencedColumnNames.length > 1,
509
+ ),
510
+ );
511
+ } else {
512
+ const ownerTableName = this.isExplicitTableName(meta.root) ? meta.root.tableName : undefined;
513
+ prop.joinColumns ??= prop.referencedColumnNames.map(referencedColumnName =>
514
+ this.#namingStrategy.joinKeyColumnName(
515
+ meta.root.className,
516
+ referencedColumnName,
517
+ meta.compositePK,
518
+ ownerTableName,
519
+ ),
520
+ );
521
+ }
522
+ const inverseTableName = this.isExplicitTableName(meta2.root) ? meta2.root.tableName : undefined;
523
+ prop.inverseJoinColumns ??= this.initManyToOneFieldName(prop, meta2.root.className, inverseTableName);
524
+ }
525
+ isExplicitTableName(meta) {
526
+ return meta.tableName !== this.#namingStrategy.classToTableName(meta.className);
527
+ }
528
+ initManyToOneFields(prop) {
529
+ if (prop.polymorphic && prop.polymorphTargets) {
530
+ const fieldNames1 = prop.targetMeta.getPrimaryProps().flatMap(pk => pk.fieldNames);
531
+ const idColumns = fieldNames1.map(fieldName =>
532
+ this.#namingStrategy.joinKeyColumnName(prop.discriminator, fieldName, fieldNames1.length > 1),
533
+ );
534
+ prop.fieldNames ??= [prop.discriminatorColumn, ...idColumns];
535
+ prop.joinColumns ??= idColumns;
536
+ prop.referencedColumnNames ??= fieldNames1;
537
+ prop.referencedTableName ??= prop.targetMeta.tableName;
538
+ return;
539
+ }
540
+ const meta2 = prop.targetMeta;
541
+ let fieldNames;
542
+ // If targetKey is specified, use that property's field names instead of PKs
543
+ if (prop.targetKey) {
544
+ const targetProp = meta2.properties[prop.targetKey];
545
+ fieldNames = targetProp.fieldNames;
546
+ } else {
547
+ fieldNames = Utils.flatten(meta2.primaryKeys.map(primaryKey => meta2.properties[primaryKey].fieldNames));
548
+ }
549
+ Utils.defaultValue(prop, 'referencedTableName', meta2.tableName);
550
+ if (!prop.joinColumns) {
551
+ prop.joinColumns = fieldNames.map(fieldName =>
552
+ this.#namingStrategy.joinKeyColumnName(prop.name, fieldName, fieldNames.length > 1),
553
+ );
554
+ }
555
+ if (!prop.referencedColumnNames) {
556
+ prop.referencedColumnNames = fieldNames;
557
+ }
558
+ // Relations to composite PK targets need cascade update by default,
559
+ // since composite PKs are more likely to have mutable components
560
+ if (meta2.compositePK) {
561
+ prop.updateRule ??= 'cascade';
562
+ }
563
+ // Nullable relations default to 'set null' on delete - when the referenced
564
+ // entity is deleted, set the FK to null rather than failing
565
+ if (prop.nullable) {
566
+ prop.deleteRule ??= 'set null';
567
+ }
568
+ }
569
+ initOneToManyFields(prop) {
570
+ const meta2 = prop.targetMeta;
571
+ if (!prop.joinColumns) {
572
+ prop.joinColumns = [this.#namingStrategy.joinColumnName(prop.name)];
573
+ }
574
+ if (!prop.referencedColumnNames) {
575
+ meta2.getPrimaryProps().forEach(pk => this.applyNamingStrategy(meta2, pk));
576
+ prop.referencedColumnNames = Utils.flatten(meta2.getPrimaryProps().map(pk => pk.fieldNames));
577
+ }
578
+ }
579
+ processEntity(meta) {
580
+ const pks = Object.values(meta.properties).filter(prop => prop.primary);
581
+ meta.primaryKeys = pks.map(prop => prop.name);
582
+ meta.compositePK = pks.length > 1;
583
+ // FK used as PK, we need to cascade - applies to both single FK-as-PK
584
+ // and composite PKs where all PKs are FKs (e.g., pivot-like entities)
585
+ const fkPks = pks.filter(pk => pk.kind !== ReferenceKind.SCALAR);
586
+ if (fkPks.length > 0 && fkPks.length === pks.length) {
587
+ for (const pk of fkPks) {
588
+ pk.deleteRule ??= 'cascade';
589
+ pk.updateRule ??= 'cascade';
590
+ }
591
+ }
592
+ meta.forceConstructor ??= this.shouldForceConstructorUsage(meta);
593
+ this.#validator.validateEntityDefinition(this.#metadata, meta.class, this.#config.get('discovery'));
594
+ for (const prop of Object.values(meta.properties)) {
595
+ this.initNullability(prop);
596
+ this.applyNamingStrategy(meta, prop);
597
+ this.initDefaultValue(prop);
598
+ this.inferTypeFromDefault(prop);
599
+ this.initVersionProperty(meta, prop);
600
+ this.initCustomType(meta, prop);
601
+ this.initColumnType(prop);
602
+ this.initRelation(prop);
603
+ }
604
+ this.initOwnColumns(meta);
605
+ meta.simplePK =
606
+ pks.length === 1 && pks[0].kind === ReferenceKind.SCALAR && !pks[0].customType && pks[0].runtimeType !== 'Date';
607
+ meta.serializedPrimaryKey ??= meta.props.find(prop => prop.serializedPrimaryKey)?.name;
608
+ if (meta.serializedPrimaryKey && meta.serializedPrimaryKey !== meta.primaryKeys[0]) {
609
+ meta.properties[meta.serializedPrimaryKey].persist ??= false;
610
+ }
611
+ if (this.#platform.usesPivotTable()) {
612
+ return Object.values(meta.properties)
613
+ .filter(prop => prop.kind === ReferenceKind.MANY_TO_MANY && prop.owner && prop.pivotTable)
614
+ .map(prop => {
615
+ const pivotMeta = this.definePivotTableEntity(meta, prop);
616
+ prop.pivotEntity = pivotMeta.class;
617
+ if (prop.inversedBy) {
618
+ prop.targetMeta.properties[prop.inversedBy].pivotEntity = pivotMeta.class;
619
+ }
620
+ return pivotMeta;
690
621
  });
691
- prop.pivotEntity = pivotMeta2.class;
692
- if (prop.fixedOrder) {
693
- const primaryProp = this.defineFixedOrderProperty(prop, targetMeta);
694
- pivotMeta2.properties[primaryProp.name] = primaryProp;
695
- }
696
- else {
697
- pivotMeta2.compositePK = true;
698
- }
699
- // handle self-referenced m:n with same default field names
700
- if (meta.className === targetType &&
701
- prop.joinColumns.every((joinColumn, idx) => joinColumn === prop.inverseJoinColumns[idx])) {
702
- // use tableName only when explicitly provided by user, otherwise use className for backwards compatibility
703
- const baseName = this.isExplicitTableName(meta) ? meta.tableName : meta.className;
704
- prop.joinColumns = prop.referencedColumnNames.map(name => this.#namingStrategy.joinKeyColumnName(baseName + '_1', name, meta.compositePK));
705
- prop.inverseJoinColumns = prop.referencedColumnNames.map(name => this.#namingStrategy.joinKeyColumnName(baseName + '_2', name, meta.compositePK));
706
- if (prop.inversedBy) {
707
- const prop2 = targetMeta.properties[prop.inversedBy];
708
- prop2.inverseJoinColumns = prop.joinColumns;
709
- prop2.joinColumns = prop.inverseJoinColumns;
710
- }
711
- // propagate updated joinColumns to all child entities that inherit this property (STI)
712
- for (const childMeta of this.#discovered.filter(m => m.root === meta && m !== meta)) {
713
- const childProp = childMeta.properties[prop.name];
714
- if (childProp) {
715
- childProp.joinColumns = prop.joinColumns;
716
- childProp.inverseJoinColumns = prop.inverseJoinColumns;
717
- }
718
- }
719
- }
720
- // For polymorphic M:N, create discriminator column and polymorphic FK
721
- if (prop.polymorphic && prop.discriminatorColumn) {
722
- this.definePolymorphicPivotProperties(pivotMeta2, meta, prop, targetMeta);
723
- }
724
- else {
725
- pivotMeta2.properties[meta.name + '_owner'] = this.definePivotProperty(prop, meta.name + '_owner', meta.class, targetType + '_inverse', true, meta.className === targetType);
726
- pivotMeta2.properties[targetType + '_inverse'] = this.definePivotProperty(prop, targetType + '_inverse', targetMeta.class, meta.name + '_owner', false, meta.className === targetType);
727
- }
728
- return this.#metadata.set(pivotMeta2.class, EntitySchema.fromMetadata(pivotMeta2).init().meta);
729
- }
730
- /**
731
- * Create a scalar property for a pivot table column.
732
- */
733
- createPivotScalarProperty(name, columnTypes, fieldNames = [name], options = {}) {
734
- return {
735
- name,
736
- fieldNames,
737
- columnTypes,
738
- type: options.type ?? 'number',
739
- kind: ReferenceKind.SCALAR,
740
- primary: options.primary ?? false,
741
- nullable: options.nullable ?? true,
742
- ...(options.persist !== undefined && { persist: options.persist }),
743
- };
744
- }
745
- /**
746
- * Get column types for an entity's primary keys, initializing them if needed.
747
- */
748
- getPrimaryKeyColumnTypes(meta) {
749
- const columnTypes = [];
750
- for (const pk of meta.primaryKeys) {
751
- const pkProp = meta.properties[pk];
752
- this.initCustomType(meta, pkProp);
753
- this.initColumnType(pkProp);
754
- columnTypes.push(...pkProp.columnTypes);
755
- }
756
- return columnTypes;
757
- }
758
- /**
759
- * Add missing FK columns for a polymorphic entity to an existing pivot table.
760
- */
761
- addPolymorphicPivotColumns(pivotMeta, meta, prop) {
762
- const existingFieldNames = new Set(Object.values(pivotMeta.properties).flatMap(p => p.fieldNames ?? []));
763
- const columnTypes = this.getPrimaryKeyColumnTypes(meta);
764
- for (let i = 0; i < prop.joinColumns.length; i++) {
765
- const joinColumn = prop.joinColumns[i];
766
- if (!existingFieldNames.has(joinColumn)) {
767
- pivotMeta.properties[joinColumn] = this.createPivotScalarProperty(joinColumn, [columnTypes[i]]);
768
- }
769
- }
770
622
  }
771
- /**
772
- * Define properties for a polymorphic pivot table.
773
- */
774
- definePolymorphicPivotProperties(pivotMeta, meta, prop, targetMeta) {
775
- const discriminatorColumn = prop.discriminatorColumn;
776
- const isCompositePK = meta.compositePK;
777
- // For composite PK polymorphic M:N, we need fixedOrder (auto-increment PK)
778
- if (isCompositePK && !prop.fixedOrder) {
779
- prop.fixedOrder = true;
780
- const primaryProp = this.defineFixedOrderProperty(prop, targetMeta);
781
- pivotMeta.properties[primaryProp.name] = primaryProp;
782
- pivotMeta.compositePK = false;
783
- }
784
- const discriminatorProp = this.createPivotScalarProperty(discriminatorColumn, [this.#platform.getVarcharTypeDeclarationSQL(prop)], [discriminatorColumn], { type: 'string', primary: !isCompositePK, nullable: false });
785
- this.initFieldName(discriminatorProp);
786
- pivotMeta.properties[discriminatorColumn] = discriminatorProp;
787
- const columnTypes = this.getPrimaryKeyColumnTypes(meta);
788
- if (isCompositePK) {
789
- // Create separate properties for each PK column (nullable for other entity types)
790
- for (let i = 0; i < prop.joinColumns.length; i++) {
791
- pivotMeta.properties[prop.joinColumns[i]] = this.createPivotScalarProperty(prop.joinColumns[i], [
792
- columnTypes[i],
793
- ]);
794
- }
795
- // Virtual property combining all columns (for compatibility)
796
- pivotMeta.properties[prop.discriminator] = this.createPivotScalarProperty(prop.discriminator, columnTypes, [...prop.joinColumns], { type: meta.className, persist: false });
797
- }
798
- else {
799
- pivotMeta.properties[prop.discriminator] = this.createPivotScalarProperty(prop.discriminator, columnTypes, [...prop.joinColumns], { type: meta.className, primary: true, nullable: false });
800
- }
801
- pivotMeta.properties[targetMeta.className + '_inverse'] = this.definePivotProperty(prop, targetMeta.className + '_inverse', targetMeta.class, prop.discriminator, false, false);
802
- // Create virtual M:1 relation to the polymorphic owner for single-query join loading
803
- const ownerRelationName = `${prop.discriminator}_${meta.tableName}`;
804
- pivotMeta.properties[ownerRelationName] = this.definePolymorphicOwnerRelation(prop, ownerRelationName, meta);
623
+ return [];
624
+ }
625
+ findReferencingProperties(meta, metadata) {
626
+ for (const meta2 of metadata) {
627
+ for (const prop2 of meta2.relations) {
628
+ if (prop2.kind !== ReferenceKind.SCALAR && prop2.type === meta.className) {
629
+ meta.referencingProperties.push({ meta: meta2, prop: prop2 });
630
+ }
631
+ }
632
+ }
633
+ }
634
+ initFactoryField(meta, prop) {
635
+ ['mappedBy', 'inversedBy', 'pivotEntity'].forEach(type => {
636
+ const value = prop[type];
637
+ if (value instanceof Function) {
638
+ const meta2 = prop.targetMeta ?? this.#metadata.get(prop.target);
639
+ prop[type] = value(meta2.properties)?.name;
640
+ if (type === 'pivotEntity' && value) {
641
+ prop[type] = value(meta2.properties);
642
+ }
643
+ if (prop[type] == null) {
644
+ throw MetadataError.fromWrongReference(meta, prop, type);
645
+ }
646
+ }
647
+ });
648
+ }
649
+ ensureCorrectFKOrderInPivotEntity(meta, owner) {
650
+ const pks = Object.values(meta.properties).filter(p => p.primary);
651
+ const fks = Object.values(meta.properties).filter(p => p.kind === ReferenceKind.MANY_TO_ONE);
652
+ let first, second;
653
+ if (pks.length === 2) {
654
+ [first, second] = pks;
655
+ } else if (fks.length >= 2) {
656
+ [first, second] = fks;
657
+ } else {
658
+ /* v8 ignore next */
659
+ return [];
660
+ }
661
+ // wrong FK order, first FK needs to point to the owning side
662
+ // (note that we can detect this only if the FKs target different types)
663
+ if (owner.type === first.type && first.type !== second.type) {
664
+ delete meta.properties[first.name];
665
+ meta.removeProperty(first.name, false);
666
+ meta.addProperty(first);
667
+ [first, second] = [second, first];
668
+ }
669
+ return [first, second];
670
+ }
671
+ definePivotTableEntity(meta, prop) {
672
+ const pivotMeta = prop.pivotEntity
673
+ ? this.#metadata.find(prop.pivotEntity)
674
+ : this.#metadata.getByClassName(prop.pivotTable, false);
675
+ // ensure inverse side exists so we can join it when populating via pivot tables
676
+ if (!prop.inversedBy && prop.targetMeta) {
677
+ const inverseName = `${meta.className}_${prop.name}__inverse`;
678
+ prop.inversedBy = inverseName;
679
+ const inverseProp = {
680
+ name: inverseName,
681
+ kind: ReferenceKind.MANY_TO_MANY,
682
+ type: meta.className,
683
+ target: meta.class,
684
+ targetMeta: meta,
685
+ mappedBy: prop.name,
686
+ pivotEntity: prop.pivotEntity,
687
+ pivotTable: prop.pivotTable,
688
+ persist: false,
689
+ hydrate: false,
690
+ };
691
+ this.applyNamingStrategy(prop.targetMeta, inverseProp);
692
+ this.initCustomType(prop.targetMeta, inverseProp);
693
+ prop.targetMeta.properties[inverseName] = inverseProp;
694
+ }
695
+ if (pivotMeta) {
696
+ prop.pivotEntity = pivotMeta.class;
697
+ this.ensureCorrectFKOrderInPivotEntity(pivotMeta, prop);
698
+ if (prop.polymorphic && prop.discriminatorValue) {
805
699
  pivotMeta.polymorphicDiscriminatorMap ??= {};
806
700
  pivotMeta.polymorphicDiscriminatorMap[prop.discriminatorValue] = meta.class;
807
- }
808
- /**
809
- * Create a virtual M:1 relation from pivot to a polymorphic owner entity.
810
- * This enables single-query join loading for inverse-side polymorphic M:N.
811
- */
812
- definePolymorphicOwnerRelation(prop, name, targetMeta) {
813
- const ret = {
814
- name,
815
- type: targetMeta.className,
816
- target: targetMeta.class,
817
- kind: ReferenceKind.MANY_TO_ONE,
818
- nullable: true,
819
- owner: true,
820
- primary: false,
821
- createForeignKeyConstraint: false,
822
- persist: false,
823
- index: false,
824
- };
825
- ret.targetMeta = targetMeta;
826
- ret.fieldNames = ret.joinColumns = ret.ownColumns = [...prop.joinColumns];
827
- ret.referencedColumnNames = [];
828
- ret.inverseJoinColumns = [];
829
- for (const primaryKey of targetMeta.primaryKeys) {
830
- const pkProp = targetMeta.properties[primaryKey];
831
- ret.referencedColumnNames.push(...pkProp.fieldNames);
832
- ret.inverseJoinColumns.push(...pkProp.fieldNames);
833
- ret.length = pkProp.length;
834
- ret.precision = pkProp.precision;
835
- ret.scale = pkProp.scale;
836
- }
837
- const schema = targetMeta.schema ?? this.#config.get('schema') ?? this.#platform.getDefaultSchemaName();
838
- ret.referencedTableName = schema && schema !== '*' ? schema + '.' + targetMeta.tableName : targetMeta.tableName;
839
- this.initColumnType(ret);
840
- this.initRelation(ret);
841
- return ret;
842
- }
843
- defineFixedOrderProperty(prop, targetMeta) {
844
- const pk = prop.fixedOrderColumn || this.#namingStrategy.referenceColumnName();
845
- const primaryProp = {
846
- name: pk,
847
- type: 'number',
848
- runtimeType: 'number',
849
- kind: ReferenceKind.SCALAR,
850
- primary: true,
851
- autoincrement: true,
852
- unsigned: this.#platform.supportsUnsigned(),
853
- };
854
- this.initFieldName(primaryProp);
855
- this.initColumnType(primaryProp);
856
- prop.fixedOrderColumn = pk;
857
- if (prop.inversedBy) {
858
- const prop2 = targetMeta.properties[prop.inversedBy];
859
- prop2.fixedOrder = true;
860
- prop2.fixedOrderColumn = pk;
861
- }
862
- return primaryProp;
863
- }
864
- definePivotProperty(prop, name, type, inverse, owner, selfReferencing) {
865
- const ret = {
866
- name,
867
- type: Utils.className(type),
868
- target: type,
869
- kind: ReferenceKind.MANY_TO_ONE,
870
- cascade: [Cascade.ALL],
871
- fixedOrder: prop.fixedOrder,
872
- fixedOrderColumn: prop.fixedOrderColumn,
873
- index: this.#platform.indexForeignKeys(),
874
- primary: !prop.fixedOrder,
875
- autoincrement: false,
876
- updateRule: prop.updateRule,
877
- deleteRule: prop.deleteRule,
878
- createForeignKeyConstraint: prop.createForeignKeyConstraint,
879
- };
880
- const defaultRule = selfReferencing && !this.#platform.supportsMultipleCascadePaths() ? 'no action' : 'cascade';
881
- ret.updateRule ??= defaultRule;
882
- ret.deleteRule ??= defaultRule;
883
- const meta = this.#metadata.get(type);
884
- ret.targetMeta = meta;
885
- ret.joinColumns = [];
886
- ret.inverseJoinColumns = [];
887
- const schema = meta.schema ?? this.#config.get('schema') ?? this.#platform.getDefaultSchemaName();
888
- ret.referencedTableName = schema && schema !== '*' ? schema + '.' + meta.tableName : meta.tableName;
889
- if (owner) {
890
- ret.owner = true;
891
- ret.inversedBy = inverse;
892
- ret.referencedColumnNames = prop.referencedColumnNames;
893
- ret.fieldNames = ret.joinColumns = prop.joinColumns;
894
- ret.inverseJoinColumns = prop.referencedColumnNames;
895
- meta.primaryKeys.forEach(primaryKey => {
896
- const prop2 = meta.properties[primaryKey];
897
- ret.length = prop2.length;
898
- ret.precision = prop2.precision;
899
- ret.scale = prop2.scale;
900
- });
901
- }
902
- else {
903
- ret.owner = false;
904
- ret.mappedBy = inverse;
905
- ret.fieldNames = ret.joinColumns = prop.inverseJoinColumns;
906
- ret.referencedColumnNames = [];
907
- ret.inverseJoinColumns = [];
908
- meta.primaryKeys.forEach(primaryKey => {
909
- const prop2 = meta.properties[primaryKey];
910
- ret.referencedColumnNames.push(...prop2.fieldNames);
911
- ret.inverseJoinColumns.push(...prop2.fieldNames);
912
- ret.length = prop2.length;
913
- ret.precision = prop2.precision;
914
- ret.scale = prop2.scale;
915
- });
916
- }
917
- this.initColumnType(ret);
918
- this.initRelation(ret);
919
- return ret;
920
- }
921
- autoWireBidirectionalProperties(meta) {
922
- Object.values(meta.properties)
923
- .filter(prop => prop.kind !== ReferenceKind.SCALAR && !prop.owner && prop.mappedBy)
924
- .forEach(prop => {
925
- const meta2 = prop.targetMeta;
926
- const prop2 = meta2.properties[prop.mappedBy];
927
- if (prop2 && !prop2.inversedBy) {
928
- prop2.inversedBy = prop.name;
929
- }
930
- });
931
- }
932
- defineBaseEntityProperties(meta) {
933
- const base = meta.extends && this.#metadata.get(meta.extends);
934
- if (!base || base === meta) {
935
- // make sure we do not fall into infinite loop
936
- return 0;
937
- }
938
- let order = this.defineBaseEntityProperties(base);
939
- const ownProps = Object.values(meta.properties);
940
- const old = ownProps.map(x => x.name);
941
- meta.properties = {};
942
- Object.values(base.properties).forEach(prop => {
943
- if (!prop.inherited) {
944
- meta.properties[prop.name] = prop;
945
- }
946
- });
947
- ownProps.forEach(prop => (meta.properties[prop.name] = prop));
948
- meta.filters = { ...base.filters, ...meta.filters };
949
- if (!meta.discriminatorValue) {
950
- Object.values(base.properties)
951
- .filter(prop => !old.includes(prop.name))
952
- .forEach(prop => {
953
- meta.properties[prop.name] = { ...prop };
954
- meta.propertyOrder.set(prop.name, (order += 0.01));
955
- });
956
- }
957
- meta.indexes = Utils.unique([...base.indexes, ...meta.indexes]);
958
- meta.uniques = Utils.unique([...base.uniques, ...meta.uniques]);
959
- meta.checks = Utils.unique([...base.checks, ...meta.checks]);
960
- const pks = Object.values(meta.properties)
961
- .filter(p => p.primary)
962
- .map(p => p.name);
963
- if (pks.length > 0 && meta.primaryKeys.length === 0) {
964
- meta.primaryKeys = pks;
965
- }
966
- Utils.keys(base.hooks).forEach(type => {
967
- meta.hooks[type] = Utils.unique([...base.hooks[type], ...(meta.hooks[type] || [])]);
701
+ // For composite PK entities sharing a polymorphic pivot table,
702
+ // we need to add columns for each entity type's PKs
703
+ this.addPolymorphicPivotColumns(pivotMeta, meta, prop);
704
+ // Add virtual M:1 relation for this polymorphic owner (for join loading)
705
+ const ownerRelationName = `${prop.discriminator}_${meta.tableName}`;
706
+ if (!pivotMeta.properties[ownerRelationName]) {
707
+ pivotMeta.properties[ownerRelationName] = this.definePolymorphicOwnerRelation(prop, ownerRelationName, meta);
708
+ }
709
+ }
710
+ return pivotMeta;
711
+ }
712
+ let tableName = prop.pivotTable;
713
+ let schemaName;
714
+ if (prop.pivotTable.includes('.')) {
715
+ [schemaName, tableName] = prop.pivotTable.split('.');
716
+ }
717
+ schemaName ??= meta.schema;
718
+ const targetMeta = prop.targetMeta;
719
+ const targetType = targetMeta.className;
720
+ const pivotMeta2 = new EntityMetadata({
721
+ name: prop.pivotTable,
722
+ className: prop.pivotTable,
723
+ collection: tableName,
724
+ schema: schemaName,
725
+ pivotTable: true,
726
+ });
727
+ prop.pivotEntity = pivotMeta2.class;
728
+ if (prop.fixedOrder) {
729
+ const primaryProp = this.defineFixedOrderProperty(prop, targetMeta);
730
+ pivotMeta2.properties[primaryProp.name] = primaryProp;
731
+ } else {
732
+ pivotMeta2.compositePK = true;
733
+ }
734
+ // handle self-referenced m:n with same default field names
735
+ if (
736
+ meta.className === targetType &&
737
+ prop.joinColumns.every((joinColumn, idx) => joinColumn === prop.inverseJoinColumns[idx])
738
+ ) {
739
+ // use tableName only when explicitly provided by user, otherwise use className for backwards compatibility
740
+ const baseName = this.isExplicitTableName(meta) ? meta.tableName : meta.className;
741
+ prop.joinColumns = prop.referencedColumnNames.map(name =>
742
+ this.#namingStrategy.joinKeyColumnName(baseName + '_1', name, meta.compositePK),
743
+ );
744
+ prop.inverseJoinColumns = prop.referencedColumnNames.map(name =>
745
+ this.#namingStrategy.joinKeyColumnName(baseName + '_2', name, meta.compositePK),
746
+ );
747
+ if (prop.inversedBy) {
748
+ const prop2 = targetMeta.properties[prop.inversedBy];
749
+ prop2.inverseJoinColumns = prop.joinColumns;
750
+ prop2.joinColumns = prop.inverseJoinColumns;
751
+ }
752
+ // propagate updated joinColumns to all child entities that inherit this property (STI)
753
+ for (const childMeta of this.#discovered.filter(m => m.root === meta && m !== meta)) {
754
+ const childProp = childMeta.properties[prop.name];
755
+ if (childProp) {
756
+ childProp.joinColumns = prop.joinColumns;
757
+ childProp.inverseJoinColumns = prop.inverseJoinColumns;
758
+ }
759
+ }
760
+ }
761
+ // For polymorphic M:N, create discriminator column and polymorphic FK
762
+ if (prop.polymorphic && prop.discriminatorColumn) {
763
+ this.definePolymorphicPivotProperties(pivotMeta2, meta, prop, targetMeta);
764
+ } else {
765
+ pivotMeta2.properties[meta.name + '_owner'] = this.definePivotProperty(
766
+ prop,
767
+ meta.name + '_owner',
768
+ meta.class,
769
+ targetType + '_inverse',
770
+ true,
771
+ meta.className === targetType,
772
+ );
773
+ pivotMeta2.properties[targetType + '_inverse'] = this.definePivotProperty(
774
+ prop,
775
+ targetType + '_inverse',
776
+ targetMeta.class,
777
+ meta.name + '_owner',
778
+ false,
779
+ meta.className === targetType,
780
+ );
781
+ }
782
+ return this.#metadata.set(pivotMeta2.class, EntitySchema.fromMetadata(pivotMeta2).init().meta);
783
+ }
784
+ /**
785
+ * Create a scalar property for a pivot table column.
786
+ */
787
+ createPivotScalarProperty(name, columnTypes, fieldNames = [name], options = {}) {
788
+ return {
789
+ name,
790
+ fieldNames,
791
+ columnTypes,
792
+ type: options.type ?? 'number',
793
+ kind: ReferenceKind.SCALAR,
794
+ primary: options.primary ?? false,
795
+ nullable: options.nullable ?? true,
796
+ ...(options.persist !== undefined && { persist: options.persist }),
797
+ };
798
+ }
799
+ /**
800
+ * Get column types for an entity's primary keys, initializing them if needed.
801
+ */
802
+ getPrimaryKeyColumnTypes(meta) {
803
+ const columnTypes = [];
804
+ for (const pk of meta.primaryKeys) {
805
+ const pkProp = meta.properties[pk];
806
+ this.initCustomType(meta, pkProp);
807
+ this.initColumnType(pkProp);
808
+ columnTypes.push(...pkProp.columnTypes);
809
+ }
810
+ return columnTypes;
811
+ }
812
+ /**
813
+ * Add missing FK columns for a polymorphic entity to an existing pivot table.
814
+ */
815
+ addPolymorphicPivotColumns(pivotMeta, meta, prop) {
816
+ const existingFieldNames = new Set(Object.values(pivotMeta.properties).flatMap(p => p.fieldNames ?? []));
817
+ const columnTypes = this.getPrimaryKeyColumnTypes(meta);
818
+ for (let i = 0; i < prop.joinColumns.length; i++) {
819
+ const joinColumn = prop.joinColumns[i];
820
+ if (!existingFieldNames.has(joinColumn)) {
821
+ pivotMeta.properties[joinColumn] = this.createPivotScalarProperty(joinColumn, [columnTypes[i]]);
822
+ }
823
+ }
824
+ }
825
+ /**
826
+ * Define properties for a polymorphic pivot table.
827
+ */
828
+ definePolymorphicPivotProperties(pivotMeta, meta, prop, targetMeta) {
829
+ const discriminatorColumn = prop.discriminatorColumn;
830
+ const isCompositePK = meta.compositePK;
831
+ // For composite PK polymorphic M:N, we need fixedOrder (auto-increment PK)
832
+ if (isCompositePK && !prop.fixedOrder) {
833
+ prop.fixedOrder = true;
834
+ const primaryProp = this.defineFixedOrderProperty(prop, targetMeta);
835
+ pivotMeta.properties[primaryProp.name] = primaryProp;
836
+ pivotMeta.compositePK = false;
837
+ }
838
+ const discriminatorProp = this.createPivotScalarProperty(
839
+ discriminatorColumn,
840
+ [this.#platform.getVarcharTypeDeclarationSQL(prop)],
841
+ [discriminatorColumn],
842
+ { type: 'string', primary: !isCompositePK, nullable: false },
843
+ );
844
+ this.initFieldName(discriminatorProp);
845
+ pivotMeta.properties[discriminatorColumn] = discriminatorProp;
846
+ const columnTypes = this.getPrimaryKeyColumnTypes(meta);
847
+ if (isCompositePK) {
848
+ // Create separate properties for each PK column (nullable for other entity types)
849
+ for (let i = 0; i < prop.joinColumns.length; i++) {
850
+ pivotMeta.properties[prop.joinColumns[i]] = this.createPivotScalarProperty(prop.joinColumns[i], [
851
+ columnTypes[i],
852
+ ]);
853
+ }
854
+ // Virtual property combining all columns (for compatibility)
855
+ pivotMeta.properties[prop.discriminator] = this.createPivotScalarProperty(
856
+ prop.discriminator,
857
+ columnTypes,
858
+ [...prop.joinColumns],
859
+ { type: meta.className, persist: false },
860
+ );
861
+ } else {
862
+ pivotMeta.properties[prop.discriminator] = this.createPivotScalarProperty(
863
+ prop.discriminator,
864
+ columnTypes,
865
+ [...prop.joinColumns],
866
+ { type: meta.className, primary: true, nullable: false },
867
+ );
868
+ }
869
+ pivotMeta.properties[targetMeta.className + '_inverse'] = this.definePivotProperty(
870
+ prop,
871
+ targetMeta.className + '_inverse',
872
+ targetMeta.class,
873
+ prop.discriminator,
874
+ false,
875
+ false,
876
+ );
877
+ // Create virtual M:1 relation to the polymorphic owner for single-query join loading
878
+ const ownerRelationName = `${prop.discriminator}_${meta.tableName}`;
879
+ pivotMeta.properties[ownerRelationName] = this.definePolymorphicOwnerRelation(prop, ownerRelationName, meta);
880
+ pivotMeta.polymorphicDiscriminatorMap ??= {};
881
+ pivotMeta.polymorphicDiscriminatorMap[prop.discriminatorValue] = meta.class;
882
+ }
883
+ /**
884
+ * Create a virtual M:1 relation from pivot to a polymorphic owner entity.
885
+ * This enables single-query join loading for inverse-side polymorphic M:N.
886
+ */
887
+ definePolymorphicOwnerRelation(prop, name, targetMeta) {
888
+ const ret = {
889
+ name,
890
+ type: targetMeta.className,
891
+ target: targetMeta.class,
892
+ kind: ReferenceKind.MANY_TO_ONE,
893
+ nullable: true,
894
+ owner: true,
895
+ primary: false,
896
+ createForeignKeyConstraint: false,
897
+ persist: false,
898
+ index: false,
899
+ };
900
+ ret.targetMeta = targetMeta;
901
+ ret.fieldNames = ret.joinColumns = ret.ownColumns = [...prop.joinColumns];
902
+ ret.referencedColumnNames = [];
903
+ ret.inverseJoinColumns = [];
904
+ for (const primaryKey of targetMeta.primaryKeys) {
905
+ const pkProp = targetMeta.properties[primaryKey];
906
+ ret.referencedColumnNames.push(...pkProp.fieldNames);
907
+ ret.inverseJoinColumns.push(...pkProp.fieldNames);
908
+ ret.length = pkProp.length;
909
+ ret.precision = pkProp.precision;
910
+ ret.scale = pkProp.scale;
911
+ }
912
+ const schema = targetMeta.schema ?? this.#config.get('schema') ?? this.#platform.getDefaultSchemaName();
913
+ ret.referencedTableName = schema && schema !== '*' ? schema + '.' + targetMeta.tableName : targetMeta.tableName;
914
+ this.initColumnType(ret);
915
+ this.initRelation(ret);
916
+ return ret;
917
+ }
918
+ defineFixedOrderProperty(prop, targetMeta) {
919
+ const pk = prop.fixedOrderColumn || this.#namingStrategy.referenceColumnName();
920
+ const primaryProp = {
921
+ name: pk,
922
+ type: 'number',
923
+ runtimeType: 'number',
924
+ kind: ReferenceKind.SCALAR,
925
+ primary: true,
926
+ autoincrement: true,
927
+ unsigned: this.#platform.supportsUnsigned(),
928
+ };
929
+ this.initFieldName(primaryProp);
930
+ this.initColumnType(primaryProp);
931
+ prop.fixedOrderColumn = pk;
932
+ if (prop.inversedBy) {
933
+ const prop2 = targetMeta.properties[prop.inversedBy];
934
+ prop2.fixedOrder = true;
935
+ prop2.fixedOrderColumn = pk;
936
+ }
937
+ return primaryProp;
938
+ }
939
+ definePivotProperty(prop, name, type, inverse, owner, selfReferencing) {
940
+ const ret = {
941
+ name,
942
+ type: Utils.className(type),
943
+ target: type,
944
+ kind: ReferenceKind.MANY_TO_ONE,
945
+ cascade: [Cascade.ALL],
946
+ fixedOrder: prop.fixedOrder,
947
+ fixedOrderColumn: prop.fixedOrderColumn,
948
+ index: this.#platform.indexForeignKeys(),
949
+ primary: !prop.fixedOrder,
950
+ autoincrement: false,
951
+ updateRule: prop.updateRule,
952
+ deleteRule: prop.deleteRule,
953
+ createForeignKeyConstraint: prop.createForeignKeyConstraint,
954
+ };
955
+ const defaultRule = selfReferencing && !this.#platform.supportsMultipleCascadePaths() ? 'no action' : 'cascade';
956
+ ret.updateRule ??= defaultRule;
957
+ ret.deleteRule ??= defaultRule;
958
+ const meta = this.#metadata.get(type);
959
+ ret.targetMeta = meta;
960
+ ret.joinColumns = [];
961
+ ret.inverseJoinColumns = [];
962
+ const schema = meta.schema ?? this.#config.get('schema') ?? this.#platform.getDefaultSchemaName();
963
+ ret.referencedTableName = schema && schema !== '*' ? schema + '.' + meta.tableName : meta.tableName;
964
+ if (owner) {
965
+ ret.owner = true;
966
+ ret.inversedBy = inverse;
967
+ ret.referencedColumnNames = prop.referencedColumnNames;
968
+ ret.fieldNames = ret.joinColumns = prop.joinColumns;
969
+ ret.inverseJoinColumns = prop.referencedColumnNames;
970
+ meta.primaryKeys.forEach(primaryKey => {
971
+ const prop2 = meta.properties[primaryKey];
972
+ ret.length = prop2.length;
973
+ ret.precision = prop2.precision;
974
+ ret.scale = prop2.scale;
975
+ });
976
+ } else {
977
+ ret.owner = false;
978
+ ret.mappedBy = inverse;
979
+ ret.fieldNames = ret.joinColumns = prop.inverseJoinColumns;
980
+ ret.referencedColumnNames = [];
981
+ ret.inverseJoinColumns = [];
982
+ meta.primaryKeys.forEach(primaryKey => {
983
+ const prop2 = meta.properties[primaryKey];
984
+ ret.referencedColumnNames.push(...prop2.fieldNames);
985
+ ret.inverseJoinColumns.push(...prop2.fieldNames);
986
+ ret.length = prop2.length;
987
+ ret.precision = prop2.precision;
988
+ ret.scale = prop2.scale;
989
+ });
990
+ }
991
+ this.initColumnType(ret);
992
+ this.initRelation(ret);
993
+ return ret;
994
+ }
995
+ autoWireBidirectionalProperties(meta) {
996
+ Object.values(meta.properties)
997
+ .filter(prop => prop.kind !== ReferenceKind.SCALAR && !prop.owner && prop.mappedBy)
998
+ .forEach(prop => {
999
+ const meta2 = prop.targetMeta;
1000
+ const prop2 = meta2.properties[prop.mappedBy];
1001
+ if (prop2 && !prop2.inversedBy) {
1002
+ prop2.inversedBy = prop.name;
1003
+ }
1004
+ });
1005
+ }
1006
+ defineBaseEntityProperties(meta) {
1007
+ const base = meta.extends && this.#metadata.get(meta.extends);
1008
+ if (!base || base === meta) {
1009
+ // make sure we do not fall into infinite loop
1010
+ return 0;
1011
+ }
1012
+ let order = this.defineBaseEntityProperties(base);
1013
+ const ownProps = Object.values(meta.properties);
1014
+ const old = ownProps.map(x => x.name);
1015
+ meta.properties = {};
1016
+ Object.values(base.properties).forEach(prop => {
1017
+ if (!prop.inherited) {
1018
+ meta.properties[prop.name] = prop;
1019
+ }
1020
+ });
1021
+ ownProps.forEach(prop => (meta.properties[prop.name] = prop));
1022
+ meta.filters = { ...base.filters, ...meta.filters };
1023
+ if (!meta.discriminatorValue) {
1024
+ Object.values(base.properties)
1025
+ .filter(prop => !old.includes(prop.name))
1026
+ .forEach(prop => {
1027
+ meta.properties[prop.name] = { ...prop };
1028
+ meta.propertyOrder.set(prop.name, (order += 0.01));
968
1029
  });
969
- if ((meta.constructorParams?.length ?? 0) === 0 && (base.constructorParams?.length ?? 0) > 0) {
970
- meta.constructorParams = [...base.constructorParams];
971
- }
972
- return order;
973
- }
974
- initPolyEmbeddables(embeddedProp, discovered, visited = new Set()) {
975
- if (embeddedProp.kind !== ReferenceKind.EMBEDDED || visited.has(embeddedProp)) {
976
- return;
977
- }
978
- visited.add(embeddedProp);
979
- const types = embeddedProp.type.split(/ ?\| ?/);
980
- let embeddable = this.#discovered.find(m => m.name === embeddedProp.type);
981
- const polymorphs = this.#discovered.filter(m => types.includes(m.name));
982
- // create virtual polymorphic entity
983
- if (!embeddable && polymorphs.length > 0) {
984
- const properties = {};
985
- let discriminatorColumn;
986
- const inlineProperties = (meta) => {
987
- Object.values(meta.properties).forEach(prop => {
988
- // defaults on db level would mess up with change tracking
989
- delete prop.default;
990
- if (properties[prop.name] && properties[prop.name].type !== prop.type) {
991
- properties[prop.name].type = `${properties[prop.name].type} | ${prop.type}`;
992
- properties[prop.name].runtimeType = 'any';
993
- properties[prop.name].stiMerged = true;
994
- return properties[prop.name];
995
- }
996
- // Deep copy to prevent mutating the original entity's property —
997
- // both from the merge path above (GH #6522/#6523) and from
998
- // downstream code that mutates nested arrays like fieldNames.
999
- return (properties[prop.name] = Utils.copy(prop));
1000
- });
1001
- };
1002
- const processExtensions = (meta) => {
1003
- const parent = this.#discovered.find(m => {
1004
- return meta.extends && Utils.className(meta.extends) === m.className;
1005
- });
1006
- if (!parent) {
1007
- return;
1008
- }
1009
- discriminatorColumn ??= parent.discriminatorColumn;
1010
- inlineProperties(parent);
1011
- processExtensions(parent);
1012
- };
1013
- polymorphs.forEach(meta => {
1014
- inlineProperties(meta);
1015
- processExtensions(meta);
1016
- });
1017
- const name = polymorphs
1018
- .map(t => t.className)
1019
- .sort()
1020
- .join(' | ');
1021
- embeddable = new EntityMetadata({
1022
- name,
1023
- className: name,
1024
- embeddable: true,
1025
- abstract: true,
1026
- properties,
1027
- polymorphs,
1028
- discriminatorColumn,
1029
- });
1030
- embeddable.sync();
1031
- discovered.push(embeddable);
1032
- polymorphs.forEach(meta => (meta.root = embeddable));
1033
- }
1034
- }
1035
- initPolymorphicRelation(meta, prop, discovered) {
1036
- if (!prop.discriminator && !prop.discriminatorColumn && !prop.discriminatorMap && !Array.isArray(prop.target)) {
1037
- return;
1038
- }
1039
- prop.polymorphic = true;
1040
- prop.discriminator ??= prop.name;
1041
- prop.discriminatorColumn ??= this.#namingStrategy.discriminatorColumnName(prop.discriminator);
1042
- prop.createForeignKeyConstraint = false;
1043
- const isToOne = [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind);
1044
- if (isToOne) {
1045
- const types = prop.type.split(/ ?\| ?/);
1046
- prop.polymorphTargets = discovered.filter(m => types.includes(m.className) && !m.embeddable);
1047
- prop.targetMeta = prop.polymorphTargets[0];
1048
- prop.referencedPKs = prop.targetMeta?.primaryKeys;
1049
- }
1050
- if (prop.discriminatorMap) {
1051
- const normalizedMap = {};
1052
- for (const [key, value] of Object.entries(prop.discriminatorMap)) {
1053
- const targetMeta = this.#metadata.getByClassName(value, false);
1054
- if (!targetMeta) {
1055
- throw MetadataError.fromUnknownEntity(value, `${meta.className}.${prop.name} discriminatorMap`);
1056
- }
1057
- normalizedMap[key] = targetMeta.class;
1058
- if (targetMeta.class === meta.class) {
1059
- prop.discriminatorValue = key;
1060
- }
1061
- }
1062
- prop.discriminatorMap = normalizedMap;
1063
- }
1064
- else if (isToOne) {
1065
- prop.discriminatorMap = {};
1066
- const tableNameToTarget = new Map();
1067
- for (const target of prop.polymorphTargets) {
1068
- const existing = tableNameToTarget.get(target.tableName);
1069
- if (existing) {
1070
- throw MetadataError.incompatiblePolymorphicTargets(meta, prop, existing, target, `both use table '${target.tableName}'. Use separate properties instead of a single polymorphic relation.`);
1071
- }
1072
- tableNameToTarget.set(target.tableName, target);
1073
- prop.discriminatorMap[target.tableName] = target.class;
1074
- }
1075
- }
1076
- else {
1077
- prop.discriminatorValue ??= meta.tableName;
1078
- if (!prop.discriminatorMap) {
1079
- prop.discriminatorMap = { [prop.discriminatorValue]: meta.class };
1080
- }
1081
- }
1082
- }
1083
- initEmbeddables(meta, embeddedProp, visited = new Set()) {
1084
- if (embeddedProp.kind !== ReferenceKind.EMBEDDED || visited.has(embeddedProp)) {
1085
- return;
1086
- }
1087
- visited.add(embeddedProp);
1088
- const embeddable = this.#discovered.find(m => m.name === embeddedProp.type);
1089
- if (!embeddable) {
1090
- throw MetadataError.fromUnknownEntity(embeddedProp.type, `${meta.className}.${embeddedProp.name}`);
1091
- }
1092
- embeddedProp.embeddable = embeddable.class;
1093
- embeddedProp.embeddedProps = {};
1094
- let order = meta.propertyOrder.get(embeddedProp.name);
1095
- const getRootProperty = (prop) => prop.embedded ? getRootProperty(meta.properties[prop.embedded[0]]) : prop;
1096
- const isParentObject = (prop) => {
1097
- if (prop.object || prop.array) {
1098
- return true;
1099
- }
1100
- return prop.embedded ? isParentObject(meta.properties[prop.embedded[0]]) : false;
1101
- };
1102
- const isParentArray = (prop) => {
1103
- if (prop.array) {
1104
- return true;
1105
- }
1106
- return prop.embedded ? isParentArray(meta.properties[prop.embedded[0]]) : false;
1107
- };
1108
- const rootProperty = getRootProperty(embeddedProp);
1109
- const parentProperty = meta.properties[embeddedProp.embedded?.[0] ?? ''];
1110
- const object = isParentObject(embeddedProp);
1111
- const array = isParentArray(embeddedProp);
1112
- this.initFieldName(embeddedProp, rootProperty !== embeddedProp && object);
1113
- // the prefix of the parent cannot be a boolean; it already passed here
1114
- const prefix = this.getPrefix(embeddedProp, parentProperty);
1115
- const glue = object ? '~' : '_';
1116
- for (const prop of Object.values(embeddable.properties)) {
1117
- const name = (embeddedProp.embeddedPath?.join(glue) ?? embeddedProp.fieldNames[0] + glue) + prop.name;
1118
- meta.properties[name] = Utils.copy(prop);
1119
- meta.properties[name].name = name;
1120
- meta.properties[name].embedded = [embeddedProp.name, prop.name];
1121
- meta.propertyOrder.set(name, (order += 0.01));
1122
- embeddedProp.embeddedProps[prop.name] = meta.properties[name];
1123
- meta.properties[name].persist ??= embeddedProp.persist;
1124
- const refInArray = array && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && prop.owner;
1125
- if (embeddedProp.nullable || refInArray) {
1126
- meta.properties[name].nullable = true;
1127
- }
1128
- if (meta.properties[name].fieldNames) {
1129
- meta.properties[name].fieldNames[0] = prefix + meta.properties[name].fieldNames[0];
1130
- }
1131
- else {
1132
- const name2 = meta.properties[name].name;
1133
- meta.properties[name].name = prefix + prop.name;
1134
- this.initFieldName(meta.properties[name]);
1135
- meta.properties[name].name = name2;
1136
- }
1137
- if (object) {
1138
- embeddedProp.object = true;
1139
- let path = [];
1140
- let tmp = embeddedProp;
1141
- while (tmp.embedded && tmp.object) {
1142
- path.unshift(tmp.embedded[1]);
1143
- tmp = meta.properties[tmp.embedded[0]];
1144
- }
1145
- if (tmp === rootProperty) {
1146
- path.unshift(rootProperty.fieldNames[0]);
1147
- }
1148
- else if (embeddedProp.embeddedPath) {
1149
- path = [...embeddedProp.embeddedPath];
1150
- }
1151
- else {
1152
- path = [embeddedProp.fieldNames[0]];
1153
- }
1154
- this.initFieldName(prop, true);
1155
- this.initRelation(prop);
1156
- path.push(prop.fieldNames[0]);
1157
- meta.properties[name].fieldNames = prop.fieldNames;
1158
- meta.properties[name].embeddedPath = path;
1159
- const targetProp = prop.targetMeta?.getPrimaryProp() ?? prop;
1160
- const fieldName = raw(this.#platform.getSearchJsonPropertySQL(path.join('->'), targetProp.runtimeType ?? targetProp.type, true));
1161
- meta.properties[name].fieldNameRaw = fieldName.sql; // for querying in SQL drivers
1162
- meta.properties[name].persist = false; // only virtual as we store the whole object
1163
- meta.properties[name].userDefined = false; // mark this as a generated/internal property, so we can distinguish from user-defined non-persist properties
1164
- meta.properties[name].object = true;
1165
- this.initCustomType(meta, meta.properties[name], false, true);
1166
- }
1167
- this.initEmbeddables(meta, meta.properties[name], visited);
1168
- }
1169
- for (const index of embeddable.indexes) {
1170
- meta.indexes.push({
1171
- ...index,
1172
- properties: Utils.asArray(index.properties).map(p => {
1173
- return embeddedProp.embeddedProps[p].name;
1174
- }),
1175
- });
1176
- }
1177
- for (const unique of embeddable.uniques) {
1178
- meta.uniques.push({
1179
- ...unique,
1180
- properties: Utils.asArray(unique.properties).map(p => {
1181
- return embeddedProp.embeddedProps[p].name;
1182
- }),
1183
- });
1184
- }
1185
1030
  }
1186
- initSingleTableInheritance(meta, metadata) {
1187
- if (meta.root !== meta && !meta.__processed) {
1188
- meta.root = metadata.find(m => m.class === meta.root.class);
1189
- meta.root.__processed = true;
1190
- }
1191
- else {
1192
- delete meta.root.__processed;
1193
- }
1194
- if (!meta.root.discriminatorColumn) {
1195
- return;
1196
- }
1197
- meta.root.inheritanceType = 'sti';
1198
- if (meta.root.discriminatorMap) {
1199
- const map = meta.root.discriminatorMap;
1200
- Object.keys(map)
1201
- .filter(key => typeof map[key] === 'string')
1202
- .forEach(key => (map[key] = this.#metadata.getByClassName(map[key]).class));
1203
- }
1204
- else {
1205
- meta.root.discriminatorMap = {};
1206
- const children = metadata
1207
- .filter(m => m.root.class === meta.root.class && !m.abstract)
1208
- .sort((a, b) => a.className.localeCompare(b.className));
1209
- for (const m of children) {
1210
- const name = m.discriminatorValue ?? this.#namingStrategy.classToTableName(m.className);
1211
- meta.root.discriminatorMap[name] = m.class;
1212
- }
1213
- }
1214
- meta.discriminatorValue = QueryHelper.findDiscriminatorValue(meta.root.discriminatorMap, meta.class);
1215
- if (!meta.root.properties[meta.root.discriminatorColumn]) {
1216
- this.createDiscriminatorProperty(meta.root);
1217
- }
1218
- Utils.defaultValue(meta.root.properties[meta.root.discriminatorColumn], 'items', Object.keys(meta.root.discriminatorMap));
1219
- Utils.defaultValue(meta.root.properties[meta.root.discriminatorColumn], 'index', true);
1220
- if (meta.root === meta) {
1221
- return;
1222
- }
1031
+ meta.indexes = Utils.unique([...base.indexes, ...meta.indexes]);
1032
+ meta.uniques = Utils.unique([...base.uniques, ...meta.uniques]);
1033
+ meta.checks = Utils.unique([...base.checks, ...meta.checks]);
1034
+ const pks = Object.values(meta.properties)
1035
+ .filter(p => p.primary)
1036
+ .map(p => p.name);
1037
+ if (pks.length > 0 && meta.primaryKeys.length === 0) {
1038
+ meta.primaryKeys = pks;
1039
+ }
1040
+ Utils.keys(base.hooks).forEach(type => {
1041
+ meta.hooks[type] = Utils.unique([...base.hooks[type], ...(meta.hooks[type] || [])]);
1042
+ });
1043
+ if ((meta.constructorParams?.length ?? 0) === 0 && (base.constructorParams?.length ?? 0) > 0) {
1044
+ meta.constructorParams = [...base.constructorParams];
1045
+ }
1046
+ return order;
1047
+ }
1048
+ initPolyEmbeddables(embeddedProp, discovered, visited = new Set()) {
1049
+ if (embeddedProp.kind !== ReferenceKind.EMBEDDED || visited.has(embeddedProp)) {
1050
+ return;
1051
+ }
1052
+ visited.add(embeddedProp);
1053
+ const types = embeddedProp.type.split(/ ?\| ?/);
1054
+ let embeddable = this.#discovered.find(m => m.name === embeddedProp.type);
1055
+ const polymorphs = this.#discovered.filter(m => types.includes(m.name));
1056
+ // create virtual polymorphic entity
1057
+ if (!embeddable && polymorphs.length > 0) {
1058
+ const properties = {};
1059
+ let discriminatorColumn;
1060
+ const inlineProperties = meta => {
1223
1061
  Object.values(meta.properties).forEach(prop => {
1224
- const newProp = { ...prop };
1225
- const rootProp = meta.root.properties[prop.name];
1226
- // stiMerged is set during inlineProperties when a property was merged
1227
- // from multiple polymorphic variants with different types. The flag is
1228
- // cleared implicitly when the first child claims the root property via
1229
- // addProperty below, so subsequent children correctly trigger renaming.
1230
- const typesMatch = rootProp?.type === prop.type || rootProp?.stiMerged === true;
1231
- if (rootProp &&
1232
- (!typesMatch ||
1233
- (rootProp.fieldNames && prop.fieldNames && !compareArrays(rootProp.fieldNames, prop.fieldNames)))) {
1234
- const name = newProp.name;
1235
- this.initFieldName(newProp, newProp.object);
1236
- newProp.renamedFrom = name;
1237
- newProp.name = `${name}_${meta._id}`;
1238
- meta.root.addProperty(newProp);
1239
- this.initFieldName(prop, prop.object);
1240
- // Track all field variants and map discriminator values to field names
1241
- if (!rootProp.stiFieldNames) {
1242
- this.initFieldName(rootProp, rootProp.object);
1243
- rootProp.stiFieldNames = [...rootProp.fieldNames];
1244
- rootProp.stiFieldNameMap = {};
1245
- // Find which discriminator owns the original fieldNames
1246
- for (const [discValue, childClass] of Object.entries(meta.root.discriminatorMap)) {
1247
- const childMeta = this.#metadata.find(childClass);
1248
- if (childMeta?.properties[prop.name]?.fieldNames &&
1249
- compareArrays(childMeta.properties[prop.name].fieldNames, rootProp.fieldNames)) {
1250
- rootProp.stiFieldNameMap[discValue] = rootProp.fieldNames[0];
1251
- break;
1252
- }
1253
- }
1254
- }
1255
- rootProp.stiFieldNameMap[meta.discriminatorValue] = prop.fieldNames[0];
1256
- rootProp.stiFieldNames.push(...prop.fieldNames);
1257
- newProp.nullable = true;
1258
- newProp.name = name;
1259
- newProp.hydrate = false;
1260
- newProp.inherited = true;
1261
- return;
1262
- }
1263
- if (prop.enum && prop.items && rootProp?.items) {
1264
- newProp.items = Utils.unique([...rootProp.items, ...prop.items]);
1265
- }
1266
- newProp.nullable = true;
1267
- newProp.inherited = !rootProp;
1268
- meta.root.addProperty(newProp);
1062
+ // defaults on db level would mess up with change tracking
1063
+ delete prop.default;
1064
+ if (properties[prop.name] && properties[prop.name].type !== prop.type) {
1065
+ properties[prop.name].type = `${properties[prop.name].type} | ${prop.type}`;
1066
+ properties[prop.name].runtimeType = 'any';
1067
+ properties[prop.name].stiMerged = true;
1068
+ return properties[prop.name];
1069
+ }
1070
+ // Deep copy to prevent mutating the original entity's property —
1071
+ // both from the merge path above (GH #6522/#6523) and from
1072
+ // downstream code that mutates nested arrays like fieldNames.
1073
+ return (properties[prop.name] = Utils.copy(prop));
1269
1074
  });
1270
- meta.tableName = meta.root.tableName;
1271
- meta.root.indexes = Utils.unique([...meta.root.indexes, ...meta.indexes]);
1272
- meta.root.uniques = Utils.unique([...meta.root.uniques, ...meta.uniques]);
1273
- meta.root.checks = Utils.unique([...meta.root.checks, ...meta.checks]);
1274
- }
1275
- /**
1276
- * First pass of TPT initialization: sets up hierarchy relationships
1277
- * (inheritanceType, tptParent, tptChildren) before properties have fieldNames.
1278
- */
1279
- initTPTRelationships(meta, metadata) {
1280
- if (meta.root !== meta) {
1281
- meta.root = metadata.find(m => m.class === meta.root.class);
1282
- }
1283
- const inheritance = meta.inheritance;
1284
- if (inheritance === 'tpt' && meta.root === meta) {
1285
- meta.inheritanceType = 'tpt';
1286
- meta.tptChildren = [];
1287
- }
1288
- if (meta.root.inheritanceType !== 'tpt') {
1289
- return;
1290
- }
1291
- const parent = this.getTPTParent(meta, metadata);
1292
- if (parent) {
1293
- meta.tptParent = parent;
1294
- meta.inheritanceType = 'tpt';
1295
- parent.tptChildren ??= [];
1296
- if (!parent.tptChildren.includes(meta)) {
1297
- parent.tptChildren.push(meta);
1298
- }
1299
- }
1300
- }
1301
- /**
1302
- * Second pass of TPT initialization: re-resolves metadata references after fieldNames
1303
- * are set, syncs to registry metadata, and sets up discriminators.
1304
- */
1305
- finalizeTPTInheritance(meta, metadata) {
1306
- if (meta.inheritanceType !== 'tpt') {
1307
- return;
1308
- }
1309
- if (meta.tptParent) {
1310
- meta.tptParent = metadata.find(m => m.class === meta.tptParent.class) ?? meta.tptParent;
1311
- }
1312
- if (meta.tptChildren) {
1313
- meta.tptChildren = meta.tptChildren.map(child => metadata.find(m => m.class === child.class) ?? child);
1314
- }
1315
- const registryMeta = this.#metadata.get(meta.class);
1316
- if (registryMeta && registryMeta !== meta) {
1317
- registryMeta.inheritanceType = meta.inheritanceType;
1318
- registryMeta.tptParent = meta.tptParent ? this.#metadata.get(meta.tptParent.class) : undefined;
1319
- registryMeta.tptChildren = meta.tptChildren?.map(child => this.#metadata.get(child.class));
1320
- }
1321
- this.initTPTDiscriminator(meta, metadata);
1322
- }
1323
- /**
1324
- * Initialize TPT discriminator map and virtual discriminator property.
1325
- * Unlike STI where the discriminator is a persisted column, TPT discriminator is computed
1326
- * at query time using CASE WHEN expressions based on which child table has data.
1327
- */
1328
- initTPTDiscriminator(meta, metadata) {
1329
- const allDescendants = this.collectAllTPTDescendants(meta, metadata);
1330
- allDescendants.sort((a, b) => this.getTPTDepth(b) - this.getTPTDepth(a));
1331
- meta.allTPTDescendants = allDescendants;
1332
- if (meta.root !== meta) {
1333
- return;
1334
- }
1335
- meta.root.discriminatorMap = {};
1336
- for (const m of allDescendants) {
1337
- const name = this.#namingStrategy.classToTableName(m.className);
1338
- meta.root.discriminatorMap[name] = m.class;
1339
- m.discriminatorValue = name;
1340
- }
1341
- if (!meta.abstract) {
1342
- const name = this.#namingStrategy.classToTableName(meta.className);
1343
- meta.root.discriminatorMap[name] = meta.class;
1344
- meta.discriminatorValue = name;
1345
- }
1346
- // Virtual discriminator property - computed at query time via CASE WHEN, not persisted
1347
- const discriminatorColumn = '__tpt_type';
1348
- if (!meta.root.properties[discriminatorColumn]) {
1349
- meta.root.addProperty({
1350
- name: discriminatorColumn,
1351
- type: 'string',
1352
- kind: ReferenceKind.SCALAR,
1353
- persist: false,
1354
- userDefined: false,
1355
- hidden: true,
1356
- });
1357
- }
1358
- meta.root.tptDiscriminatorColumn = discriminatorColumn;
1359
- }
1360
- /**
1361
- * Recursively collect all TPT descendants (children, grandchildren, etc.)
1362
- */
1363
- collectAllTPTDescendants(meta, metadata) {
1364
- const descendants = [];
1365
- const collect = (parent) => {
1366
- for (const child of parent.tptChildren ?? []) {
1367
- const resolved = metadata.find(m => m.class === child.class) ?? child;
1368
- if (!resolved.abstract) {
1369
- descendants.push(resolved);
1370
- }
1371
- collect(resolved);
1372
- }
1373
- };
1374
- collect(meta);
1375
- return descendants;
1376
- }
1377
- /**
1378
- * Computes ownProps for TPT entities - only properties defined in THIS entity,
1379
- * not inherited from parent. Also creates synthetic join properties for parent/child relationships.
1380
- *
1381
- * Called multiple times during discovery as metadata is progressively built.
1382
- * Each pass overwrites earlier results to reflect the final state of properties.
1383
- */
1384
- computeTPTOwnProps(meta) {
1385
- if (meta.inheritanceType !== 'tpt') {
1386
- return;
1387
- }
1388
- const belongsToTable = (prop) => prop.persist !== false || prop.primary;
1389
- // Use meta.properties (object) since meta.props (array) may not be populated yet
1390
- const allProps = Object.values(meta.properties);
1391
- if (!meta.tptParent) {
1392
- meta.ownProps = allProps.filter(belongsToTable);
1393
- return;
1394
- }
1395
- const parentPropNames = new Set(Object.values(meta.tptParent.properties).map(p => p.name));
1396
- meta.ownProps = allProps.filter(prop => !parentPropNames.has(prop.name) && belongsToTable(prop));
1397
- // Create synthetic join properties for the parent-child relationship
1398
- const childFieldNames = meta.getPrimaryProps().flatMap(p => p.fieldNames);
1399
- const parentFieldNames = meta.tptParent.getPrimaryProps().flatMap(p => p.fieldNames);
1400
- meta.tptParentProp = {
1401
- name: '[tpt:parent]',
1402
- kind: ReferenceKind.MANY_TO_ONE,
1403
- targetMeta: meta.tptParent,
1404
- fieldNames: childFieldNames,
1405
- referencedColumnNames: parentFieldNames,
1406
- persist: false,
1407
- };
1408
- meta.tptInverseProp = {
1409
- name: '[tpt:child]',
1410
- kind: ReferenceKind.ONE_TO_ONE,
1411
- owner: true,
1412
- targetMeta: meta,
1413
- fieldNames: parentFieldNames,
1414
- joinColumns: parentFieldNames,
1415
- referencedColumnNames: childFieldNames,
1416
- persist: false,
1417
- };
1418
- }
1419
- /** Returns the depth of a TPT entity in its hierarchy (0 for root). */
1420
- getTPTDepth(meta) {
1421
- let depth = 0;
1422
- let current = meta;
1423
- while (current.tptParent) {
1424
- depth++;
1425
- current = current.tptParent;
1426
- }
1427
- return depth;
1428
- }
1429
- /**
1430
- * Find the direct TPT parent entity for the given entity.
1431
- */
1432
- getTPTParent(meta, metadata) {
1433
- if (meta.root === meta) {
1434
- return undefined;
1435
- }
1436
- return metadata.find(m => {
1437
- const ext = meta.extends;
1438
- if (ext instanceof EntitySchema) {
1439
- return m.class === ext.meta.class || m.className === ext.meta.className;
1440
- }
1441
- return m.class === ext || m.className === Utils.className(ext);
1075
+ };
1076
+ const processExtensions = meta => {
1077
+ const parent = this.#discovered.find(m => {
1078
+ return meta.extends && Utils.className(meta.extends) === m.className;
1442
1079
  });
1443
- }
1444
- createDiscriminatorProperty(meta) {
1445
- meta.addProperty({
1446
- name: meta.discriminatorColumn,
1447
- type: 'string',
1448
- enum: true,
1449
- kind: ReferenceKind.SCALAR,
1450
- userDefined: false,
1451
- });
1452
- }
1453
- initAutoincrement(meta) {
1454
- const pks = meta.getPrimaryProps();
1455
- if (pks.length === 1 && this.#platform.isNumericProperty(pks[0])) {
1456
- /* v8 ignore next */
1457
- pks[0].autoincrement ??= true;
1458
- }
1459
- }
1460
- createSchemaTable(meta) {
1461
- const qualifiedName = meta.schema ? `${meta.schema}.${meta.tableName}` : meta.tableName;
1462
- return {
1463
- name: meta.tableName,
1464
- schema: meta.schema,
1465
- qualifiedName,
1466
- toString: () => qualifiedName,
1467
- };
1468
- }
1469
- initCheckConstraints(meta) {
1470
- const columns = meta.createSchemaColumnMappingObject();
1471
- const table = this.createSchemaTable(meta);
1472
- for (const check of meta.checks) {
1473
- const fieldNames = check.property ? meta.properties[check.property].fieldNames : [];
1474
- check.name ??= this.#namingStrategy.indexName(meta.tableName, fieldNames, 'check');
1475
- if (check.expression instanceof Function) {
1476
- check.expression = check.expression(columns, table);
1477
- }
1478
- }
1479
- if (this.#platform.usesEnumCheckConstraints() && !meta.embeddable) {
1480
- for (const prop of meta.props) {
1481
- if (prop.enum &&
1482
- prop.persist !== false &&
1483
- !prop.nativeEnumName &&
1484
- prop.items?.every(item => typeof item === 'string')) {
1485
- this.initFieldName(prop);
1486
- meta.checks.push({
1487
- name: this.#namingStrategy.indexName(meta.tableName, prop.fieldNames, 'check'),
1488
- property: prop.name,
1489
- expression: `${this.#platform.quoteIdentifier(prop.fieldNames[0])} in ('${prop.items.join("', '")}')`,
1490
- });
1491
- }
1492
- }
1493
- }
1494
- }
1495
- initGeneratedColumn(meta, prop) {
1496
- if (!prop.generated && prop.columnTypes) {
1497
- const match = /(.*) generated always as (.*)/i.exec(prop.columnTypes[0]);
1498
- if (match) {
1499
- prop.columnTypes[0] = match[1];
1500
- prop.generated = match[2];
1501
- return;
1502
- }
1503
- const match2 = /^as (.*)/i.exec(prop.columnTypes[0]?.trim());
1504
- if (match2) {
1505
- prop.generated = match2[1];
1506
- }
1507
- return;
1508
- }
1509
- if (prop.generated instanceof Function) {
1510
- const columns = meta.createSchemaColumnMappingObject();
1511
- prop.generated = prop.generated(columns, this.createSchemaTable(meta));
1512
- }
1513
- }
1514
- getDefaultVersionValue(meta, prop) {
1515
- if (typeof prop.defaultRaw !== 'undefined') {
1516
- return prop.defaultRaw;
1517
- }
1518
- /* v8 ignore next */
1519
- if (prop.default != null) {
1520
- return '' + this.#platform.convertVersionValue(prop.default, prop);
1521
- }
1522
- this.initCustomType(meta, prop, true);
1523
- const type = prop.customType?.runtimeType ?? prop.runtimeType ?? prop.type;
1524
- if (type === 'Date') {
1525
- prop.length ??= this.#platform.getDefaultVersionLength();
1526
- return this.#platform.getCurrentTimestampSQL(prop.length);
1527
- }
1528
- return '1';
1529
- }
1530
- inferDefaultValue(meta, prop) {
1531
- try {
1532
- // try to create two entity instances to detect the value is stable
1533
- const now = Date.now();
1534
- const entity1 = new meta.class();
1535
- const entity2 = new meta.class();
1536
- // we compare the two values by reference, this will discard things like `new Date()` or `Date.now()`
1537
- if (this.#config.get('discovery').inferDefaultValues &&
1538
- prop.default === undefined &&
1539
- entity1[prop.name] != null &&
1540
- entity1[prop.name] === entity2[prop.name] &&
1541
- entity1[prop.name] !== now) {
1542
- prop.default ??= entity1[prop.name];
1543
- }
1544
- // if the default value is null, infer nullability
1545
- if (entity1[prop.name] === null) {
1546
- prop.nullable ??= true;
1547
- }
1548
- // but still use object values for type inference if not explicitly set, e.g. `createdAt = new Date()`
1549
- if (prop.kind === ReferenceKind.SCALAR && prop.type == null && entity1[prop.name] != null) {
1550
- prop.type = prop.runtimeType = Utils.getObjectType(entity1[prop.name]);
1551
- }
1552
- }
1553
- catch {
1554
- // ignore
1555
- }
1556
- }
1557
- initDefaultValue(prop) {
1558
- if (prop.defaultRaw || !('default' in prop)) {
1559
- return;
1560
- }
1561
- let val = prop.default;
1562
- const raw = Raw.getKnownFragment(val);
1563
- if (raw) {
1564
- prop.defaultRaw = this.#platform.formatQuery(raw.sql, raw.params);
1565
- return;
1566
- }
1567
- if (Array.isArray(prop.default) && prop.customType) {
1568
- val = prop.customType.convertToDatabaseValue(prop.default, this.#platform);
1569
- }
1570
- prop.defaultRaw = typeof val === 'string' ? `'${val}'` : '' + val;
1571
- }
1572
- inferTypeFromDefault(prop) {
1573
- if ((prop.defaultRaw == null && prop.default == null) || prop.type !== 'any') {
1574
- return;
1575
- }
1576
- switch (typeof prop.default) {
1577
- case 'string':
1578
- prop.type = prop.runtimeType = 'string';
1579
- break;
1580
- case 'number':
1581
- prop.type = prop.runtimeType = 'number';
1582
- break;
1583
- case 'boolean':
1584
- prop.type = prop.runtimeType = 'boolean';
1585
- break;
1586
- }
1587
- if (prop.defaultRaw?.startsWith('current_timestamp')) {
1588
- prop.type = prop.runtimeType = 'Date';
1589
- }
1590
- }
1591
- initVersionProperty(meta, prop) {
1592
- if (prop.version) {
1593
- this.initDefaultValue(prop);
1594
- meta.versionProperty = prop.name;
1595
- prop.defaultRaw = this.getDefaultVersionValue(meta, prop);
1596
- }
1597
- if (prop.concurrencyCheck && !prop.primary) {
1598
- meta.concurrencyCheckKeys.add(prop.name);
1599
- }
1600
- }
1601
- initCustomType(meta, prop, simple = false, objectEmbeddable = false) {
1602
- // `prop.type` might be actually instance of custom type class
1603
- if (Type.isMappedType(prop.type) && !prop.customType) {
1604
- prop.customType = prop.type;
1605
- prop.type = prop.customType.constructor.name;
1606
- }
1607
- // `prop.type` might also be custom type class (not instance), so `typeof MyType` will give us `function`, not `object`
1608
- if (typeof prop.type === 'function' &&
1609
- Type.isMappedType(prop.type.prototype) &&
1610
- !prop.customType) {
1611
- // if the type is an ORM defined mapped type without `ensureComparable: true`,
1612
- // we use just the type name, to have more performant hydration code
1613
- const type = Utils.keys(t).find(type => {
1614
- return !Type.getType(t[type]).ensureComparable(meta, prop) && prop.type === t[type];
1615
- });
1616
- if (type) {
1617
- prop.type = type === 'datetime' ? 'Date' : type;
1618
- }
1619
- else {
1620
- prop.customType = new prop.type();
1621
- prop.type = prop.customType.constructor.name;
1622
- }
1623
- }
1624
- if (simple) {
1625
- return;
1626
- }
1627
- if (!prop.customType && ['json', 'jsonb'].includes(prop.type?.toLowerCase())) {
1628
- prop.customType = new t.json();
1629
- }
1630
- if (prop.kind === ReferenceKind.SCALAR &&
1631
- !prop.customType &&
1632
- prop.columnTypes &&
1633
- ['json', 'jsonb'].includes(prop.columnTypes[0])) {
1634
- prop.customType = new t.json();
1635
- }
1636
- if (prop.kind === ReferenceKind.EMBEDDED && !prop.customType && (prop.object || prop.array)) {
1637
- prop.customType = new t.json();
1638
- }
1639
- if (!prop.customType && prop.array && prop.items) {
1640
- prop.customType = new t.enumArray(`${meta.className}.${prop.name}`, prop.items);
1641
- }
1642
- const isArray = prop.type?.toLowerCase() === 'array' || prop.type?.toString().endsWith('[]');
1643
- if (objectEmbeddable && !prop.customType && isArray) {
1644
- prop.customType = new t.json();
1645
- }
1646
- // for number arrays we make sure to convert the items to numbers
1647
- if (!prop.customType && prop.type === 'number[]') {
1648
- prop.customType = new t.array(i => +i);
1649
- }
1650
- // `string[]` can be returned via ts-morph, while reflect metadata will give us just `array`
1651
- if (!prop.customType && isArray) {
1652
- prop.customType = new t.array();
1653
- }
1654
- if (!prop.customType && prop.type?.toLowerCase() === 'buffer') {
1655
- prop.customType = new t.blob();
1656
- }
1657
- if (!prop.customType && prop.type?.toLowerCase() === 'uint8array') {
1658
- prop.customType = new t.uint8array();
1659
- }
1660
- const mappedType = this.getMappedType(prop);
1661
- if (prop.fieldNames?.length === 1 && !prop.customType) {
1662
- [t.bigint, t.double, t.decimal, t.interval, t.date]
1663
- .filter(type => mappedType instanceof type)
1664
- .forEach((type) => (prop.customType = new type()));
1665
- }
1666
- if (prop.customType && !prop.columnTypes) {
1667
- const mappedType = this.getMappedType({
1668
- columnTypes: [prop.customType.getColumnType(prop, this.#platform)],
1669
- });
1670
- if (prop.customType.compareAsType() === 'any' && ![t.json].some(t => prop.customType instanceof t)) {
1671
- prop.runtimeType ??= mappedType.runtimeType;
1672
- }
1673
- else {
1674
- prop.runtimeType ??= prop.customType.runtimeType;
1675
- }
1676
- }
1677
- else if (prop.runtimeType === 'object') {
1678
- prop.runtimeType = mappedType.runtimeType;
1679
- }
1680
- else {
1681
- prop.runtimeType ??= mappedType.runtimeType;
1682
- }
1683
- if (prop.customType) {
1684
- prop.customType.platform = this.#platform;
1685
- prop.customType.meta = meta;
1686
- prop.customType.prop = prop;
1687
- prop.columnTypes ??= [prop.customType.getColumnType(prop, this.#platform)];
1688
- prop.hasConvertToJSValueSQL =
1689
- !!prop.customType.convertToJSValueSQL && prop.customType.convertToJSValueSQL('', this.#platform) !== '';
1690
- prop.hasConvertToDatabaseValueSQL =
1691
- !!prop.customType.convertToDatabaseValueSQL &&
1692
- prop.customType.convertToDatabaseValueSQL('', this.#platform) !== '';
1693
- if (prop.customType instanceof t.bigint &&
1694
- ['string', 'bigint', 'number'].includes(prop.runtimeType.toLowerCase())) {
1695
- prop.customType.mode = prop.runtimeType.toLowerCase();
1696
- }
1697
- }
1698
- if (Type.isMappedType(prop.customType) && prop.kind === ReferenceKind.SCALAR && !isArray) {
1699
- prop.type = prop.customType.name;
1700
- }
1701
- if (!prop.customType &&
1702
- [ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind) &&
1703
- !prop.polymorphic &&
1704
- prop.targetMeta.compositePK) {
1705
- prop.customTypes = [];
1706
- for (const pk of prop.targetMeta.getPrimaryProps()) {
1707
- if (pk.customType) {
1708
- prop.customTypes.push(pk.customType);
1709
- prop.hasConvertToJSValueSQL ||=
1710
- !!pk.customType.convertToJSValueSQL && pk.customType.convertToJSValueSQL('', this.#platform) !== '';
1711
- /* v8 ignore next */
1712
- prop.hasConvertToDatabaseValueSQL ||=
1713
- !!pk.customType.convertToDatabaseValueSQL &&
1714
- pk.customType.convertToDatabaseValueSQL('', this.#platform) !== '';
1715
- }
1716
- else {
1717
- prop.customTypes.push(undefined);
1718
- }
1719
- }
1720
- }
1721
- if (prop.kind === ReferenceKind.SCALAR && !(mappedType instanceof t.unknown)) {
1722
- if (!prop.columnTypes &&
1723
- prop.nativeEnumName &&
1724
- meta.schema !== this.#platform.getDefaultSchemaName() &&
1725
- meta.schema &&
1726
- !prop.nativeEnumName.includes('.')) {
1727
- prop.columnTypes = [`${meta.schema}.${prop.nativeEnumName}`];
1728
- }
1729
- else {
1730
- prop.columnTypes ??= [mappedType.getColumnType(prop, this.#platform)];
1731
- }
1732
- // use only custom types provided by user, we don't need to use the ones provided by ORM,
1733
- // with exception for ArrayType and JsonType, those two are handled in
1734
- if (!Object.values(t).some(type => type === mappedType.constructor)) {
1735
- prop.customType ??= mappedType;
1736
- }
1737
- }
1738
- }
1739
- initRelation(prop) {
1740
- if (prop.kind === ReferenceKind.SCALAR || prop.polymorphTargets) {
1741
- return;
1742
- }
1743
- // when the target is a polymorphic embedded entity, `prop.target` is an array of classes, we need to get the metadata by the type name instead
1744
- const meta2 = this.#metadata.find(prop.target) ?? this.#metadata.getByClassName(prop.type);
1745
- // If targetKey is specified, use that property instead of PKs for referencedPKs
1746
- prop.referencedPKs = prop.targetKey ? [prop.targetKey] : meta2.primaryKeys;
1747
- prop.targetMeta = meta2;
1748
- if (meta2.view) {
1749
- prop.createForeignKeyConstraint = false;
1750
- }
1751
- // Auto-generate formula for persist: false relations, but only for single-column FKs
1752
- // Composite FK relations need standard JOIN conditions, not formula-based
1753
- if (!prop.formula &&
1754
- prop.persist === false &&
1755
- [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) &&
1756
- !prop.embedded) {
1757
- this.initFieldName(prop);
1758
- if (prop.fieldNames?.length === 1) {
1759
- prop.formula = table => `${table}.${this.#platform.quoteIdentifier(prop.fieldNames[0])}`;
1760
- }
1761
- }
1762
- }
1763
- initColumnType(prop) {
1764
- this.initUnsigned(prop);
1765
- // Get the target properties for FK relations - use targetKey property if specified, otherwise PKs
1766
- const targetProps = prop.targetMeta
1767
- ? prop.targetKey
1768
- ? [prop.targetMeta.properties[prop.targetKey]]
1769
- : prop.targetMeta.getPrimaryProps()
1770
- : [];
1771
- targetProps.map(targetProp => {
1772
- prop.length ??= targetProp.length;
1773
- prop.precision ??= targetProp.precision;
1774
- prop.scale ??= targetProp.scale;
1775
- });
1776
- if (prop.kind === ReferenceKind.SCALAR && (prop.type == null || prop.type === 'object') && prop.columnTypes?.[0]) {
1777
- delete prop.type;
1778
- const mappedType = this.getMappedType(prop);
1779
- prop.type = mappedType.compareAsType();
1780
- }
1781
- if (prop.columnTypes || !this.#schemaHelper) {
1782
- return;
1783
- }
1784
- if (prop.kind === ReferenceKind.SCALAR) {
1785
- const mappedType = this.getMappedType(prop);
1786
- const SCALAR_TYPES = ['string', 'number', 'boolean', 'bigint', 'Date', 'Buffer', 'RegExp', 'any', 'unknown'];
1787
- if (mappedType instanceof t.unknown &&
1788
- // it could be a runtime type from reflect-metadata
1789
- !SCALAR_TYPES.includes(prop.type) &&
1790
- // or it might be inferred via ts-morph to some generic type alias
1791
- !/[<>:"';{}]/.exec(prop.type)) {
1792
- const type = prop.length != null && !prop.type.endsWith(`(${prop.length})`) ? `${prop.type}(${prop.length})` : prop.type;
1793
- prop.columnTypes = [type];
1794
- }
1795
- else {
1796
- prop.columnTypes = [mappedType.getColumnType(prop, this.#platform)];
1797
- }
1798
- return;
1799
- }
1800
- /* v8 ignore next */
1801
- if (prop.kind === ReferenceKind.EMBEDDED && prop.object) {
1802
- prop.columnTypes = [this.#platform.getJsonDeclarationSQL()];
1803
- return;
1804
- }
1805
- const targetMeta = prop.targetMeta;
1806
- prop.columnTypes = [];
1807
- // Use targetKey property if specified, otherwise use primary key properties
1808
- const referencedProps = prop.targetKey ? [targetMeta.properties[prop.targetKey]] : targetMeta.getPrimaryProps();
1809
- if (prop.polymorphic && prop.polymorphTargets) {
1810
- prop.columnTypes.push(this.#platform.getVarcharTypeDeclarationSQL(prop));
1811
- }
1812
- for (const referencedProp of referencedProps) {
1813
- this.initCustomType(targetMeta, referencedProp);
1814
- this.initColumnType(referencedProp);
1815
- const mappedType = this.getMappedType(referencedProp);
1816
- let columnTypes = referencedProp.columnTypes;
1817
- if (referencedProp.autoincrement) {
1818
- columnTypes = [mappedType.getColumnType({ ...referencedProp, autoincrement: false }, this.#platform)];
1819
- }
1820
- prop.columnTypes.push(...columnTypes);
1821
- if (!targetMeta.compositePK || prop.targetKey) {
1822
- prop.customType = referencedProp.customType;
1823
- }
1824
- }
1825
- }
1826
- getMappedType(prop) {
1827
- if (prop.customType) {
1828
- return prop.customType;
1829
- }
1830
- /* v8 ignore next */
1831
- let t = prop.columnTypes?.[0] ?? prop.type ?? '';
1832
- if (prop.nativeEnumName) {
1833
- t = 'enum';
1834
- }
1835
- else if (prop.enum) {
1836
- t = prop.items?.every(item => typeof item === 'string') ? 'enum' : 'tinyint';
1837
- }
1838
- if (t === 'Date') {
1839
- t = 'datetime';
1840
- }
1841
- return this.#platform.getMappedType(t);
1842
- }
1843
- getPrefix(prop, parent) {
1844
- const { embeddedPath = [], fieldNames, prefix = true, prefixMode } = prop;
1845
- if (prefix === true) {
1846
- return (embeddedPath.length ? embeddedPath.join('_') : fieldNames[0]) + '_';
1847
- }
1848
- const prefixParent = parent ? this.getPrefix(parent, null) : '';
1849
- if (prefix === false) {
1850
- return prefixParent;
1851
- }
1852
- const mode = prefixMode ?? this.#config.get('embeddables').prefixMode;
1853
- return mode === 'absolute' ? prefix : prefixParent + prefix;
1854
- }
1855
- initUnsigned(prop) {
1856
- if (prop.unsigned != null) {
1857
- return;
1858
- }
1859
- if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
1860
- prop.unsigned = prop.targetMeta.getPrimaryProps().some(pk => {
1861
- this.initUnsigned(pk);
1862
- return pk.unsigned;
1863
- });
1864
- return;
1865
- }
1866
- prop.unsigned ??=
1867
- (prop.primary || prop.unsigned) && this.#platform.isNumericProperty(prop) && this.#platform.supportsUnsigned();
1868
- }
1869
- initIndexes(meta, prop) {
1870
- const hasIndex = meta.indexes.some(idx => idx.properties?.length === 1 && idx.properties[0] === prop.name);
1871
- if (prop.kind === ReferenceKind.MANY_TO_ONE && this.#platform.indexForeignKeys() && !hasIndex) {
1872
- prop.index ??= true;
1873
- }
1874
- }
1875
- shouldForceConstructorUsage(meta) {
1876
- const forceConstructor = this.#config.get('forceEntityConstructor');
1877
- if (Array.isArray(forceConstructor)) {
1878
- return forceConstructor.some(cls => Utils.className(cls) === meta.className);
1879
- }
1880
- return forceConstructor;
1881
- }
1080
+ if (!parent) {
1081
+ return;
1082
+ }
1083
+ discriminatorColumn ??= parent.discriminatorColumn;
1084
+ inlineProperties(parent);
1085
+ processExtensions(parent);
1086
+ };
1087
+ polymorphs.forEach(meta => {
1088
+ inlineProperties(meta);
1089
+ processExtensions(meta);
1090
+ });
1091
+ const name = polymorphs
1092
+ .map(t => t.className)
1093
+ .sort()
1094
+ .join(' | ');
1095
+ embeddable = new EntityMetadata({
1096
+ name,
1097
+ className: name,
1098
+ embeddable: true,
1099
+ abstract: true,
1100
+ properties,
1101
+ polymorphs,
1102
+ discriminatorColumn,
1103
+ });
1104
+ embeddable.sync();
1105
+ discovered.push(embeddable);
1106
+ polymorphs.forEach(meta => (meta.root = embeddable));
1107
+ }
1108
+ }
1109
+ initPolymorphicRelation(meta, prop, discovered) {
1110
+ if (!prop.discriminator && !prop.discriminatorColumn && !prop.discriminatorMap && !Array.isArray(prop.target)) {
1111
+ return;
1112
+ }
1113
+ prop.polymorphic = true;
1114
+ prop.discriminator ??= prop.name;
1115
+ prop.discriminatorColumn ??= this.#namingStrategy.discriminatorColumnName(prop.discriminator);
1116
+ prop.createForeignKeyConstraint = false;
1117
+ const isToOne = [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind);
1118
+ if (isToOne) {
1119
+ const types = prop.type.split(/ ?\| ?/);
1120
+ prop.polymorphTargets = discovered.filter(m => types.includes(m.className) && !m.embeddable);
1121
+ prop.targetMeta = prop.polymorphTargets[0];
1122
+ prop.referencedPKs = prop.targetMeta?.primaryKeys;
1123
+ }
1124
+ if (prop.discriminatorMap) {
1125
+ const normalizedMap = {};
1126
+ for (const [key, value] of Object.entries(prop.discriminatorMap)) {
1127
+ const targetMeta = this.#metadata.getByClassName(value, false);
1128
+ if (!targetMeta) {
1129
+ throw MetadataError.fromUnknownEntity(value, `${meta.className}.${prop.name} discriminatorMap`);
1130
+ }
1131
+ normalizedMap[key] = targetMeta.class;
1132
+ if (targetMeta.class === meta.class) {
1133
+ prop.discriminatorValue = key;
1134
+ }
1135
+ }
1136
+ prop.discriminatorMap = normalizedMap;
1137
+ } else if (isToOne) {
1138
+ prop.discriminatorMap = {};
1139
+ const tableNameToTarget = new Map();
1140
+ for (const target of prop.polymorphTargets) {
1141
+ const existing = tableNameToTarget.get(target.tableName);
1142
+ if (existing) {
1143
+ throw MetadataError.incompatiblePolymorphicTargets(
1144
+ meta,
1145
+ prop,
1146
+ existing,
1147
+ target,
1148
+ `both use table '${target.tableName}'. Use separate properties instead of a single polymorphic relation.`,
1149
+ );
1150
+ }
1151
+ tableNameToTarget.set(target.tableName, target);
1152
+ prop.discriminatorMap[target.tableName] = target.class;
1153
+ }
1154
+ } else {
1155
+ prop.discriminatorValue ??= meta.tableName;
1156
+ if (!prop.discriminatorMap) {
1157
+ prop.discriminatorMap = { [prop.discriminatorValue]: meta.class };
1158
+ }
1159
+ }
1160
+ }
1161
+ initEmbeddables(meta, embeddedProp, visited = new Set()) {
1162
+ if (embeddedProp.kind !== ReferenceKind.EMBEDDED || visited.has(embeddedProp)) {
1163
+ return;
1164
+ }
1165
+ visited.add(embeddedProp);
1166
+ const embeddable = this.#discovered.find(m => m.name === embeddedProp.type);
1167
+ if (!embeddable) {
1168
+ throw MetadataError.fromUnknownEntity(embeddedProp.type, `${meta.className}.${embeddedProp.name}`);
1169
+ }
1170
+ embeddedProp.embeddable = embeddable.class;
1171
+ embeddedProp.embeddedProps = {};
1172
+ let order = meta.propertyOrder.get(embeddedProp.name);
1173
+ const getRootProperty = prop => (prop.embedded ? getRootProperty(meta.properties[prop.embedded[0]]) : prop);
1174
+ const isParentObject = prop => {
1175
+ if (prop.object || prop.array) {
1176
+ return true;
1177
+ }
1178
+ return prop.embedded ? isParentObject(meta.properties[prop.embedded[0]]) : false;
1179
+ };
1180
+ const isParentArray = prop => {
1181
+ if (prop.array) {
1182
+ return true;
1183
+ }
1184
+ return prop.embedded ? isParentArray(meta.properties[prop.embedded[0]]) : false;
1185
+ };
1186
+ const rootProperty = getRootProperty(embeddedProp);
1187
+ const parentProperty = meta.properties[embeddedProp.embedded?.[0] ?? ''];
1188
+ const object = isParentObject(embeddedProp);
1189
+ const array = isParentArray(embeddedProp);
1190
+ this.initFieldName(embeddedProp, rootProperty !== embeddedProp && object);
1191
+ // the prefix of the parent cannot be a boolean; it already passed here
1192
+ const prefix = this.getPrefix(embeddedProp, parentProperty);
1193
+ const glue = object ? '~' : '_';
1194
+ for (const prop of Object.values(embeddable.properties)) {
1195
+ const name = (embeddedProp.embeddedPath?.join(glue) ?? embeddedProp.fieldNames[0] + glue) + prop.name;
1196
+ meta.properties[name] = Utils.copy(prop);
1197
+ meta.properties[name].name = name;
1198
+ meta.properties[name].embedded = [embeddedProp.name, prop.name];
1199
+ meta.propertyOrder.set(name, (order += 0.01));
1200
+ embeddedProp.embeddedProps[prop.name] = meta.properties[name];
1201
+ meta.properties[name].persist ??= embeddedProp.persist;
1202
+ const refInArray =
1203
+ array && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && prop.owner;
1204
+ if (embeddedProp.nullable || refInArray) {
1205
+ meta.properties[name].nullable = true;
1206
+ }
1207
+ if (meta.properties[name].fieldNames) {
1208
+ meta.properties[name].fieldNames[0] = prefix + meta.properties[name].fieldNames[0];
1209
+ } else {
1210
+ const name2 = meta.properties[name].name;
1211
+ meta.properties[name].name = prefix + prop.name;
1212
+ this.initFieldName(meta.properties[name]);
1213
+ meta.properties[name].name = name2;
1214
+ }
1215
+ if (object) {
1216
+ embeddedProp.object = true;
1217
+ let path = [];
1218
+ let tmp = embeddedProp;
1219
+ while (tmp.embedded && tmp.object) {
1220
+ path.unshift(tmp.embedded[1]);
1221
+ tmp = meta.properties[tmp.embedded[0]];
1222
+ }
1223
+ if (tmp === rootProperty) {
1224
+ path.unshift(rootProperty.fieldNames[0]);
1225
+ } else if (embeddedProp.embeddedPath) {
1226
+ path = [...embeddedProp.embeddedPath];
1227
+ } else {
1228
+ path = [embeddedProp.fieldNames[0]];
1229
+ }
1230
+ this.initFieldName(prop, true);
1231
+ this.initRelation(prop);
1232
+ path.push(prop.fieldNames[0]);
1233
+ meta.properties[name].fieldNames = prop.fieldNames;
1234
+ meta.properties[name].embeddedPath = path;
1235
+ const targetProp = prop.targetMeta?.getPrimaryProp() ?? prop;
1236
+ const fieldName = raw(
1237
+ this.#platform.getSearchJsonPropertySQL(path.join('->'), targetProp.runtimeType ?? targetProp.type, true),
1238
+ );
1239
+ meta.properties[name].fieldNameRaw = fieldName.sql; // for querying in SQL drivers
1240
+ meta.properties[name].persist = false; // only virtual as we store the whole object
1241
+ meta.properties[name].userDefined = false; // mark this as a generated/internal property, so we can distinguish from user-defined non-persist properties
1242
+ meta.properties[name].object = true;
1243
+ this.initCustomType(meta, meta.properties[name], false, true);
1244
+ }
1245
+ this.initEmbeddables(meta, meta.properties[name], visited);
1246
+ }
1247
+ for (const index of embeddable.indexes) {
1248
+ meta.indexes.push({
1249
+ ...index,
1250
+ properties: Utils.asArray(index.properties).map(p => {
1251
+ return embeddedProp.embeddedProps[p].name;
1252
+ }),
1253
+ });
1254
+ }
1255
+ for (const unique of embeddable.uniques) {
1256
+ meta.uniques.push({
1257
+ ...unique,
1258
+ properties: Utils.asArray(unique.properties).map(p => {
1259
+ return embeddedProp.embeddedProps[p].name;
1260
+ }),
1261
+ });
1262
+ }
1263
+ }
1264
+ initSingleTableInheritance(meta, metadata) {
1265
+ if (meta.root !== meta && !meta.__processed) {
1266
+ meta.root = metadata.find(m => m.class === meta.root.class);
1267
+ meta.root.__processed = true;
1268
+ } else {
1269
+ delete meta.root.__processed;
1270
+ }
1271
+ if (!meta.root.discriminatorColumn) {
1272
+ return;
1273
+ }
1274
+ meta.root.inheritanceType = 'sti';
1275
+ if (meta.root.discriminatorMap) {
1276
+ const map = meta.root.discriminatorMap;
1277
+ Object.keys(map)
1278
+ .filter(key => typeof map[key] === 'string')
1279
+ .forEach(key => (map[key] = this.#metadata.getByClassName(map[key]).class));
1280
+ } else {
1281
+ meta.root.discriminatorMap = {};
1282
+ const children = metadata
1283
+ .filter(m => m.root.class === meta.root.class && !m.abstract)
1284
+ .sort((a, b) => a.className.localeCompare(b.className));
1285
+ for (const m of children) {
1286
+ const name = m.discriminatorValue ?? this.#namingStrategy.classToTableName(m.className);
1287
+ meta.root.discriminatorMap[name] = m.class;
1288
+ }
1289
+ }
1290
+ meta.discriminatorValue = QueryHelper.findDiscriminatorValue(meta.root.discriminatorMap, meta.class);
1291
+ if (!meta.root.properties[meta.root.discriminatorColumn]) {
1292
+ this.createDiscriminatorProperty(meta.root);
1293
+ }
1294
+ Utils.defaultValue(
1295
+ meta.root.properties[meta.root.discriminatorColumn],
1296
+ 'items',
1297
+ Object.keys(meta.root.discriminatorMap),
1298
+ );
1299
+ Utils.defaultValue(meta.root.properties[meta.root.discriminatorColumn], 'index', true);
1300
+ if (meta.root === meta) {
1301
+ return;
1302
+ }
1303
+ Object.values(meta.properties).forEach(prop => {
1304
+ const newProp = { ...prop };
1305
+ const rootProp = meta.root.properties[prop.name];
1306
+ // stiMerged is set during inlineProperties when a property was merged
1307
+ // from multiple polymorphic variants with different types. The flag is
1308
+ // cleared implicitly when the first child claims the root property via
1309
+ // addProperty below, so subsequent children correctly trigger renaming.
1310
+ const typesMatch = rootProp?.type === prop.type || rootProp?.stiMerged === true;
1311
+ if (
1312
+ rootProp &&
1313
+ (!typesMatch ||
1314
+ (rootProp.fieldNames && prop.fieldNames && !compareArrays(rootProp.fieldNames, prop.fieldNames)))
1315
+ ) {
1316
+ const name = newProp.name;
1317
+ this.initFieldName(newProp, newProp.object);
1318
+ newProp.renamedFrom = name;
1319
+ newProp.name = `${name}_${meta._id}`;
1320
+ meta.root.addProperty(newProp);
1321
+ this.initFieldName(prop, prop.object);
1322
+ // Track all field variants and map discriminator values to field names
1323
+ if (!rootProp.stiFieldNames) {
1324
+ this.initFieldName(rootProp, rootProp.object);
1325
+ rootProp.stiFieldNames = [...rootProp.fieldNames];
1326
+ rootProp.stiFieldNameMap = {};
1327
+ // Find which discriminator owns the original fieldNames
1328
+ for (const [discValue, childClass] of Object.entries(meta.root.discriminatorMap)) {
1329
+ const childMeta = this.#metadata.find(childClass);
1330
+ if (
1331
+ childMeta?.properties[prop.name]?.fieldNames &&
1332
+ compareArrays(childMeta.properties[prop.name].fieldNames, rootProp.fieldNames)
1333
+ ) {
1334
+ rootProp.stiFieldNameMap[discValue] = rootProp.fieldNames[0];
1335
+ break;
1336
+ }
1337
+ }
1338
+ }
1339
+ rootProp.stiFieldNameMap[meta.discriminatorValue] = prop.fieldNames[0];
1340
+ rootProp.stiFieldNames.push(...prop.fieldNames);
1341
+ newProp.nullable = true;
1342
+ newProp.name = name;
1343
+ newProp.hydrate = false;
1344
+ newProp.inherited = true;
1345
+ return;
1346
+ }
1347
+ if (prop.enum && prop.items && rootProp?.items) {
1348
+ newProp.items = Utils.unique([...rootProp.items, ...prop.items]);
1349
+ }
1350
+ newProp.nullable = true;
1351
+ newProp.inherited = !rootProp;
1352
+ meta.root.addProperty(newProp);
1353
+ });
1354
+ meta.tableName = meta.root.tableName;
1355
+ meta.root.indexes = Utils.unique([...meta.root.indexes, ...meta.indexes]);
1356
+ meta.root.uniques = Utils.unique([...meta.root.uniques, ...meta.uniques]);
1357
+ meta.root.checks = Utils.unique([...meta.root.checks, ...meta.checks]);
1358
+ }
1359
+ /**
1360
+ * First pass of TPT initialization: sets up hierarchy relationships
1361
+ * (inheritanceType, tptParent, tptChildren) before properties have fieldNames.
1362
+ */
1363
+ initTPTRelationships(meta, metadata) {
1364
+ if (meta.root !== meta) {
1365
+ meta.root = metadata.find(m => m.class === meta.root.class);
1366
+ }
1367
+ const inheritance = meta.inheritance;
1368
+ if (inheritance === 'tpt' && meta.root === meta) {
1369
+ meta.inheritanceType = 'tpt';
1370
+ meta.tptChildren = [];
1371
+ }
1372
+ if (meta.root.inheritanceType !== 'tpt') {
1373
+ return;
1374
+ }
1375
+ const parent = this.getTPTParent(meta, metadata);
1376
+ if (parent) {
1377
+ meta.tptParent = parent;
1378
+ meta.inheritanceType = 'tpt';
1379
+ parent.tptChildren ??= [];
1380
+ if (!parent.tptChildren.includes(meta)) {
1381
+ parent.tptChildren.push(meta);
1382
+ }
1383
+ }
1384
+ }
1385
+ /**
1386
+ * Second pass of TPT initialization: re-resolves metadata references after fieldNames
1387
+ * are set, syncs to registry metadata, and sets up discriminators.
1388
+ */
1389
+ finalizeTPTInheritance(meta, metadata) {
1390
+ if (meta.inheritanceType !== 'tpt') {
1391
+ return;
1392
+ }
1393
+ if (meta.tptParent) {
1394
+ meta.tptParent = metadata.find(m => m.class === meta.tptParent.class) ?? meta.tptParent;
1395
+ }
1396
+ if (meta.tptChildren) {
1397
+ meta.tptChildren = meta.tptChildren.map(child => metadata.find(m => m.class === child.class) ?? child);
1398
+ }
1399
+ const registryMeta = this.#metadata.get(meta.class);
1400
+ if (registryMeta && registryMeta !== meta) {
1401
+ registryMeta.inheritanceType = meta.inheritanceType;
1402
+ registryMeta.tptParent = meta.tptParent ? this.#metadata.get(meta.tptParent.class) : undefined;
1403
+ registryMeta.tptChildren = meta.tptChildren?.map(child => this.#metadata.get(child.class));
1404
+ }
1405
+ this.initTPTDiscriminator(meta, metadata);
1406
+ }
1407
+ /**
1408
+ * Initialize TPT discriminator map and virtual discriminator property.
1409
+ * Unlike STI where the discriminator is a persisted column, TPT discriminator is computed
1410
+ * at query time using CASE WHEN expressions based on which child table has data.
1411
+ */
1412
+ initTPTDiscriminator(meta, metadata) {
1413
+ const allDescendants = this.collectAllTPTDescendants(meta, metadata);
1414
+ allDescendants.sort((a, b) => this.getTPTDepth(b) - this.getTPTDepth(a));
1415
+ meta.allTPTDescendants = allDescendants;
1416
+ if (meta.root !== meta) {
1417
+ return;
1418
+ }
1419
+ meta.root.discriminatorMap = {};
1420
+ for (const m of allDescendants) {
1421
+ const name = this.#namingStrategy.classToTableName(m.className);
1422
+ meta.root.discriminatorMap[name] = m.class;
1423
+ m.discriminatorValue = name;
1424
+ }
1425
+ if (!meta.abstract) {
1426
+ const name = this.#namingStrategy.classToTableName(meta.className);
1427
+ meta.root.discriminatorMap[name] = meta.class;
1428
+ meta.discriminatorValue = name;
1429
+ }
1430
+ // Virtual discriminator property - computed at query time via CASE WHEN, not persisted
1431
+ const discriminatorColumn = '__tpt_type';
1432
+ if (!meta.root.properties[discriminatorColumn]) {
1433
+ meta.root.addProperty({
1434
+ name: discriminatorColumn,
1435
+ type: 'string',
1436
+ kind: ReferenceKind.SCALAR,
1437
+ persist: false,
1438
+ userDefined: false,
1439
+ hidden: true,
1440
+ });
1441
+ }
1442
+ meta.root.tptDiscriminatorColumn = discriminatorColumn;
1443
+ }
1444
+ /**
1445
+ * Recursively collect all TPT descendants (children, grandchildren, etc.)
1446
+ */
1447
+ collectAllTPTDescendants(meta, metadata) {
1448
+ const descendants = [];
1449
+ const collect = parent => {
1450
+ for (const child of parent.tptChildren ?? []) {
1451
+ const resolved = metadata.find(m => m.class === child.class) ?? child;
1452
+ if (!resolved.abstract) {
1453
+ descendants.push(resolved);
1454
+ }
1455
+ collect(resolved);
1456
+ }
1457
+ };
1458
+ collect(meta);
1459
+ return descendants;
1460
+ }
1461
+ /**
1462
+ * Computes ownProps for TPT entities - only properties defined in THIS entity,
1463
+ * not inherited from parent. Also creates synthetic join properties for parent/child relationships.
1464
+ *
1465
+ * Called multiple times during discovery as metadata is progressively built.
1466
+ * Each pass overwrites earlier results to reflect the final state of properties.
1467
+ */
1468
+ computeTPTOwnProps(meta) {
1469
+ if (meta.inheritanceType !== 'tpt') {
1470
+ return;
1471
+ }
1472
+ const belongsToTable = prop => prop.persist !== false || prop.primary;
1473
+ // Use meta.properties (object) since meta.props (array) may not be populated yet
1474
+ const allProps = Object.values(meta.properties);
1475
+ if (!meta.tptParent) {
1476
+ meta.ownProps = allProps.filter(belongsToTable);
1477
+ return;
1478
+ }
1479
+ const parentPropNames = new Set(Object.values(meta.tptParent.properties).map(p => p.name));
1480
+ meta.ownProps = allProps.filter(prop => !parentPropNames.has(prop.name) && belongsToTable(prop));
1481
+ // Create synthetic join properties for the parent-child relationship
1482
+ const childFieldNames = meta.getPrimaryProps().flatMap(p => p.fieldNames);
1483
+ const parentFieldNames = meta.tptParent.getPrimaryProps().flatMap(p => p.fieldNames);
1484
+ meta.tptParentProp = {
1485
+ name: '[tpt:parent]',
1486
+ kind: ReferenceKind.MANY_TO_ONE,
1487
+ targetMeta: meta.tptParent,
1488
+ fieldNames: childFieldNames,
1489
+ referencedColumnNames: parentFieldNames,
1490
+ persist: false,
1491
+ };
1492
+ meta.tptInverseProp = {
1493
+ name: '[tpt:child]',
1494
+ kind: ReferenceKind.ONE_TO_ONE,
1495
+ owner: true,
1496
+ targetMeta: meta,
1497
+ fieldNames: parentFieldNames,
1498
+ joinColumns: parentFieldNames,
1499
+ referencedColumnNames: childFieldNames,
1500
+ persist: false,
1501
+ };
1502
+ }
1503
+ /** Returns the depth of a TPT entity in its hierarchy (0 for root). */
1504
+ getTPTDepth(meta) {
1505
+ let depth = 0;
1506
+ let current = meta;
1507
+ while (current.tptParent) {
1508
+ depth++;
1509
+ current = current.tptParent;
1510
+ }
1511
+ return depth;
1512
+ }
1513
+ /**
1514
+ * Find the direct TPT parent entity for the given entity.
1515
+ */
1516
+ getTPTParent(meta, metadata) {
1517
+ if (meta.root === meta) {
1518
+ return undefined;
1519
+ }
1520
+ return metadata.find(m => {
1521
+ const ext = meta.extends;
1522
+ if (EntitySchema.is(ext)) {
1523
+ return m.class === ext.meta.class || m.className === ext.meta.className;
1524
+ }
1525
+ return m.class === ext || m.className === Utils.className(ext);
1526
+ });
1527
+ }
1528
+ createDiscriminatorProperty(meta) {
1529
+ meta.addProperty({
1530
+ name: meta.discriminatorColumn,
1531
+ type: 'string',
1532
+ enum: true,
1533
+ kind: ReferenceKind.SCALAR,
1534
+ userDefined: false,
1535
+ });
1536
+ }
1537
+ initAutoincrement(meta) {
1538
+ const pks = meta.getPrimaryProps();
1539
+ if (pks.length === 1 && this.#platform.isNumericProperty(pks[0])) {
1540
+ /* v8 ignore next */
1541
+ pks[0].autoincrement ??= true;
1542
+ }
1543
+ }
1544
+ createSchemaTable(meta) {
1545
+ const qualifiedName = meta.schema ? `${meta.schema}.${meta.tableName}` : meta.tableName;
1546
+ return {
1547
+ name: meta.tableName,
1548
+ schema: meta.schema,
1549
+ qualifiedName,
1550
+ toString: () => qualifiedName,
1551
+ };
1552
+ }
1553
+ initCheckConstraints(meta) {
1554
+ const columns = meta.createSchemaColumnMappingObject();
1555
+ const table = this.createSchemaTable(meta);
1556
+ for (const check of meta.checks) {
1557
+ const fieldNames = check.property ? meta.properties[check.property].fieldNames : [];
1558
+ check.name ??= this.#namingStrategy.indexName(meta.tableName, fieldNames, 'check');
1559
+ if (check.expression instanceof Function) {
1560
+ check.expression = check.expression(columns, table);
1561
+ }
1562
+ }
1563
+ if (this.#platform.usesEnumCheckConstraints() && !meta.embeddable) {
1564
+ for (const prop of meta.props) {
1565
+ if (
1566
+ prop.enum &&
1567
+ prop.persist !== false &&
1568
+ !prop.nativeEnumName &&
1569
+ prop.items?.every(item => typeof item === 'string')
1570
+ ) {
1571
+ this.initFieldName(prop);
1572
+ meta.checks.push({
1573
+ name: this.#namingStrategy.indexName(meta.tableName, prop.fieldNames, 'check'),
1574
+ property: prop.name,
1575
+ expression: `${this.#platform.quoteIdentifier(prop.fieldNames[0])} in ('${prop.items.join("', '")}')`,
1576
+ });
1577
+ }
1578
+ }
1579
+ }
1580
+ }
1581
+ initGeneratedColumn(meta, prop) {
1582
+ if (!prop.generated && prop.columnTypes) {
1583
+ const match = /(.*) generated always as (.*)/i.exec(prop.columnTypes[0]);
1584
+ if (match) {
1585
+ prop.columnTypes[0] = match[1];
1586
+ prop.generated = match[2];
1587
+ return;
1588
+ }
1589
+ const match2 = /^as (.*)/i.exec(prop.columnTypes[0]?.trim());
1590
+ if (match2) {
1591
+ prop.generated = match2[1];
1592
+ }
1593
+ return;
1594
+ }
1595
+ if (prop.generated instanceof Function) {
1596
+ const columns = meta.createSchemaColumnMappingObject();
1597
+ prop.generated = prop.generated(columns, this.createSchemaTable(meta));
1598
+ }
1599
+ }
1600
+ getDefaultVersionValue(meta, prop) {
1601
+ if (typeof prop.defaultRaw !== 'undefined') {
1602
+ return prop.defaultRaw;
1603
+ }
1604
+ /* v8 ignore next */
1605
+ if (prop.default != null) {
1606
+ return '' + this.#platform.convertVersionValue(prop.default, prop);
1607
+ }
1608
+ this.initCustomType(meta, prop, true);
1609
+ const type = prop.customType?.runtimeType ?? prop.runtimeType ?? prop.type;
1610
+ if (type === 'Date') {
1611
+ prop.length ??= this.#platform.getDefaultVersionLength();
1612
+ return this.#platform.getCurrentTimestampSQL(prop.length);
1613
+ }
1614
+ return '1';
1615
+ }
1616
+ inferDefaultValue(meta, prop) {
1617
+ try {
1618
+ // try to create two entity instances to detect the value is stable
1619
+ const now = Date.now();
1620
+ const entity1 = new meta.class();
1621
+ const entity2 = new meta.class();
1622
+ // we compare the two values by reference, this will discard things like `new Date()` or `Date.now()`
1623
+ if (
1624
+ this.#config.get('discovery').inferDefaultValues &&
1625
+ prop.default === undefined &&
1626
+ entity1[prop.name] != null &&
1627
+ entity1[prop.name] === entity2[prop.name] &&
1628
+ entity1[prop.name] !== now
1629
+ ) {
1630
+ prop.default ??= entity1[prop.name];
1631
+ }
1632
+ // if the default value is null, infer nullability
1633
+ if (entity1[prop.name] === null) {
1634
+ prop.nullable ??= true;
1635
+ }
1636
+ // but still use object values for type inference if not explicitly set, e.g. `createdAt = new Date()`
1637
+ if (prop.kind === ReferenceKind.SCALAR && prop.type == null && entity1[prop.name] != null) {
1638
+ prop.type = prop.runtimeType = Utils.getObjectType(entity1[prop.name]);
1639
+ }
1640
+ } catch {
1641
+ // ignore
1642
+ }
1643
+ }
1644
+ initDefaultValue(prop) {
1645
+ if (prop.defaultRaw || !('default' in prop)) {
1646
+ return;
1647
+ }
1648
+ let val = prop.default;
1649
+ const raw = Raw.getKnownFragment(val);
1650
+ if (raw) {
1651
+ prop.defaultRaw = this.#platform.formatQuery(raw.sql, raw.params);
1652
+ return;
1653
+ }
1654
+ if (Array.isArray(prop.default) && prop.customType) {
1655
+ val = prop.customType.convertToDatabaseValue(prop.default, this.#platform);
1656
+ }
1657
+ prop.defaultRaw = typeof val === 'string' ? `'${val}'` : '' + val;
1658
+ }
1659
+ inferTypeFromDefault(prop) {
1660
+ if ((prop.defaultRaw == null && prop.default == null) || prop.type !== 'any') {
1661
+ return;
1662
+ }
1663
+ switch (typeof prop.default) {
1664
+ case 'string':
1665
+ prop.type = prop.runtimeType = 'string';
1666
+ break;
1667
+ case 'number':
1668
+ prop.type = prop.runtimeType = 'number';
1669
+ break;
1670
+ case 'boolean':
1671
+ prop.type = prop.runtimeType = 'boolean';
1672
+ break;
1673
+ }
1674
+ if (prop.defaultRaw?.startsWith('current_timestamp')) {
1675
+ prop.type = prop.runtimeType = 'Date';
1676
+ }
1677
+ }
1678
+ initVersionProperty(meta, prop) {
1679
+ if (prop.version) {
1680
+ this.initDefaultValue(prop);
1681
+ meta.versionProperty = prop.name;
1682
+ prop.defaultRaw = this.getDefaultVersionValue(meta, prop);
1683
+ }
1684
+ if (prop.concurrencyCheck && !prop.primary) {
1685
+ meta.concurrencyCheckKeys.add(prop.name);
1686
+ }
1687
+ }
1688
+ initCustomType(meta, prop, simple = false, objectEmbeddable = false) {
1689
+ // `prop.type` might be actually instance of custom type class
1690
+ if (Type.isMappedType(prop.type) && !prop.customType) {
1691
+ prop.customType = prop.type;
1692
+ prop.type = prop.customType.constructor.name;
1693
+ }
1694
+ // `prop.type` might also be custom type class (not instance), so `typeof MyType` will give us `function`, not `object`
1695
+ if (typeof prop.type === 'function' && Type.isMappedType(prop.type.prototype) && !prop.customType) {
1696
+ // if the type is an ORM defined mapped type without `ensureComparable: true`,
1697
+ // we use just the type name, to have more performant hydration code
1698
+ const brand = Type.getOrmType(prop.type);
1699
+ const type = Utils.keys(t).find(type => {
1700
+ return !Type.getType(t[type]).ensureComparable(meta, prop) && (prop.type === t[type] || brand === type);
1701
+ });
1702
+ if (type) {
1703
+ prop.type = type === 'datetime' ? 'Date' : type;
1704
+ } else {
1705
+ prop.customType = new prop.type();
1706
+ prop.type = prop.customType.constructor.name;
1707
+ }
1708
+ }
1709
+ if (simple) {
1710
+ return;
1711
+ }
1712
+ if (!prop.customType && ['json', 'jsonb'].includes(prop.type?.toLowerCase())) {
1713
+ prop.customType = new t.json();
1714
+ }
1715
+ if (
1716
+ prop.kind === ReferenceKind.SCALAR &&
1717
+ !prop.customType &&
1718
+ prop.columnTypes &&
1719
+ ['json', 'jsonb'].includes(prop.columnTypes[0])
1720
+ ) {
1721
+ prop.customType = new t.json();
1722
+ }
1723
+ if (prop.kind === ReferenceKind.EMBEDDED && !prop.customType && (prop.object || prop.array)) {
1724
+ prop.customType = new t.json();
1725
+ }
1726
+ if (!prop.customType && prop.array && prop.items) {
1727
+ prop.customType = new t.enumArray(`${meta.className}.${prop.name}`, prop.items);
1728
+ }
1729
+ const isArray = prop.type?.toLowerCase() === 'array' || prop.type?.toString().endsWith('[]');
1730
+ if (objectEmbeddable && !prop.customType && isArray) {
1731
+ prop.customType = new t.json();
1732
+ }
1733
+ // for number arrays we make sure to convert the items to numbers
1734
+ if (!prop.customType && prop.type === 'number[]') {
1735
+ prop.customType = new t.array(i => +i);
1736
+ }
1737
+ // `string[]` can be returned via ts-morph, while reflect metadata will give us just `array`
1738
+ if (!prop.customType && isArray) {
1739
+ prop.customType = new t.array();
1740
+ }
1741
+ if (!prop.customType && prop.type?.toLowerCase() === 'buffer') {
1742
+ prop.customType = new t.blob();
1743
+ }
1744
+ if (!prop.customType && prop.type?.toLowerCase() === 'uint8array') {
1745
+ prop.customType = new t.uint8array();
1746
+ }
1747
+ const mappedType = this.getMappedType(prop);
1748
+ if (prop.fieldNames?.length === 1 && !prop.customType) {
1749
+ [t.bigint, t.double, t.decimal, t.interval, t.date]
1750
+ .filter(type => mappedType instanceof type)
1751
+ .forEach(type => (prop.customType = new type()));
1752
+ }
1753
+ if (prop.customType && !prop.columnTypes) {
1754
+ const mappedType = this.getMappedType({
1755
+ columnTypes: [prop.customType.getColumnType(prop, this.#platform)],
1756
+ });
1757
+ if (prop.customType.compareAsType() === 'any' && ![t.json].some(t => prop.customType instanceof t)) {
1758
+ prop.runtimeType ??= mappedType.runtimeType;
1759
+ } else {
1760
+ prop.runtimeType ??= prop.customType.runtimeType;
1761
+ }
1762
+ } else if (prop.runtimeType === 'object') {
1763
+ prop.runtimeType = mappedType.runtimeType;
1764
+ } else {
1765
+ prop.runtimeType ??= mappedType.runtimeType;
1766
+ }
1767
+ if (prop.customType) {
1768
+ prop.customType.platform = this.#platform;
1769
+ prop.customType.meta = meta;
1770
+ prop.customType.prop = prop;
1771
+ prop.columnTypes ??= [prop.customType.getColumnType(prop, this.#platform)];
1772
+ prop.hasConvertToJSValueSQL =
1773
+ !!prop.customType.convertToJSValueSQL && prop.customType.convertToJSValueSQL('', this.#platform) !== '';
1774
+ prop.hasConvertToDatabaseValueSQL =
1775
+ !!prop.customType.convertToDatabaseValueSQL &&
1776
+ prop.customType.convertToDatabaseValueSQL('', this.#platform) !== '';
1777
+ if (
1778
+ prop.customType instanceof t.bigint &&
1779
+ ['string', 'bigint', 'number'].includes(prop.runtimeType.toLowerCase())
1780
+ ) {
1781
+ prop.customType.mode = prop.runtimeType.toLowerCase();
1782
+ }
1783
+ }
1784
+ if (Type.isMappedType(prop.customType) && prop.kind === ReferenceKind.SCALAR && !isArray) {
1785
+ prop.type = prop.customType.name;
1786
+ }
1787
+ if (
1788
+ !prop.customType &&
1789
+ [ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind) &&
1790
+ !prop.polymorphic &&
1791
+ prop.targetMeta.compositePK
1792
+ ) {
1793
+ prop.customTypes = [];
1794
+ for (const pk of prop.targetMeta.getPrimaryProps()) {
1795
+ if (pk.customType) {
1796
+ prop.customTypes.push(pk.customType);
1797
+ prop.hasConvertToJSValueSQL ||=
1798
+ !!pk.customType.convertToJSValueSQL && pk.customType.convertToJSValueSQL('', this.#platform) !== '';
1799
+ /* v8 ignore next */
1800
+ prop.hasConvertToDatabaseValueSQL ||=
1801
+ !!pk.customType.convertToDatabaseValueSQL &&
1802
+ pk.customType.convertToDatabaseValueSQL('', this.#platform) !== '';
1803
+ } else {
1804
+ prop.customTypes.push(undefined);
1805
+ }
1806
+ }
1807
+ }
1808
+ if (prop.kind === ReferenceKind.SCALAR && !(mappedType instanceof t.unknown)) {
1809
+ if (
1810
+ !prop.columnTypes &&
1811
+ prop.nativeEnumName &&
1812
+ meta.schema !== this.#platform.getDefaultSchemaName() &&
1813
+ meta.schema &&
1814
+ !prop.nativeEnumName.includes('.')
1815
+ ) {
1816
+ prop.columnTypes = [`${meta.schema}.${prop.nativeEnumName}`];
1817
+ } else {
1818
+ prop.columnTypes ??= [mappedType.getColumnType(prop, this.#platform)];
1819
+ }
1820
+ // use only custom types provided by user, we don't need to use the ones provided by ORM,
1821
+ // with exception for ArrayType and JsonType, those two are handled in
1822
+ if (!Object.values(t).some(type => type === mappedType.constructor)) {
1823
+ prop.customType ??= mappedType;
1824
+ }
1825
+ }
1826
+ }
1827
+ initRelation(prop) {
1828
+ if (prop.kind === ReferenceKind.SCALAR || prop.polymorphTargets) {
1829
+ return;
1830
+ }
1831
+ // when the target is a polymorphic embedded entity, `prop.target` is an array of classes, we need to get the metadata by the type name instead
1832
+ const meta2 = this.#metadata.find(prop.target) ?? this.#metadata.getByClassName(prop.type);
1833
+ // If targetKey is specified, use that property instead of PKs for referencedPKs
1834
+ prop.referencedPKs = prop.targetKey ? [prop.targetKey] : meta2.primaryKeys;
1835
+ prop.targetMeta = meta2;
1836
+ if (meta2.view) {
1837
+ prop.createForeignKeyConstraint = false;
1838
+ }
1839
+ // Auto-generate formula for persist: false relations, but only for single-column FKs
1840
+ // Composite FK relations need standard JOIN conditions, not formula-based
1841
+ if (
1842
+ !prop.formula &&
1843
+ prop.persist === false &&
1844
+ [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) &&
1845
+ !prop.embedded
1846
+ ) {
1847
+ this.initFieldName(prop);
1848
+ if (prop.fieldNames?.length === 1) {
1849
+ prop.formula = table => `${table}.${this.#platform.quoteIdentifier(prop.fieldNames[0])}`;
1850
+ }
1851
+ }
1852
+ }
1853
+ initColumnType(prop) {
1854
+ this.initUnsigned(prop);
1855
+ // Get the target properties for FK relations - use targetKey property if specified, otherwise PKs
1856
+ const targetProps = prop.targetMeta
1857
+ ? prop.targetKey
1858
+ ? [prop.targetMeta.properties[prop.targetKey]]
1859
+ : prop.targetMeta.getPrimaryProps()
1860
+ : [];
1861
+ targetProps.map(targetProp => {
1862
+ prop.length ??= targetProp.length;
1863
+ prop.precision ??= targetProp.precision;
1864
+ prop.scale ??= targetProp.scale;
1865
+ });
1866
+ if (prop.kind === ReferenceKind.SCALAR && (prop.type == null || prop.type === 'object') && prop.columnTypes?.[0]) {
1867
+ delete prop.type;
1868
+ const mappedType = this.getMappedType(prop);
1869
+ prop.type = mappedType.compareAsType();
1870
+ }
1871
+ if (prop.columnTypes || !this.#schemaHelper) {
1872
+ return;
1873
+ }
1874
+ if (prop.kind === ReferenceKind.SCALAR) {
1875
+ const mappedType = this.getMappedType(prop);
1876
+ const SCALAR_TYPES = ['string', 'number', 'boolean', 'bigint', 'Date', 'Buffer', 'RegExp', 'any', 'unknown'];
1877
+ if (
1878
+ mappedType instanceof t.unknown &&
1879
+ // it could be a runtime type from reflect-metadata
1880
+ !SCALAR_TYPES.includes(prop.type) &&
1881
+ // or it might be inferred via ts-morph to some generic type alias
1882
+ !/[<>:"';{}]/.exec(prop.type)
1883
+ ) {
1884
+ const type =
1885
+ prop.length != null && !prop.type.endsWith(`(${prop.length})`) ? `${prop.type}(${prop.length})` : prop.type;
1886
+ prop.columnTypes = [type];
1887
+ } else {
1888
+ prop.columnTypes = [mappedType.getColumnType(prop, this.#platform)];
1889
+ }
1890
+ return;
1891
+ }
1892
+ /* v8 ignore next */
1893
+ if (prop.kind === ReferenceKind.EMBEDDED && prop.object) {
1894
+ prop.columnTypes = [this.#platform.getJsonDeclarationSQL()];
1895
+ return;
1896
+ }
1897
+ const targetMeta = prop.targetMeta;
1898
+ prop.columnTypes = [];
1899
+ // Use targetKey property if specified, otherwise use primary key properties
1900
+ const referencedProps = prop.targetKey ? [targetMeta.properties[prop.targetKey]] : targetMeta.getPrimaryProps();
1901
+ if (prop.polymorphic && prop.polymorphTargets) {
1902
+ prop.columnTypes.push(this.#platform.getVarcharTypeDeclarationSQL(prop));
1903
+ }
1904
+ for (const referencedProp of referencedProps) {
1905
+ this.initCustomType(targetMeta, referencedProp);
1906
+ this.initColumnType(referencedProp);
1907
+ const mappedType = this.getMappedType(referencedProp);
1908
+ let columnTypes = referencedProp.columnTypes;
1909
+ if (referencedProp.autoincrement) {
1910
+ columnTypes = [mappedType.getColumnType({ ...referencedProp, autoincrement: false }, this.#platform)];
1911
+ }
1912
+ prop.columnTypes.push(...columnTypes);
1913
+ if (!targetMeta.compositePK || prop.targetKey) {
1914
+ prop.customType = referencedProp.customType;
1915
+ }
1916
+ }
1917
+ }
1918
+ getMappedType(prop) {
1919
+ if (prop.customType) {
1920
+ return prop.customType;
1921
+ }
1922
+ /* v8 ignore next */
1923
+ let t = prop.columnTypes?.[0] ?? prop.type ?? '';
1924
+ if (prop.nativeEnumName) {
1925
+ t = 'enum';
1926
+ } else if (prop.enum) {
1927
+ t = prop.items?.every(item => typeof item === 'string') ? 'enum' : 'tinyint';
1928
+ }
1929
+ if (t === 'Date') {
1930
+ t = 'datetime';
1931
+ }
1932
+ return this.#platform.getMappedType(t);
1933
+ }
1934
+ getPrefix(prop, parent) {
1935
+ const { embeddedPath = [], fieldNames, prefix = true, prefixMode } = prop;
1936
+ if (prefix === true) {
1937
+ return (embeddedPath.length ? embeddedPath.join('_') : fieldNames[0]) + '_';
1938
+ }
1939
+ const prefixParent = parent ? this.getPrefix(parent, null) : '';
1940
+ if (prefix === false) {
1941
+ return prefixParent;
1942
+ }
1943
+ const mode = prefixMode ?? this.#config.get('embeddables').prefixMode;
1944
+ return mode === 'absolute' ? prefix : prefixParent + prefix;
1945
+ }
1946
+ initUnsigned(prop) {
1947
+ if (prop.unsigned != null) {
1948
+ return;
1949
+ }
1950
+ if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
1951
+ prop.unsigned = prop.targetMeta.getPrimaryProps().some(pk => {
1952
+ this.initUnsigned(pk);
1953
+ return pk.unsigned;
1954
+ });
1955
+ return;
1956
+ }
1957
+ prop.unsigned ??=
1958
+ (prop.primary || prop.unsigned) && this.#platform.isNumericProperty(prop) && this.#platform.supportsUnsigned();
1959
+ }
1960
+ initIndexes(meta, prop) {
1961
+ const hasIndex = meta.indexes.some(idx => idx.properties?.length === 1 && idx.properties[0] === prop.name);
1962
+ if (prop.kind === ReferenceKind.MANY_TO_ONE && this.#platform.indexForeignKeys() && !hasIndex) {
1963
+ prop.index ??= true;
1964
+ }
1965
+ }
1966
+ shouldForceConstructorUsage(meta) {
1967
+ const forceConstructor = this.#config.get('forceEntityConstructor');
1968
+ if (Array.isArray(forceConstructor)) {
1969
+ return forceConstructor.some(cls => Utils.className(cls) === meta.className);
1970
+ }
1971
+ return forceConstructor;
1972
+ }
1882
1973
  }