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

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