@mikro-orm/core 7.0.0-dev.30 → 7.0.0-dev.300

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 (209) hide show
  1. package/EntityManager.d.ts +68 -60
  2. package/EntityManager.js +290 -259
  3. package/MikroORM.d.ts +44 -35
  4. package/MikroORM.js +109 -142
  5. package/README.md +2 -0
  6. package/cache/FileCacheAdapter.d.ts +1 -2
  7. package/cache/FileCacheAdapter.js +18 -11
  8. package/cache/GeneratedCacheAdapter.d.ts +0 -1
  9. package/cache/GeneratedCacheAdapter.js +0 -2
  10. package/cache/index.d.ts +0 -1
  11. package/cache/index.js +0 -1
  12. package/connections/Connection.d.ts +12 -5
  13. package/connections/Connection.js +21 -12
  14. package/drivers/DatabaseDriver.d.ts +25 -16
  15. package/drivers/DatabaseDriver.js +119 -36
  16. package/drivers/IDatabaseDriver.d.ts +118 -23
  17. package/entity/BaseEntity.d.ts +63 -4
  18. package/entity/BaseEntity.js +0 -3
  19. package/entity/Collection.d.ts +101 -29
  20. package/entity/Collection.js +436 -104
  21. package/entity/EntityAssigner.js +17 -17
  22. package/entity/EntityFactory.d.ts +7 -1
  23. package/entity/EntityFactory.js +87 -55
  24. package/entity/EntityHelper.d.ts +2 -2
  25. package/entity/EntityHelper.js +57 -19
  26. package/entity/EntityLoader.d.ts +11 -10
  27. package/entity/EntityLoader.js +213 -82
  28. package/entity/EntityRepository.d.ts +28 -8
  29. package/entity/EntityRepository.js +8 -2
  30. package/entity/PolymorphicRef.d.ts +12 -0
  31. package/entity/PolymorphicRef.js +18 -0
  32. package/entity/Reference.d.ts +1 -5
  33. package/entity/Reference.js +15 -11
  34. package/entity/WrappedEntity.d.ts +3 -8
  35. package/entity/WrappedEntity.js +2 -7
  36. package/entity/defineEntity.d.ts +526 -310
  37. package/entity/defineEntity.js +134 -290
  38. package/entity/index.d.ts +2 -2
  39. package/entity/index.js +2 -2
  40. package/entity/utils.d.ts +6 -1
  41. package/entity/utils.js +34 -1
  42. package/entity/validators.d.ts +11 -0
  43. package/entity/validators.js +65 -0
  44. package/enums.d.ts +8 -6
  45. package/enums.js +2 -1
  46. package/errors.d.ts +20 -10
  47. package/errors.js +55 -23
  48. package/events/EventManager.d.ts +2 -1
  49. package/events/EventManager.js +19 -11
  50. package/hydration/Hydrator.js +1 -2
  51. package/hydration/ObjectHydrator.d.ts +4 -4
  52. package/hydration/ObjectHydrator.js +79 -34
  53. package/index.d.ts +2 -2
  54. package/index.js +1 -2
  55. package/logging/DefaultLogger.d.ts +1 -1
  56. package/logging/DefaultLogger.js +1 -0
  57. package/logging/SimpleLogger.d.ts +1 -1
  58. package/logging/colors.d.ts +1 -1
  59. package/logging/colors.js +7 -6
  60. package/logging/index.d.ts +1 -0
  61. package/logging/index.js +1 -0
  62. package/logging/inspect.d.ts +2 -0
  63. package/logging/inspect.js +11 -0
  64. package/metadata/EntitySchema.d.ts +47 -23
  65. package/metadata/EntitySchema.js +92 -33
  66. package/metadata/MetadataDiscovery.d.ts +64 -9
  67. package/metadata/MetadataDiscovery.js +782 -325
  68. package/metadata/MetadataProvider.d.ts +11 -2
  69. package/metadata/MetadataProvider.js +66 -2
  70. package/metadata/MetadataStorage.d.ts +13 -11
  71. package/metadata/MetadataStorage.js +72 -39
  72. package/metadata/MetadataValidator.d.ts +32 -9
  73. package/metadata/MetadataValidator.js +196 -41
  74. package/metadata/discover-entities.d.ts +5 -0
  75. package/metadata/discover-entities.js +40 -0
  76. package/metadata/index.d.ts +1 -1
  77. package/metadata/index.js +1 -1
  78. package/metadata/types.d.ts +577 -0
  79. package/metadata/types.js +1 -0
  80. package/naming-strategy/AbstractNamingStrategy.d.ts +16 -4
  81. package/naming-strategy/AbstractNamingStrategy.js +20 -2
  82. package/naming-strategy/EntityCaseNamingStrategy.d.ts +3 -3
  83. package/naming-strategy/EntityCaseNamingStrategy.js +6 -5
  84. package/naming-strategy/MongoNamingStrategy.d.ts +3 -3
  85. package/naming-strategy/MongoNamingStrategy.js +6 -6
  86. package/naming-strategy/NamingStrategy.d.ts +28 -4
  87. package/naming-strategy/UnderscoreNamingStrategy.d.ts +3 -3
  88. package/naming-strategy/UnderscoreNamingStrategy.js +6 -6
  89. package/not-supported.d.ts +2 -0
  90. package/not-supported.js +4 -0
  91. package/package.json +22 -11
  92. package/platforms/ExceptionConverter.js +1 -1
  93. package/platforms/Platform.d.ts +11 -15
  94. package/platforms/Platform.js +24 -44
  95. package/serialization/EntitySerializer.d.ts +6 -3
  96. package/serialization/EntitySerializer.js +46 -26
  97. package/serialization/EntityTransformer.js +33 -21
  98. package/serialization/SerializationContext.d.ts +6 -6
  99. package/serialization/SerializationContext.js +3 -3
  100. package/types/ArrayType.d.ts +1 -1
  101. package/types/ArrayType.js +2 -3
  102. package/types/BigIntType.js +1 -1
  103. package/types/BlobType.d.ts +0 -1
  104. package/types/BlobType.js +0 -3
  105. package/types/BooleanType.d.ts +1 -0
  106. package/types/BooleanType.js +3 -0
  107. package/types/DecimalType.js +2 -2
  108. package/types/DoubleType.js +1 -1
  109. package/types/EnumArrayType.js +1 -2
  110. package/types/JsonType.d.ts +1 -1
  111. package/types/JsonType.js +7 -2
  112. package/types/TinyIntType.js +1 -1
  113. package/types/Type.d.ts +2 -4
  114. package/types/Type.js +3 -3
  115. package/types/Uint8ArrayType.d.ts +0 -1
  116. package/types/Uint8ArrayType.js +1 -4
  117. package/types/index.d.ts +1 -1
  118. package/typings.d.ts +412 -155
  119. package/typings.js +99 -44
  120. package/unit-of-work/ChangeSet.d.ts +4 -6
  121. package/unit-of-work/ChangeSet.js +4 -5
  122. package/unit-of-work/ChangeSetComputer.d.ts +3 -8
  123. package/unit-of-work/ChangeSetComputer.js +41 -20
  124. package/unit-of-work/ChangeSetPersister.d.ts +13 -12
  125. package/unit-of-work/ChangeSetPersister.js +94 -36
  126. package/unit-of-work/CommitOrderCalculator.d.ts +12 -10
  127. package/unit-of-work/CommitOrderCalculator.js +13 -13
  128. package/unit-of-work/IdentityMap.d.ts +12 -0
  129. package/unit-of-work/IdentityMap.js +39 -1
  130. package/unit-of-work/UnitOfWork.d.ts +27 -3
  131. package/unit-of-work/UnitOfWork.js +248 -90
  132. package/utils/AbstractMigrator.d.ts +101 -0
  133. package/utils/AbstractMigrator.js +305 -0
  134. package/utils/AbstractSchemaGenerator.d.ts +5 -5
  135. package/utils/AbstractSchemaGenerator.js +28 -17
  136. package/utils/AsyncContext.d.ts +6 -0
  137. package/utils/AsyncContext.js +42 -0
  138. package/utils/Configuration.d.ts +795 -211
  139. package/utils/Configuration.js +153 -194
  140. package/utils/ConfigurationLoader.d.ts +1 -52
  141. package/utils/ConfigurationLoader.js +1 -330
  142. package/utils/Cursor.d.ts +0 -3
  143. package/utils/Cursor.js +24 -11
  144. package/utils/DataloaderUtils.d.ts +10 -5
  145. package/utils/DataloaderUtils.js +29 -12
  146. package/utils/EntityComparator.d.ts +16 -9
  147. package/utils/EntityComparator.js +154 -56
  148. package/utils/QueryHelper.d.ts +18 -6
  149. package/utils/QueryHelper.js +76 -23
  150. package/utils/RawQueryFragment.d.ts +28 -34
  151. package/utils/RawQueryFragment.js +35 -71
  152. package/utils/RequestContext.js +2 -2
  153. package/utils/TransactionContext.js +2 -2
  154. package/utils/TransactionManager.js +9 -6
  155. package/utils/Utils.d.ts +15 -126
  156. package/utils/Utils.js +80 -382
  157. package/utils/clone.js +8 -23
  158. package/utils/env-vars.d.ts +7 -0
  159. package/utils/env-vars.js +97 -0
  160. package/utils/fs-utils.d.ts +34 -0
  161. package/utils/fs-utils.js +196 -0
  162. package/utils/index.d.ts +1 -3
  163. package/utils/index.js +1 -3
  164. package/utils/upsert-utils.d.ts +9 -4
  165. package/utils/upsert-utils.js +46 -3
  166. package/decorators/Check.d.ts +0 -3
  167. package/decorators/Check.js +0 -13
  168. package/decorators/CreateRequestContext.d.ts +0 -3
  169. package/decorators/CreateRequestContext.js +0 -32
  170. package/decorators/Embeddable.d.ts +0 -8
  171. package/decorators/Embeddable.js +0 -11
  172. package/decorators/Embedded.d.ts +0 -12
  173. package/decorators/Embedded.js +0 -18
  174. package/decorators/Entity.d.ts +0 -33
  175. package/decorators/Entity.js +0 -12
  176. package/decorators/Enum.d.ts +0 -9
  177. package/decorators/Enum.js +0 -16
  178. package/decorators/Filter.d.ts +0 -2
  179. package/decorators/Filter.js +0 -8
  180. package/decorators/Formula.d.ts +0 -4
  181. package/decorators/Formula.js +0 -15
  182. package/decorators/Indexed.d.ts +0 -19
  183. package/decorators/Indexed.js +0 -20
  184. package/decorators/ManyToMany.d.ts +0 -42
  185. package/decorators/ManyToMany.js +0 -14
  186. package/decorators/ManyToOne.d.ts +0 -34
  187. package/decorators/ManyToOne.js +0 -14
  188. package/decorators/OneToMany.d.ts +0 -28
  189. package/decorators/OneToMany.js +0 -17
  190. package/decorators/OneToOne.d.ts +0 -28
  191. package/decorators/OneToOne.js +0 -7
  192. package/decorators/PrimaryKey.d.ts +0 -8
  193. package/decorators/PrimaryKey.js +0 -20
  194. package/decorators/Property.d.ts +0 -250
  195. package/decorators/Property.js +0 -32
  196. package/decorators/Transactional.d.ts +0 -14
  197. package/decorators/Transactional.js +0 -28
  198. package/decorators/hooks.d.ts +0 -16
  199. package/decorators/hooks.js +0 -47
  200. package/decorators/index.d.ts +0 -17
  201. package/decorators/index.js +0 -17
  202. package/entity/ArrayCollection.d.ts +0 -118
  203. package/entity/ArrayCollection.js +0 -407
  204. package/entity/EntityValidator.d.ts +0 -19
  205. package/entity/EntityValidator.js +0 -150
  206. package/metadata/ReflectMetadataProvider.d.ts +0 -8
  207. package/metadata/ReflectMetadataProvider.js +0 -44
  208. package/utils/resolveContextProvider.d.ts +0 -10
  209. package/utils/resolveContextProvider.js +0 -28
@@ -1,22 +1,22 @@
1
- import { basename, extname } from 'node:path';
2
- import { glob } from 'tinyglobby';
3
1
  import { EntityMetadata, } from '../typings.js';
4
- import { Utils } from '../utils/Utils.js';
2
+ import { compareArrays, Utils } from '../utils/Utils.js';
3
+ import { QueryHelper } from '../utils/QueryHelper.js';
5
4
  import { MetadataValidator } from './MetadataValidator.js';
5
+ import { MetadataProvider } from './MetadataProvider.js';
6
6
  import { MetadataStorage } from './MetadataStorage.js';
7
7
  import { EntitySchema } from './EntitySchema.js';
8
8
  import { Cascade, ReferenceKind } from '../enums.js';
9
9
  import { MetadataError } from '../errors.js';
10
- import { ArrayType, BigIntType, BlobType, DateType, DecimalType, DoubleType, EnumArrayType, IntervalType, JsonType, t, Type, Uint8ArrayType, UnknownType, } from '../types/index.js';
10
+ import { t, Type } from '../types/index.js';
11
11
  import { colors } from '../logging/colors.js';
12
- import { raw, RawQueryFragment } from '../utils/RawQueryFragment.js';
12
+ import { raw, Raw } from '../utils/RawQueryFragment.js';
13
+ import { BaseEntity } from '../entity/BaseEntity.js';
13
14
  export class MetadataDiscovery {
14
15
  metadata;
15
16
  platform;
16
17
  config;
17
18
  namingStrategy;
18
19
  metadataProvider;
19
- cache;
20
20
  logger;
21
21
  schemaHelper;
22
22
  validator = new MetadataValidator();
@@ -27,13 +27,14 @@ export class MetadataDiscovery {
27
27
  this.config = config;
28
28
  this.namingStrategy = this.config.getNamingStrategy();
29
29
  this.metadataProvider = this.config.getMetadataProvider();
30
- this.cache = this.config.getMetadataCacheAdapter();
31
30
  this.logger = this.config.getLogger();
32
31
  this.schemaHelper = this.platform.getSchemaHelper();
33
32
  }
34
33
  async discover(preferTs = true) {
34
+ this.discovered.length = 0;
35
35
  const startTime = Date.now();
36
- this.logger.log('discovery', `ORM entity discovery started, using ${colors.cyan(this.metadataProvider.constructor.name)}`);
36
+ const suffix = this.metadataProvider.constructor === MetadataProvider ? '' : `, using ${colors.cyan(this.metadataProvider.constructor.name)}`;
37
+ this.logger.log('discovery', `ORM entity discovery started${suffix}`);
37
38
  await this.findEntities(preferTs);
38
39
  for (const meta of this.discovered) {
39
40
  /* v8 ignore next */
@@ -47,10 +48,13 @@ export class MetadataDiscovery {
47
48
  await this.config.get('discovery').afterDiscovered?.(storage, this.platform);
48
49
  return storage;
49
50
  }
50
- discoverSync(preferTs = true) {
51
+ discoverSync() {
52
+ this.discovered.length = 0;
51
53
  const startTime = Date.now();
52
- this.logger.log('discovery', `ORM entity discovery started, using ${colors.cyan(this.metadataProvider.constructor.name)} in sync mode`);
53
- this.findEntities(preferTs, true);
54
+ const suffix = this.metadataProvider.constructor === MetadataProvider ? '' : `, using ${colors.cyan(this.metadataProvider.constructor.name)}`;
55
+ this.logger.log('discovery', `ORM entity discovery started${suffix} in sync mode`);
56
+ const refs = this.config.get('entities');
57
+ this.discoverReferences(refs);
54
58
  for (const meta of this.discovered) {
55
59
  /* v8 ignore next */
56
60
  void this.config.get('discovery').onMetadata?.(meta, this.platform);
@@ -70,82 +74,124 @@ export class MetadataDiscovery {
70
74
  .sort((a, b) => b.root.name.localeCompare(a.root.name))
71
75
  .forEach(meta => {
72
76
  this.platform.validateMetadata(meta);
73
- discovered.set(meta.className, meta);
77
+ discovered.set(meta.class, meta);
74
78
  });
79
+ for (const meta of discovered) {
80
+ meta.root = discovered.get(meta.root.class);
81
+ if (meta.inheritanceType === 'tpt') {
82
+ this.computeTPTOwnProps(meta);
83
+ }
84
+ }
75
85
  return discovered;
76
86
  }
87
+ initAccessors(meta) {
88
+ for (const prop of Object.values(meta.properties)) {
89
+ if (!prop.accessor || meta.properties[prop.accessor]) {
90
+ continue;
91
+ }
92
+ const desc = Object.getOwnPropertyDescriptor(meta.prototype, prop.name);
93
+ if (desc?.get || desc?.set) {
94
+ this.initRelation(prop);
95
+ this.initFieldName(prop);
96
+ const accessor = prop.name;
97
+ prop.name = typeof prop.accessor === 'string' ? prop.accessor : prop.name;
98
+ if (prop.accessor === true) {
99
+ prop.getter = prop.setter = true;
100
+ }
101
+ else {
102
+ prop.getter = prop.setter = false;
103
+ }
104
+ prop.accessor = accessor;
105
+ prop.serializedName ??= accessor;
106
+ Utils.renameKey(meta.properties, accessor, prop.name);
107
+ }
108
+ else {
109
+ const name = prop.name;
110
+ if (prop.kind === ReferenceKind.SCALAR || prop.kind === ReferenceKind.EMBEDDED) {
111
+ prop.name = prop.accessor;
112
+ }
113
+ this.initRelation(prop);
114
+ this.initFieldName(prop);
115
+ prop.serializedName ??= prop.accessor;
116
+ prop.name = name;
117
+ }
118
+ }
119
+ }
77
120
  processDiscoveredEntities(discovered) {
78
121
  for (const meta of discovered) {
79
122
  let i = 1;
80
123
  Object.values(meta.properties).forEach(prop => meta.propertyOrder.set(prop.name, i++));
81
124
  Object.values(meta.properties).forEach(prop => this.initPolyEmbeddables(prop, discovered));
125
+ this.initAccessors(meta);
82
126
  }
83
127
  // ignore base entities (not annotated with @Entity)
84
128
  const filtered = discovered.filter(meta => meta.root.name);
85
129
  // sort so we discover entities first to get around issues with nested embeddables
86
130
  filtered.sort((a, b) => !a.embeddable === !b.embeddable ? 0 : (a.embeddable ? 1 : -1));
87
131
  filtered.forEach(meta => this.initSingleTableInheritance(meta, filtered));
132
+ filtered.forEach(meta => this.initTPTRelationships(meta, filtered));
88
133
  filtered.forEach(meta => this.defineBaseEntityProperties(meta));
89
- filtered.forEach(meta => this.metadata.set(meta.className, EntitySchema.fromMetadata(meta).init().meta));
134
+ filtered.forEach(meta => {
135
+ const newMeta = EntitySchema.fromMetadata(meta).init().meta;
136
+ return this.metadata.set(newMeta.class, newMeta);
137
+ });
90
138
  filtered.forEach(meta => this.initAutoincrement(meta));
91
- filtered.forEach(meta => Object.values(meta.properties).forEach(prop => this.initEmbeddables(meta, prop)));
92
- filtered.forEach(meta => Object.values(meta.properties).forEach(prop => this.initFactoryField(meta, prop)));
93
- filtered.forEach(meta => Object.values(meta.properties).forEach(prop => this.initFieldName(prop)));
94
- filtered.forEach(meta => Object.values(meta.properties).forEach(prop => this.initVersionProperty(meta, prop)));
95
- filtered.forEach(meta => Object.values(meta.properties).forEach(prop => this.initCustomType(meta, prop)));
96
- filtered.forEach(meta => Object.values(meta.properties).forEach(prop => this.initGeneratedColumn(meta, prop)));
139
+ const forEachProp = (cb) => {
140
+ filtered.forEach(meta => Object.values(meta.properties).forEach(prop => cb(meta, prop)));
141
+ };
142
+ forEachProp((m, p) => this.initFactoryField(m, p));
143
+ forEachProp((m, p) => this.initPolymorphicRelation(m, p, filtered));
144
+ forEachProp((_m, p) => this.initRelation(p));
145
+ forEachProp((m, p) => this.initEmbeddables(m, p));
146
+ forEachProp((_m, p) => this.initFieldName(p));
147
+ filtered.forEach(meta => this.finalizeTPTInheritance(meta, filtered));
148
+ filtered.forEach(meta => this.computeTPTOwnProps(meta));
149
+ forEachProp((m, p) => this.initVersionProperty(m, p));
150
+ forEachProp((m, p) => this.initCustomType(m, p));
151
+ forEachProp((m, p) => this.initGeneratedColumn(m, p));
97
152
  filtered.forEach(meta => this.initAutoincrement(meta)); // once again after we init custom types
98
153
  filtered.forEach(meta => this.initCheckConstraints(meta));
99
- for (const meta of filtered) {
100
- for (const prop of Object.values(meta.properties)) {
101
- this.initDefaultValue(prop);
102
- this.inferTypeFromDefault(prop);
103
- this.initColumnType(prop);
104
- // change tracking on scalars is used only for "auto" flushMode
105
- if (this.config.get('flushMode') !== 'auto' && [ReferenceKind.SCALAR, ReferenceKind.EMBEDDED].includes(prop.kind)) {
106
- prop.trackChanges = false;
107
- }
108
- }
109
- }
110
- filtered.forEach(meta => Object.values(meta.properties).forEach(prop => this.initIndexes(meta, prop)));
154
+ forEachProp((_m, p) => {
155
+ this.initDefaultValue(p);
156
+ this.inferTypeFromDefault(p);
157
+ this.initRelation(p);
158
+ this.initColumnType(p);
159
+ });
160
+ forEachProp((m, p) => this.initIndexes(m, p));
111
161
  filtered.forEach(meta => this.autoWireBidirectionalProperties(meta));
112
- filtered.forEach(meta => this.findReferencingProperties(meta, filtered));
113
162
  for (const meta of filtered) {
114
163
  discovered.push(...this.processEntity(meta));
115
164
  }
116
165
  discovered.forEach(meta => meta.sync(true));
117
- const combinedCachePath = this.cache.combine?.();
118
- // override the path in the options, so we can log it from the CLI in `cache:generate` command
119
- if (combinedCachePath) {
120
- this.config.get('metadataCache').combined = combinedCachePath;
121
- }
166
+ this.metadataProvider.combineCache();
122
167
  return discovered.map(meta => {
123
- meta = this.metadata.get(meta.className);
168
+ meta = this.metadata.get(meta.class);
124
169
  meta.sync(true);
170
+ this.findReferencingProperties(meta, filtered);
171
+ if (meta.inheritanceType === 'tpt') {
172
+ this.computeTPTOwnProps(meta);
173
+ }
125
174
  return meta;
126
175
  });
127
176
  }
128
- findEntities(preferTs, sync = false) {
129
- this.discovered.length = 0;
130
- const options = this.config.get('discovery');
131
- const key = (preferTs && this.config.get('preferTs', Utils.detectTypeScriptSupport()) && this.config.get('entitiesTs').length > 0) ? 'entitiesTs' : 'entities';
132
- const paths = this.config.get(key).filter(item => Utils.isString(item));
133
- const refs = this.config.get(key).filter(item => !Utils.isString(item));
177
+ async findEntities(preferTs) {
178
+ const { entities, entitiesTs, baseDir } = this.config.getAll();
179
+ const targets = (preferTs && entitiesTs.length > 0) ? entitiesTs : entities;
180
+ const processed = [];
181
+ const paths = [];
182
+ for (const entity of targets) {
183
+ if (typeof entity === 'string') {
184
+ paths.push(entity);
185
+ }
186
+ else {
187
+ processed.push(entity);
188
+ }
189
+ }
134
190
  if (paths.length > 0) {
135
- if (sync || options.requireEntitiesArray) {
136
- throw new Error(`[requireEntitiesArray] Explicit list of entities is required, please use the 'entities' option.`);
137
- }
138
- return this.discoverDirectories(paths).then(() => {
139
- this.discoverReferences(refs);
140
- this.discoverMissingTargets();
141
- this.validator.validateDiscovered(this.discovered, options);
142
- return this.discovered;
143
- });
191
+ const { discoverEntities } = await import('@mikro-orm/core/file-discovery');
192
+ processed.push(...await discoverEntities(paths, { baseDir }));
144
193
  }
145
- this.discoverReferences(refs);
146
- this.discoverMissingTargets();
147
- this.validator.validateDiscovered(this.discovered, options);
148
- return this.discovered;
194
+ return this.discoverReferences(processed);
149
195
  }
150
196
  discoverMissingTargets() {
151
197
  const unwrap = (type) => type
@@ -154,17 +200,22 @@ export class MetadataDiscovery {
154
200
  .replace(/\((.*)\)/, '$1'); // unwrap union types
155
201
  const missing = [];
156
202
  this.discovered.forEach(meta => Object.values(meta.properties).forEach(prop => {
157
- if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.pivotEntity && !this.discovered.find(m => m.className === Utils.className(prop.pivotEntity))) {
158
- const target = typeof prop.pivotEntity === 'function'
159
- ? prop.pivotEntity()
160
- : prop.pivotEntity;
161
- missing.push(target);
162
- }
163
- if (prop.kind !== ReferenceKind.SCALAR && !unwrap(prop.type).split(/ ?\| ?/).every(type => this.discovered.find(m => m.className === type))) {
164
- const target = typeof prop.entity === 'function'
203
+ if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.pivotEntity) {
204
+ const pivotEntity = prop.pivotEntity;
205
+ const target = typeof pivotEntity === 'function' && !pivotEntity.prototype
206
+ ? pivotEntity()
207
+ : pivotEntity;
208
+ if (!this.discovered.find(m => m.className === Utils.className(target))) {
209
+ missing.push(target);
210
+ }
211
+ }
212
+ if (prop.kind !== ReferenceKind.SCALAR) {
213
+ const target = typeof prop.entity === 'function' && !prop.entity.prototype
165
214
  ? prop.entity()
166
215
  : prop.type;
167
- missing.push(...Utils.asArray(target));
216
+ if (!unwrap(prop.type).split(/ ?\| ?/).every(type => this.discovered.find(m => m.className === type))) {
217
+ missing.push(...Utils.asArray(target));
218
+ }
168
219
  }
169
220
  }));
170
221
  if (missing.length > 0) {
@@ -173,123 +224,107 @@ export class MetadataDiscovery {
173
224
  }
174
225
  tryDiscoverTargets(targets) {
175
226
  for (const target of targets) {
176
- if (typeof target === 'function' && target.name && !this.metadata.has(target.name)) {
177
- this.discoverReferences([target]);
178
- this.discoverMissingTargets();
179
- }
180
- }
181
- }
182
- async discoverDirectories(paths) {
183
- paths = paths.map(path => Utils.normalizePath(path));
184
- const files = await glob(paths, { cwd: Utils.normalizePath(this.config.get('baseDir')) });
185
- this.logger.log('discovery', `- processing ${colors.cyan('' + files.length)} files`);
186
- const found = [];
187
- for (const filepath of files) {
188
- const filename = basename(filepath);
189
- if (!filename.match(/\.[cm]?[jt]s$/) ||
190
- filename.endsWith('.js.map') ||
191
- filename.match(/\.d\.[cm]?ts/) ||
192
- filename.startsWith('.') ||
193
- filename.match(/index\.[cm]?[jt]s$/)) {
194
- this.logger.log('discovery', `- ignoring file ${filename}`);
195
- continue;
196
- }
197
- const name = this.namingStrategy.getClassName(filename);
198
- const path = Utils.normalizePath(this.config.get('baseDir'), filepath);
199
- const targets = await this.getEntityClassOrSchema(path, name);
200
- for (const target of targets) {
201
- if (!(target instanceof Function) && !(target instanceof EntitySchema)) {
202
- this.logger.log('discovery', `- ignoring file ${filename}`);
203
- continue;
227
+ const schema = target instanceof EntitySchema ? target : undefined;
228
+ const isDiscoverable = typeof target === 'function' || schema;
229
+ if (isDiscoverable && target.name) {
230
+ // Get the actual class for EntitySchema, or use target directly for classes
231
+ const targetClass = schema ? schema.meta.class : target;
232
+ if (!this.metadata.has(targetClass)) {
233
+ this.discoverReferences([target], false);
234
+ this.discoverMissingTargets();
204
235
  }
205
- const entity = this.prepare(target);
206
- const schema = this.getSchema(entity, path);
207
- const meta = schema.init().meta;
208
- this.metadata.set(meta.className, meta);
209
- found.push([schema, path]);
210
236
  }
211
237
  }
212
- for (const [schema, path] of found) {
213
- this.discoverEntity(schema, path);
214
- }
215
238
  }
216
- discoverReferences(refs) {
239
+ discoverReferences(refs, validate = true) {
217
240
  const found = [];
218
241
  for (const entity of refs) {
219
- const schema = this.getSchema(this.prepare(entity));
242
+ if (typeof entity === 'string') {
243
+ throw new Error('Folder based discovery requires the async `MikroORM.init()` method.');
244
+ }
245
+ const schema = this.getSchema(entity);
220
246
  const meta = schema.init().meta;
221
- this.metadata.set(meta.className, meta);
247
+ this.metadata.set(meta.class, meta);
222
248
  found.push(schema);
223
249
  }
224
250
  // discover parents (base entities) automatically
225
251
  for (const meta of this.metadata) {
226
252
  let parent = meta.extends;
227
- if (parent instanceof EntitySchema && !this.metadata.has(parent.meta.className)) {
228
- this.discoverReferences([parent]);
253
+ if (parent instanceof EntitySchema && !this.metadata.has(parent.init().meta.class)) {
254
+ this.discoverReferences([parent], false);
255
+ }
256
+ if (typeof parent === 'function' && parent.name && !this.metadata.has(parent)) {
257
+ this.discoverReferences([parent], false);
229
258
  }
230
- /* v8 ignore next 3 */
259
+ /* v8 ignore next */
231
260
  if (!meta.class) {
232
261
  continue;
233
262
  }
234
263
  parent = Object.getPrototypeOf(meta.class);
235
- if (parent.name !== '' && !this.metadata.has(parent.name)) {
236
- this.discoverReferences([parent]);
264
+ // Skip if parent is the auto-generated base class for the same entity (from setClass usage)
265
+ if (parent.name !== '' && parent.name !== meta.className && !this.metadata.has(parent) && parent !== BaseEntity) {
266
+ this.discoverReferences([parent], false);
237
267
  }
238
268
  }
239
269
  for (const schema of found) {
240
270
  this.discoverEntity(schema);
241
271
  }
272
+ this.discoverMissingTargets();
273
+ if (validate) {
274
+ this.validator.validateDiscovered(this.discovered, this.config.get('discovery'));
275
+ }
242
276
  return this.discovered.filter(meta => found.find(m => m.name === meta.className));
243
277
  }
244
- reset(className) {
245
- const exists = this.discovered.findIndex(m => m.className === className);
278
+ reset(entityName) {
279
+ const exists = this.discovered.findIndex(m => m.class === entityName || m.className === Utils.className(entityName));
246
280
  if (exists !== -1) {
247
- this.metadata.reset(this.discovered[exists].className);
281
+ this.metadata.reset(this.discovered[exists].class);
248
282
  this.discovered.splice(exists, 1);
249
283
  }
250
284
  }
251
- prepare(entity) {
252
- /* v8 ignore next 3 */
253
- if ('schema' in entity && entity.schema instanceof EntitySchema) {
254
- return entity.schema;
255
- }
285
+ getSchema(entity) {
256
286
  if (EntitySchema.REGISTRY.has(entity)) {
257
- return EntitySchema.REGISTRY.get(entity);
287
+ entity = EntitySchema.REGISTRY.get(entity);
258
288
  }
259
- return entity;
260
- }
261
- getSchema(entity, filepath) {
262
289
  if (entity instanceof EntitySchema) {
263
- if (filepath) {
264
- // initialize global metadata for given entity
265
- MetadataStorage.getMetadata(entity.meta.className, filepath);
266
- }
267
290
  const meta = Utils.copy(entity.meta, false);
268
291
  return EntitySchema.fromMetadata(meta);
269
292
  }
270
293
  const path = entity[MetadataStorage.PATH_SYMBOL];
271
294
  if (path) {
272
295
  const meta = Utils.copy(MetadataStorage.getMetadata(entity.name, path), false);
273
- meta.path = Utils.relativePath(path, this.config.get('baseDir'));
274
- this.metadata.set(entity.name, meta);
296
+ meta.path = path;
297
+ this.metadata.set(entity, meta);
275
298
  }
276
- const exists = this.metadata.has(entity.name);
277
- const meta = this.metadata.get(entity.name, true);
299
+ const exists = this.metadata.has(entity);
300
+ const meta = this.metadata.get(entity, true);
278
301
  meta.abstract ??= !(exists && meta.name);
279
302
  const schema = EntitySchema.fromMetadata(meta);
280
303
  schema.setClass(entity);
281
304
  return schema;
282
305
  }
283
- discoverEntity(schema, path) {
284
- this.logger.log('discovery', `- processing entity ${colors.cyan(schema.meta.className)}${colors.grey(path ? ` (${path})` : '')}`);
306
+ getRootEntity(meta) {
307
+ const base = meta.extends && this.metadata.find(meta.extends);
308
+ if (!base || base === meta) { // make sure we do not fall into infinite loop
309
+ return meta;
310
+ }
311
+ const root = this.getRootEntity(base);
312
+ // For STI or TPT, use the root entity.
313
+ // Check both `inheritanceType` (set during discovery) and raw `inheritance` option (set before discovery).
314
+ if (root.discriminatorColumn || root.inheritanceType || root.inheritance === 'tpt') {
315
+ return root;
316
+ }
317
+ return meta;
318
+ }
319
+ discoverEntity(schema) {
285
320
  const meta = schema.meta;
286
- const root = Utils.getRootEntity(this.metadata, meta);
287
- schema.meta.path = Utils.relativePath(path || meta.path, this.config.get('baseDir'));
288
- const cache = this.metadataProvider.useCache() && meta.path && this.cache.get(meta.className + extname(meta.path));
321
+ const path = meta.path;
322
+ this.logger.log('discovery', `- processing entity ${colors.cyan(meta.className)}${colors.grey(path ? ` (${path})` : '')}`);
323
+ const root = this.getRootEntity(meta);
324
+ schema.meta.path = meta.path;
325
+ const cache = this.metadataProvider.getCachedMetadata(meta, root);
289
326
  if (cache) {
290
327
  this.logger.log('discovery', `- using cached metadata for entity ${colors.cyan(meta.className)}`);
291
- this.metadataProvider.loadFromCache(meta, cache);
292
- meta.root = root;
293
328
  this.discovered.push(meta);
294
329
  return;
295
330
  }
@@ -298,50 +333,18 @@ export class MetadataDiscovery {
298
333
  this.inferDefaultValue(meta, prop);
299
334
  }
300
335
  // if the definition is using EntitySchema we still want it to go through the metadata provider to validate no types are missing
301
- this.metadataProvider.loadEntityMetadata(meta, meta.className);
302
- if (!meta.collection && meta.name) {
336
+ this.metadataProvider.loadEntityMetadata(meta);
337
+ if (!meta.tableName && meta.name) {
303
338
  const entityName = root.discriminatorColumn ? root.name : meta.name;
304
- meta.collection = this.namingStrategy.classToTableName(entityName);
339
+ meta.tableName = this.namingStrategy.classToTableName(entityName);
305
340
  }
306
- delete meta.root; // to allow caching (as root can contain cycles)
307
- this.saveToCache(meta);
341
+ this.metadataProvider.saveToCache(meta);
308
342
  meta.root = root;
309
343
  this.discovered.push(meta);
310
344
  }
311
- saveToCache(meta) {
312
- if (!this.metadataProvider.useCache()) {
313
- return;
314
- }
315
- const copy = Utils.copy(meta, false);
316
- for (const prop of copy.props) {
317
- if (Type.isMappedType(prop.type)) {
318
- Reflect.deleteProperty(prop, 'type');
319
- Reflect.deleteProperty(prop, 'customType');
320
- }
321
- if (prop.default) {
322
- const raw = RawQueryFragment.getKnownFragment(prop.default);
323
- if (raw) {
324
- prop.defaultRaw ??= this.platform.formatQuery(raw.sql, raw.params);
325
- Reflect.deleteProperty(prop, 'default');
326
- }
327
- }
328
- Reflect.deleteProperty(prop, 'targetMeta');
329
- }
330
- [
331
- 'prototype', 'props', 'referencingProperties', 'propertyOrder', 'relations',
332
- 'concurrencyCheckKeys', 'checks',
333
- ].forEach(key => delete copy[key]);
334
- // base entity without properties might not have path, but nothing to cache there
335
- if (meta.path) {
336
- this.cache.set(meta.className + extname(meta.path), copy, meta.path);
337
- }
338
- }
339
345
  initNullability(prop) {
340
- if (prop.kind === ReferenceKind.MANY_TO_ONE) {
341
- return Utils.defaultValue(prop, 'nullable', prop.optional || prop.cascade.includes(Cascade.REMOVE) || prop.cascade.includes(Cascade.ALL));
342
- }
343
346
  if (prop.kind === ReferenceKind.ONE_TO_ONE) {
344
- return Utils.defaultValue(prop, 'nullable', prop.optional || !prop.owner || prop.cascade.includes(Cascade.REMOVE) || prop.cascade.includes(Cascade.ALL));
347
+ return Utils.defaultValue(prop, 'nullable', prop.optional || !prop.owner);
345
348
  }
346
349
  return Utils.defaultValue(prop, 'nullable', prop.optional);
347
350
  }
@@ -365,6 +368,12 @@ export class MetadataDiscovery {
365
368
  if (!prop.joinColumns || !prop.columnTypes || prop.ownColumns || ![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
366
369
  continue;
367
370
  }
371
+ // For polymorphic relations, ownColumns should include all fieldNames
372
+ // (discriminator + ID columns) since they are all owned by this relation
373
+ if (prop.polymorphic) {
374
+ prop.ownColumns = prop.fieldNames;
375
+ continue;
376
+ }
368
377
  if (prop.joinColumns.length > 1) {
369
378
  prop.ownColumns = prop.joinColumns.filter(col => {
370
379
  return !meta.props.find(p => p.name !== prop.name && (!p.fieldNames || p.fieldNames.includes(col)));
@@ -376,7 +385,7 @@ export class MetadataDiscovery {
376
385
  if (prop.joinColumns.length !== prop.columnTypes.length) {
377
386
  prop.columnTypes = prop.joinColumns.flatMap(field => {
378
387
  const matched = meta.props.find(p => p.fieldNames?.includes(field));
379
- /* v8 ignore next 3 */
388
+ /* v8 ignore next */
380
389
  if (!matched) {
381
390
  throw MetadataError.fromWrongForeignKey(meta, prop, 'columnTypes');
382
391
  }
@@ -395,30 +404,30 @@ export class MetadataDiscovery {
395
404
  if (prop.kind === ReferenceKind.SCALAR || prop.kind === ReferenceKind.EMBEDDED) {
396
405
  prop.fieldNames = [this.namingStrategy.propertyToColumnName(prop.name, object)];
397
406
  }
398
- else if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
407
+ else if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && !prop.polymorphic) {
399
408
  prop.fieldNames = this.initManyToOneFieldName(prop, prop.name);
400
409
  }
401
410
  else if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.owner) {
402
411
  prop.fieldNames = this.initManyToManyFieldName(prop, prop.name);
403
412
  }
404
413
  }
405
- initManyToOneFieldName(prop, name) {
406
- const meta2 = this.metadata.get(prop.type);
414
+ initManyToOneFieldName(prop, name, tableName) {
415
+ const meta2 = prop.targetMeta;
407
416
  const ret = [];
408
417
  for (const primaryKey of meta2.primaryKeys) {
409
418
  this.initFieldName(meta2.properties[primaryKey]);
410
419
  for (const fieldName of meta2.properties[primaryKey].fieldNames) {
411
- ret.push(this.namingStrategy.joinKeyColumnName(name, fieldName, meta2.compositePK));
420
+ ret.push(this.namingStrategy.joinKeyColumnName(name, fieldName, meta2.compositePK, tableName));
412
421
  }
413
422
  }
414
423
  return ret;
415
424
  }
416
425
  initManyToManyFieldName(prop, name) {
417
- const meta2 = this.metadata.get(prop.type);
426
+ const meta2 = prop.targetMeta;
418
427
  return meta2.primaryKeys.map(() => this.namingStrategy.propertyToColumnName(name));
419
428
  }
420
429
  initManyToManyFields(meta, prop) {
421
- const meta2 = this.metadata.get(prop.type);
430
+ const meta2 = prop.targetMeta;
422
431
  Utils.defaultValue(prop, 'fixedOrder', !!prop.fixedOrderColumn);
423
432
  const pivotMeta = this.metadata.find(prop.pivotEntity);
424
433
  const props = Object.values(pivotMeta?.properties ?? {});
@@ -439,35 +448,77 @@ export class MetadataDiscovery {
439
448
  prop.inverseJoinColumns ??= second.fieldNames;
440
449
  }
441
450
  if (!prop.pivotTable && prop.owner && this.platform.usesPivotTable()) {
442
- prop.pivotTable = this.namingStrategy.joinTableName(meta.tableName, meta2.tableName, prop.name);
451
+ prop.pivotTable = this.namingStrategy.joinTableName(meta.className, meta2.tableName, prop.name, meta.tableName);
443
452
  }
444
453
  if (prop.mappedBy) {
445
454
  const prop2 = meta2.properties[prop.mappedBy];
446
455
  this.initManyToManyFields(meta2, prop2);
447
456
  prop.pivotTable = prop2.pivotTable;
448
- prop.pivotEntity = prop2.pivotEntity ?? prop2.pivotTable;
457
+ prop.pivotEntity = prop2.pivotEntity;
449
458
  prop.fixedOrder = prop2.fixedOrder;
450
459
  prop.fixedOrderColumn = prop2.fixedOrderColumn;
451
460
  prop.joinColumns = prop2.inverseJoinColumns;
452
461
  prop.inverseJoinColumns = prop2.joinColumns;
462
+ prop.polymorphic = prop2.polymorphic;
463
+ prop.discriminator = prop2.discriminator;
464
+ prop.discriminatorColumn = prop2.discriminatorColumn;
465
+ prop.discriminatorValue = prop2.discriminatorValue;
453
466
  }
454
467
  prop.referencedColumnNames ??= Utils.flatten(meta.primaryKeys.map(primaryKey => meta.properties[primaryKey].fieldNames));
455
- prop.joinColumns ??= prop.referencedColumnNames.map(referencedColumnName => this.namingStrategy.joinKeyColumnName(meta.root.className, referencedColumnName, meta.compositePK));
456
- prop.inverseJoinColumns ??= this.initManyToOneFieldName(prop, meta2.root.className);
468
+ // For polymorphic M:N, use discriminator base name for FK column (e.g., taggable_id instead of post_id)
469
+ if (prop.polymorphic && prop.discriminator) {
470
+ prop.joinColumns ??= prop.referencedColumnNames.map(referencedColumnName => this.namingStrategy.joinKeyColumnName(prop.discriminator, referencedColumnName, prop.referencedColumnNames.length > 1));
471
+ }
472
+ else {
473
+ const ownerTableName = this.isExplicitTableName(meta.root) ? meta.root.tableName : undefined;
474
+ prop.joinColumns ??= prop.referencedColumnNames.map(referencedColumnName => this.namingStrategy.joinKeyColumnName(meta.root.className, referencedColumnName, meta.compositePK, ownerTableName));
475
+ }
476
+ const inverseTableName = this.isExplicitTableName(meta2.root) ? meta2.root.tableName : undefined;
477
+ prop.inverseJoinColumns ??= this.initManyToOneFieldName(prop, meta2.root.className, inverseTableName);
478
+ }
479
+ isExplicitTableName(meta) {
480
+ return meta.tableName !== this.namingStrategy.classToTableName(meta.className);
457
481
  }
458
482
  initManyToOneFields(prop) {
459
- const meta2 = this.metadata.get(prop.type);
460
- const fieldNames = Utils.flatten(meta2.primaryKeys.map(primaryKey => meta2.properties[primaryKey].fieldNames));
461
- Utils.defaultValue(prop, 'referencedTableName', meta2.collection);
483
+ if (prop.polymorphic && prop.polymorphTargets) {
484
+ const fieldNames1 = prop.targetMeta.getPrimaryProps().flatMap(pk => pk.fieldNames);
485
+ const idColumns = fieldNames1.map(fieldName => this.namingStrategy.joinKeyColumnName(prop.discriminator, fieldName, fieldNames1.length > 1));
486
+ prop.fieldNames ??= [prop.discriminatorColumn, ...idColumns];
487
+ prop.joinColumns ??= idColumns;
488
+ prop.referencedColumnNames ??= fieldNames1;
489
+ prop.referencedTableName ??= prop.targetMeta.tableName;
490
+ return;
491
+ }
492
+ const meta2 = prop.targetMeta;
493
+ let fieldNames;
494
+ // If targetKey is specified, use that property's field names instead of PKs
495
+ if (prop.targetKey) {
496
+ const targetProp = meta2.properties[prop.targetKey];
497
+ fieldNames = targetProp.fieldNames;
498
+ }
499
+ else {
500
+ fieldNames = Utils.flatten(meta2.primaryKeys.map(primaryKey => meta2.properties[primaryKey].fieldNames));
501
+ }
502
+ Utils.defaultValue(prop, 'referencedTableName', meta2.tableName);
462
503
  if (!prop.joinColumns) {
463
504
  prop.joinColumns = fieldNames.map(fieldName => this.namingStrategy.joinKeyColumnName(prop.name, fieldName, fieldNames.length > 1));
464
505
  }
465
506
  if (!prop.referencedColumnNames) {
466
507
  prop.referencedColumnNames = fieldNames;
467
508
  }
509
+ // Relations to composite PK targets need cascade update by default,
510
+ // since composite PKs are more likely to have mutable components
511
+ if (meta2.compositePK) {
512
+ prop.updateRule ??= 'cascade';
513
+ }
514
+ // Nullable relations default to 'set null' on delete - when the referenced
515
+ // entity is deleted, set the FK to null rather than failing
516
+ if (prop.nullable) {
517
+ prop.deleteRule ??= 'set null';
518
+ }
468
519
  }
469
520
  initOneToManyFields(prop) {
470
- const meta2 = this.metadata.get(prop.type);
521
+ const meta2 = prop.targetMeta;
471
522
  if (!prop.joinColumns) {
472
523
  prop.joinColumns = [this.namingStrategy.joinColumnName(prop.name)];
473
524
  }
@@ -480,12 +531,17 @@ export class MetadataDiscovery {
480
531
  const pks = Object.values(meta.properties).filter(prop => prop.primary);
481
532
  meta.primaryKeys = pks.map(prop => prop.name);
482
533
  meta.compositePK = pks.length > 1;
483
- // FK used as PK, we need to cascade
484
- if (pks.length === 1 && pks[0].kind !== ReferenceKind.SCALAR) {
485
- pks[0].deleteRule ??= 'cascade';
534
+ // FK used as PK, we need to cascade - applies to both single FK-as-PK
535
+ // and composite PKs where all PKs are FKs (e.g., pivot-like entities)
536
+ const fkPks = pks.filter(pk => pk.kind !== ReferenceKind.SCALAR);
537
+ if (fkPks.length > 0 && fkPks.length === pks.length) {
538
+ for (const pk of fkPks) {
539
+ pk.deleteRule ??= 'cascade';
540
+ pk.updateRule ??= 'cascade';
541
+ }
486
542
  }
487
543
  meta.forceConstructor ??= this.shouldForceConstructorUsage(meta);
488
- this.validator.validateEntityDefinition(this.metadata, meta.className, this.config.get('discovery'));
544
+ this.validator.validateEntityDefinition(this.metadata, meta.class, this.config.get('discovery'));
489
545
  for (const prop of Object.values(meta.properties)) {
490
546
  this.initNullability(prop);
491
547
  this.applyNamingStrategy(meta, prop);
@@ -498,15 +554,21 @@ export class MetadataDiscovery {
498
554
  }
499
555
  this.initOwnColumns(meta);
500
556
  meta.simplePK = pks.length === 1 && pks[0].kind === ReferenceKind.SCALAR && !pks[0].customType && pks[0].runtimeType !== 'Date';
501
- meta.serializedPrimaryKey = this.platform.getSerializedPrimaryKeyField(meta.primaryKeys[0]);
502
- const serializedPKProp = meta.properties[meta.serializedPrimaryKey];
503
- if (serializedPKProp && meta.serializedPrimaryKey !== meta.primaryKeys[0]) {
504
- serializedPKProp.persist = false;
557
+ meta.serializedPrimaryKey ??= meta.props.find(prop => prop.serializedPrimaryKey)?.name;
558
+ if (meta.serializedPrimaryKey && meta.serializedPrimaryKey !== meta.primaryKeys[0]) {
559
+ meta.properties[meta.serializedPrimaryKey].persist ??= false;
505
560
  }
506
561
  if (this.platform.usesPivotTable()) {
507
562
  return Object.values(meta.properties)
508
563
  .filter(prop => prop.kind === ReferenceKind.MANY_TO_MANY && prop.owner && prop.pivotTable)
509
- .map(prop => this.definePivotTableEntity(meta, prop));
564
+ .map(prop => {
565
+ const pivotMeta = this.definePivotTableEntity(meta, prop);
566
+ prop.pivotEntity = pivotMeta.class;
567
+ if (prop.inversedBy) {
568
+ prop.targetMeta.properties[prop.inversedBy].pivotEntity = pivotMeta.class;
569
+ }
570
+ return pivotMeta;
571
+ });
510
572
  }
511
573
  return [];
512
574
  }
@@ -523,8 +585,11 @@ export class MetadataDiscovery {
523
585
  ['mappedBy', 'inversedBy', 'pivotEntity'].forEach(type => {
524
586
  const value = prop[type];
525
587
  if (value instanceof Function) {
526
- const meta2 = this.metadata.get(prop.type);
588
+ const meta2 = prop.targetMeta ?? this.metadata.get(prop.target);
527
589
  prop[type] = value(meta2.properties)?.name;
590
+ if (type === 'pivotEntity' && value) {
591
+ prop[type] = value(meta2.properties);
592
+ }
528
593
  if (prop[type] == null) {
529
594
  throw MetadataError.fromWrongReference(meta, prop, type);
530
595
  }
@@ -540,9 +605,9 @@ export class MetadataDiscovery {
540
605
  }
541
606
  else if (fks.length >= 2) {
542
607
  [first, second] = fks;
543
- /* v8 ignore next 3 */
544
608
  }
545
609
  else {
610
+ /* v8 ignore next */
546
611
  return [];
547
612
  }
548
613
  // wrong FK order, first FK needs to point to the owning side
@@ -556,7 +621,9 @@ export class MetadataDiscovery {
556
621
  return [first, second];
557
622
  }
558
623
  definePivotTableEntity(meta, prop) {
559
- const pivotMeta = this.metadata.find(prop.pivotEntity);
624
+ const pivotMeta = prop.pivotEntity
625
+ ? this.metadata.find(prop.pivotEntity)
626
+ : this.metadata.getByClassName(prop.pivotTable, false);
560
627
  // ensure inverse side exists so we can join it when populating via pivot tables
561
628
  if (!prop.inversedBy && prop.targetMeta) {
562
629
  const inverseName = `${meta.className}_${prop.name}__inverse`;
@@ -565,6 +632,8 @@ export class MetadataDiscovery {
565
632
  name: inverseName,
566
633
  kind: ReferenceKind.MANY_TO_MANY,
567
634
  type: meta.className,
635
+ target: meta.class,
636
+ targetMeta: meta,
568
637
  mappedBy: prop.name,
569
638
  pivotEntity: prop.pivotEntity,
570
639
  pivotTable: prop.pivotTable,
@@ -573,55 +642,190 @@ export class MetadataDiscovery {
573
642
  };
574
643
  this.applyNamingStrategy(prop.targetMeta, inverseProp);
575
644
  this.initCustomType(prop.targetMeta, inverseProp);
576
- this.initRelation(inverseProp);
577
645
  prop.targetMeta.properties[inverseName] = inverseProp;
578
646
  }
579
647
  if (pivotMeta) {
648
+ prop.pivotEntity = pivotMeta.class;
580
649
  this.ensureCorrectFKOrderInPivotEntity(pivotMeta, prop);
650
+ if (prop.polymorphic && prop.discriminatorValue) {
651
+ pivotMeta.polymorphicDiscriminatorMap ??= {};
652
+ pivotMeta.polymorphicDiscriminatorMap[prop.discriminatorValue] = meta.class;
653
+ // For composite PK entities sharing a polymorphic pivot table,
654
+ // we need to add columns for each entity type's PKs
655
+ this.addPolymorphicPivotColumns(pivotMeta, meta, prop);
656
+ // Add virtual M:1 relation for this polymorphic owner (for join loading)
657
+ const ownerRelationName = `${prop.discriminator}_${meta.tableName}`;
658
+ if (!pivotMeta.properties[ownerRelationName]) {
659
+ pivotMeta.properties[ownerRelationName] = this.definePolymorphicOwnerRelation(prop, ownerRelationName, meta);
660
+ }
661
+ }
581
662
  return pivotMeta;
582
663
  }
583
- const exists = this.metadata.find(prop.pivotTable);
584
- if (exists) {
585
- prop.pivotEntity = exists.className;
586
- return exists;
587
- }
588
664
  let tableName = prop.pivotTable;
589
665
  let schemaName;
590
666
  if (prop.pivotTable.includes('.')) {
591
667
  [schemaName, tableName] = prop.pivotTable.split('.');
592
668
  }
593
669
  schemaName ??= meta.schema;
594
- const targetType = prop.targetMeta.className;
595
- const data = new EntityMetadata({
670
+ const targetMeta = prop.targetMeta;
671
+ const targetType = targetMeta.className;
672
+ const pivotMeta2 = new EntityMetadata({
596
673
  name: prop.pivotTable,
597
674
  className: prop.pivotTable,
598
675
  collection: tableName,
599
676
  schema: schemaName,
600
677
  pivotTable: true,
601
678
  });
602
- prop.pivotEntity = data.className;
679
+ prop.pivotEntity = pivotMeta2.class;
603
680
  if (prop.fixedOrder) {
604
- const primaryProp = this.defineFixedOrderProperty(prop, targetType);
605
- data.properties[primaryProp.name] = primaryProp;
681
+ const primaryProp = this.defineFixedOrderProperty(prop, targetMeta);
682
+ pivotMeta2.properties[primaryProp.name] = primaryProp;
606
683
  }
607
684
  else {
608
- data.compositePK = true;
685
+ pivotMeta2.compositePK = true;
609
686
  }
610
687
  // handle self-referenced m:n with same default field names
611
688
  if (meta.className === targetType && prop.joinColumns.every((joinColumn, idx) => joinColumn === prop.inverseJoinColumns[idx])) {
612
- prop.joinColumns = prop.referencedColumnNames.map(name => this.namingStrategy.joinKeyColumnName(meta.className + '_1', name, meta.compositePK));
613
- prop.inverseJoinColumns = prop.referencedColumnNames.map(name => this.namingStrategy.joinKeyColumnName(meta.className + '_2', name, meta.compositePK));
689
+ // use tableName only when explicitly provided by user, otherwise use className for backwards compatibility
690
+ const baseName = this.isExplicitTableName(meta) ? meta.tableName : meta.className;
691
+ prop.joinColumns = prop.referencedColumnNames.map(name => this.namingStrategy.joinKeyColumnName(baseName + '_1', name, meta.compositePK));
692
+ prop.inverseJoinColumns = prop.referencedColumnNames.map(name => this.namingStrategy.joinKeyColumnName(baseName + '_2', name, meta.compositePK));
614
693
  if (prop.inversedBy) {
615
- const prop2 = this.metadata.get(targetType).properties[prop.inversedBy];
694
+ const prop2 = targetMeta.properties[prop.inversedBy];
616
695
  prop2.inverseJoinColumns = prop.joinColumns;
617
696
  prop2.joinColumns = prop.inverseJoinColumns;
618
697
  }
698
+ // propagate updated joinColumns to all child entities that inherit this property (STI)
699
+ for (const childMeta of this.discovered.filter(m => m.root === meta && m !== meta)) {
700
+ const childProp = childMeta.properties[prop.name];
701
+ if (childProp) {
702
+ childProp.joinColumns = prop.joinColumns;
703
+ childProp.inverseJoinColumns = prop.inverseJoinColumns;
704
+ }
705
+ }
706
+ }
707
+ // For polymorphic M:N, create discriminator column and polymorphic FK
708
+ if (prop.polymorphic && prop.discriminatorColumn) {
709
+ this.definePolymorphicPivotProperties(pivotMeta2, meta, prop, targetMeta);
710
+ }
711
+ else {
712
+ pivotMeta2.properties[meta.name + '_owner'] = this.definePivotProperty(prop, meta.name + '_owner', meta.class, targetType + '_inverse', true, meta.className === targetType);
713
+ pivotMeta2.properties[targetType + '_inverse'] = this.definePivotProperty(prop, targetType + '_inverse', targetMeta.class, meta.name + '_owner', false, meta.className === targetType);
714
+ }
715
+ return this.metadata.set(pivotMeta2.class, EntitySchema.fromMetadata(pivotMeta2).init().meta);
716
+ }
717
+ /**
718
+ * Create a scalar property for a pivot table column.
719
+ */
720
+ createPivotScalarProperty(name, columnTypes, fieldNames = [name], options = {}) {
721
+ return {
722
+ name,
723
+ fieldNames,
724
+ columnTypes,
725
+ type: options.type ?? 'number',
726
+ kind: ReferenceKind.SCALAR,
727
+ primary: options.primary ?? false,
728
+ nullable: options.nullable ?? true,
729
+ ...(options.persist !== undefined && { persist: options.persist }),
730
+ };
731
+ }
732
+ /**
733
+ * Get column types for an entity's primary keys, initializing them if needed.
734
+ */
735
+ getPrimaryKeyColumnTypes(meta) {
736
+ const columnTypes = [];
737
+ for (const pk of meta.primaryKeys) {
738
+ const pkProp = meta.properties[pk];
739
+ this.initCustomType(meta, pkProp);
740
+ this.initColumnType(pkProp);
741
+ columnTypes.push(...pkProp.columnTypes);
742
+ }
743
+ return columnTypes;
744
+ }
745
+ /**
746
+ * Add missing FK columns for a polymorphic entity to an existing pivot table.
747
+ */
748
+ addPolymorphicPivotColumns(pivotMeta, meta, prop) {
749
+ const existingFieldNames = new Set(Object.values(pivotMeta.properties).flatMap(p => p.fieldNames ?? []));
750
+ const columnTypes = this.getPrimaryKeyColumnTypes(meta);
751
+ for (let i = 0; i < prop.joinColumns.length; i++) {
752
+ const joinColumn = prop.joinColumns[i];
753
+ if (!existingFieldNames.has(joinColumn)) {
754
+ pivotMeta.properties[joinColumn] = this.createPivotScalarProperty(joinColumn, [columnTypes[i]]);
755
+ }
756
+ }
757
+ }
758
+ /**
759
+ * Define properties for a polymorphic pivot table.
760
+ */
761
+ definePolymorphicPivotProperties(pivotMeta, meta, prop, targetMeta) {
762
+ const discriminatorColumn = prop.discriminatorColumn;
763
+ const isCompositePK = meta.compositePK;
764
+ // For composite PK polymorphic M:N, we need fixedOrder (auto-increment PK)
765
+ if (isCompositePK && !prop.fixedOrder) {
766
+ prop.fixedOrder = true;
767
+ const primaryProp = this.defineFixedOrderProperty(prop, targetMeta);
768
+ pivotMeta.properties[primaryProp.name] = primaryProp;
769
+ pivotMeta.compositePK = false;
770
+ }
771
+ const discriminatorProp = this.createPivotScalarProperty(discriminatorColumn, [this.platform.getVarcharTypeDeclarationSQL(prop)], [discriminatorColumn], { type: 'string', primary: !isCompositePK, nullable: false });
772
+ this.initFieldName(discriminatorProp);
773
+ pivotMeta.properties[discriminatorColumn] = discriminatorProp;
774
+ const columnTypes = this.getPrimaryKeyColumnTypes(meta);
775
+ if (isCompositePK) {
776
+ // Create separate properties for each PK column (nullable for other entity types)
777
+ for (let i = 0; i < prop.joinColumns.length; i++) {
778
+ pivotMeta.properties[prop.joinColumns[i]] = this.createPivotScalarProperty(prop.joinColumns[i], [columnTypes[i]]);
779
+ }
780
+ // Virtual property combining all columns (for compatibility)
781
+ pivotMeta.properties[prop.discriminator] = this.createPivotScalarProperty(prop.discriminator, columnTypes, [...prop.joinColumns], { type: meta.className, persist: false });
619
782
  }
620
- data.properties[meta.name + '_owner'] = this.definePivotProperty(prop, meta.name + '_owner', meta.className, targetType + '_inverse', true, meta.className === targetType);
621
- data.properties[targetType + '_inverse'] = this.definePivotProperty(prop, targetType + '_inverse', targetType, meta.name + '_owner', false, meta.className === targetType);
622
- return this.metadata.set(data.className, EntitySchema.fromMetadata(data).init().meta);
783
+ else {
784
+ pivotMeta.properties[prop.discriminator] = this.createPivotScalarProperty(prop.discriminator, columnTypes, [...prop.joinColumns], { type: meta.className, primary: true, nullable: false });
785
+ }
786
+ pivotMeta.properties[targetMeta.className + '_inverse'] = this.definePivotProperty(prop, targetMeta.className + '_inverse', targetMeta.class, prop.discriminator, false, false);
787
+ // Create virtual M:1 relation to the polymorphic owner for single-query join loading
788
+ const ownerRelationName = `${prop.discriminator}_${meta.tableName}`;
789
+ pivotMeta.properties[ownerRelationName] = this.definePolymorphicOwnerRelation(prop, ownerRelationName, meta);
790
+ pivotMeta.polymorphicDiscriminatorMap ??= {};
791
+ pivotMeta.polymorphicDiscriminatorMap[prop.discriminatorValue] = meta.class;
792
+ }
793
+ /**
794
+ * Create a virtual M:1 relation from pivot to a polymorphic owner entity.
795
+ * This enables single-query join loading for inverse-side polymorphic M:N.
796
+ */
797
+ definePolymorphicOwnerRelation(prop, name, targetMeta) {
798
+ const ret = {
799
+ name,
800
+ type: targetMeta.className,
801
+ target: targetMeta.class,
802
+ kind: ReferenceKind.MANY_TO_ONE,
803
+ nullable: true,
804
+ owner: true,
805
+ primary: false,
806
+ createForeignKeyConstraint: false,
807
+ persist: false,
808
+ index: false,
809
+ };
810
+ ret.targetMeta = targetMeta;
811
+ ret.fieldNames = ret.joinColumns = ret.ownColumns = [...prop.joinColumns];
812
+ ret.referencedColumnNames = [];
813
+ ret.inverseJoinColumns = [];
814
+ for (const primaryKey of targetMeta.primaryKeys) {
815
+ const pkProp = targetMeta.properties[primaryKey];
816
+ ret.referencedColumnNames.push(...pkProp.fieldNames);
817
+ ret.inverseJoinColumns.push(...pkProp.fieldNames);
818
+ ret.length = pkProp.length;
819
+ ret.precision = pkProp.precision;
820
+ ret.scale = pkProp.scale;
821
+ }
822
+ const schema = targetMeta.schema ?? this.config.get('schema') ?? this.platform.getDefaultSchemaName();
823
+ ret.referencedTableName = schema && schema !== '*' ? schema + '.' + targetMeta.tableName : targetMeta.tableName;
824
+ this.initColumnType(ret);
825
+ this.initRelation(ret);
826
+ return ret;
623
827
  }
624
- defineFixedOrderProperty(prop, targetType) {
828
+ defineFixedOrderProperty(prop, targetMeta) {
625
829
  const pk = prop.fixedOrderColumn || this.namingStrategy.referenceColumnName();
626
830
  const primaryProp = {
627
831
  name: pk,
@@ -635,7 +839,7 @@ export class MetadataDiscovery {
635
839
  this.initColumnType(primaryProp);
636
840
  prop.fixedOrderColumn = pk;
637
841
  if (prop.inversedBy) {
638
- const prop2 = this.metadata.get(targetType).properties[prop.inversedBy];
842
+ const prop2 = targetMeta.properties[prop.inversedBy];
639
843
  prop2.fixedOrder = true;
640
844
  prop2.fixedOrderColumn = pk;
641
845
  }
@@ -644,7 +848,8 @@ export class MetadataDiscovery {
644
848
  definePivotProperty(prop, name, type, inverse, owner, selfReferencing) {
645
849
  const ret = {
646
850
  name,
647
- type,
851
+ type: Utils.className(type),
852
+ target: type,
648
853
  kind: ReferenceKind.MANY_TO_ONE,
649
854
  cascade: [Cascade.ALL],
650
855
  fixedOrder: prop.fixedOrder,
@@ -656,10 +861,9 @@ export class MetadataDiscovery {
656
861
  deleteRule: prop.deleteRule,
657
862
  createForeignKeyConstraint: prop.createForeignKeyConstraint,
658
863
  };
659
- if (selfReferencing && !this.platform.supportsMultipleCascadePaths()) {
660
- ret.updateRule ??= 'no action';
661
- ret.deleteRule ??= 'no action';
662
- }
864
+ const defaultRule = selfReferencing && !this.platform.supportsMultipleCascadePaths() ? 'no action' : 'cascade';
865
+ ret.updateRule ??= defaultRule;
866
+ ret.deleteRule ??= defaultRule;
663
867
  const meta = this.metadata.get(type);
664
868
  ret.targetMeta = meta;
665
869
  ret.joinColumns = [];
@@ -702,7 +906,7 @@ export class MetadataDiscovery {
702
906
  Object.values(meta.properties)
703
907
  .filter(prop => prop.kind !== ReferenceKind.SCALAR && !prop.owner && prop.mappedBy)
704
908
  .forEach(prop => {
705
- const meta2 = this.metadata.get(prop.type);
909
+ const meta2 = prop.targetMeta;
706
910
  const prop2 = meta2.properties[prop.mappedBy];
707
911
  if (prop2 && !prop2.inversedBy) {
708
912
  prop2.inversedBy = prop.name;
@@ -710,7 +914,7 @@ export class MetadataDiscovery {
710
914
  });
711
915
  }
712
916
  defineBaseEntityProperties(meta) {
713
- const base = meta.extends && this.metadata.get(Utils.className(meta.extends));
917
+ const base = meta.extends && this.metadata.get(meta.extends);
714
918
  if (!base || base === meta) { // make sure we do not fall into infinite loop
715
919
  return 0;
716
920
  }
@@ -741,12 +945,9 @@ export class MetadataDiscovery {
741
945
  Utils.keys(base.hooks).forEach(type => {
742
946
  meta.hooks[type] = Utils.unique([...base.hooks[type], ...(meta.hooks[type] || [])]);
743
947
  });
744
- if (meta.constructorParams.length === 0 && base.constructorParams.length > 0) {
948
+ if ((meta.constructorParams?.length ?? 0) === 0 && (base.constructorParams?.length ?? 0) > 0) {
745
949
  meta.constructorParams = [...base.constructorParams];
746
950
  }
747
- if (meta.toJsonParams.length === 0 && base.toJsonParams.length > 0) {
748
- meta.toJsonParams = [...base.toJsonParams];
749
- }
750
951
  return order;
751
952
  }
752
953
  initPolyEmbeddables(embeddedProp, discovered, visited = new Set()) {
@@ -803,6 +1004,54 @@ export class MetadataDiscovery {
803
1004
  polymorphs.forEach(meta => meta.root = embeddable);
804
1005
  }
805
1006
  }
1007
+ initPolymorphicRelation(meta, prop, discovered) {
1008
+ if (!prop.discriminator && !prop.discriminatorColumn && !prop.discriminatorMap && !Array.isArray(prop.target)) {
1009
+ return;
1010
+ }
1011
+ prop.polymorphic = true;
1012
+ prop.discriminator ??= prop.name;
1013
+ prop.discriminatorColumn ??= this.namingStrategy.discriminatorColumnName(prop.discriminator);
1014
+ prop.createForeignKeyConstraint = false;
1015
+ const isToOne = [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind);
1016
+ if (isToOne) {
1017
+ const types = prop.type.split(/ ?\| ?/);
1018
+ prop.polymorphTargets = discovered.filter(m => types.includes(m.className) && !m.embeddable);
1019
+ prop.targetMeta = prop.polymorphTargets[0];
1020
+ prop.referencedPKs = prop.targetMeta?.primaryKeys;
1021
+ }
1022
+ if (prop.discriminatorMap) {
1023
+ const normalizedMap = {};
1024
+ for (const [key, value] of Object.entries(prop.discriminatorMap)) {
1025
+ const targetMeta = this.metadata.getByClassName(value, false);
1026
+ if (!targetMeta) {
1027
+ throw MetadataError.fromUnknownEntity(value, `${meta.className}.${prop.name} discriminatorMap`);
1028
+ }
1029
+ normalizedMap[key] = targetMeta.class;
1030
+ if (targetMeta.class === meta.class) {
1031
+ prop.discriminatorValue = key;
1032
+ }
1033
+ }
1034
+ prop.discriminatorMap = normalizedMap;
1035
+ }
1036
+ else if (isToOne) {
1037
+ prop.discriminatorMap = {};
1038
+ const tableNameToTarget = new Map();
1039
+ for (const target of prop.polymorphTargets) {
1040
+ const existing = tableNameToTarget.get(target.tableName);
1041
+ if (existing) {
1042
+ throw MetadataError.incompatiblePolymorphicTargets(meta, prop, existing, target, `both use table '${target.tableName}'. Use separate properties instead of a single polymorphic relation.`);
1043
+ }
1044
+ tableNameToTarget.set(target.tableName, target);
1045
+ prop.discriminatorMap[target.tableName] = target.class;
1046
+ }
1047
+ }
1048
+ else {
1049
+ prop.discriminatorValue ??= meta.tableName;
1050
+ if (!prop.discriminatorMap) {
1051
+ prop.discriminatorMap = { [prop.discriminatorValue]: meta.class };
1052
+ }
1053
+ }
1054
+ }
806
1055
  initEmbeddables(meta, embeddedProp, visited = new Set()) {
807
1056
  if (embeddedProp.kind !== ReferenceKind.EMBEDDED || visited.has(embeddedProp)) {
808
1057
  return;
@@ -838,7 +1087,7 @@ export class MetadataDiscovery {
838
1087
  const glue = object ? '~' : '_';
839
1088
  for (const prop of Object.values(embeddable.properties)) {
840
1089
  const name = (embeddedProp.embeddedPath?.join(glue) ?? embeddedProp.fieldNames[0] + glue) + prop.name;
841
- meta.properties[name] = Utils.copy(prop, false);
1090
+ meta.properties[name] = Utils.copy(prop);
842
1091
  meta.properties[name].name = name;
843
1092
  meta.properties[name].embedded = [embeddedProp.name, prop.name];
844
1093
  meta.propertyOrder.set(name, (order += 0.01));
@@ -875,10 +1124,12 @@ export class MetadataDiscovery {
875
1124
  path = [embeddedProp.fieldNames[0]];
876
1125
  }
877
1126
  this.initFieldName(prop, true);
1127
+ this.initRelation(prop);
878
1128
  path.push(prop.fieldNames[0]);
879
1129
  meta.properties[name].fieldNames = prop.fieldNames;
880
1130
  meta.properties[name].embeddedPath = path;
881
- const fieldName = raw(this.platform.getSearchJsonPropertySQL(path.join('->'), prop.runtimeType ?? prop.type, true));
1131
+ const targetProp = prop.targetMeta?.getPrimaryProp() ?? prop;
1132
+ const fieldName = raw(this.platform.getSearchJsonPropertySQL(path.join('->'), targetProp.runtimeType ?? targetProp.type, true));
882
1133
  meta.properties[name].fieldNameRaw = fieldName.sql; // for querying in SQL drivers
883
1134
  meta.properties[name].persist = false; // only virtual as we store the whole object
884
1135
  meta.properties[name].userDefined = false; // mark this as a generated/internal property, so we can distinguish from user-defined non-persist properties
@@ -906,7 +1157,7 @@ export class MetadataDiscovery {
906
1157
  }
907
1158
  initSingleTableInheritance(meta, metadata) {
908
1159
  if (meta.root !== meta && !meta.__processed) {
909
- meta.root = metadata.find(m => m.className === meta.root.className);
1160
+ meta.root = metadata.find(m => m.class === meta.root.class);
910
1161
  meta.root.__processed = true;
911
1162
  }
912
1163
  else {
@@ -915,17 +1166,24 @@ export class MetadataDiscovery {
915
1166
  if (!meta.root.discriminatorColumn) {
916
1167
  return;
917
1168
  }
918
- if (!meta.root.discriminatorMap) {
1169
+ meta.root.inheritanceType = 'sti';
1170
+ if (meta.root.discriminatorMap) {
1171
+ const map = meta.root.discriminatorMap;
1172
+ Object.keys(map)
1173
+ .filter(key => typeof map[key] === 'string')
1174
+ .forEach(key => map[key] = this.metadata.getByClassName(map[key]).class);
1175
+ }
1176
+ else {
919
1177
  meta.root.discriminatorMap = {};
920
1178
  const children = metadata
921
- .filter(m => m.root.className === meta.root.className && !m.abstract)
1179
+ .filter(m => m.root.class === meta.root.class && !m.abstract)
922
1180
  .sort((a, b) => a.className.localeCompare(b.className));
923
1181
  for (const m of children) {
924
1182
  const name = m.discriminatorValue ?? this.namingStrategy.classToTableName(m.className);
925
- meta.root.discriminatorMap[name] = m.className;
1183
+ meta.root.discriminatorMap[name] = m.class;
926
1184
  }
927
1185
  }
928
- meta.discriminatorValue = Object.entries(meta.root.discriminatorMap).find(([, className]) => className === meta.className)?.[0];
1186
+ meta.discriminatorValue = QueryHelper.findDiscriminatorValue(meta.root.discriminatorMap, meta.class);
929
1187
  if (!meta.root.properties[meta.root.discriminatorColumn]) {
930
1188
  this.createDiscriminatorProperty(meta.root);
931
1189
  }
@@ -934,32 +1192,219 @@ export class MetadataDiscovery {
934
1192
  if (meta.root === meta) {
935
1193
  return;
936
1194
  }
937
- let i = 1;
938
1195
  Object.values(meta.properties).forEach(prop => {
939
1196
  const newProp = { ...prop };
940
- if (meta.root.properties[prop.name] && meta.root.properties[prop.name].type !== prop.type) {
1197
+ const rootProp = meta.root.properties[prop.name];
1198
+ if (rootProp && (rootProp.type !== prop.type || (rootProp.fieldNames && prop.fieldNames && !compareArrays(rootProp.fieldNames, prop.fieldNames)))) {
941
1199
  const name = newProp.name;
942
1200
  this.initFieldName(newProp, newProp.object);
943
- newProp.name = name + '_' + (i++);
1201
+ newProp.renamedFrom = name;
1202
+ newProp.name = `${name}_${meta._id}`;
944
1203
  meta.root.addProperty(newProp);
1204
+ this.initFieldName(prop, prop.object);
1205
+ // Track all field variants and map discriminator values to field names
1206
+ if (!rootProp.stiFieldNames) {
1207
+ this.initFieldName(rootProp, rootProp.object);
1208
+ rootProp.stiFieldNames = [...rootProp.fieldNames];
1209
+ rootProp.stiFieldNameMap = {};
1210
+ // Find which discriminator owns the original fieldNames
1211
+ for (const [discValue, childClass] of Object.entries(meta.root.discriminatorMap)) {
1212
+ const childMeta = this.metadata.find(childClass);
1213
+ if (childMeta?.properties[prop.name]?.fieldNames && compareArrays(childMeta.properties[prop.name].fieldNames, rootProp.fieldNames)) {
1214
+ rootProp.stiFieldNameMap[discValue] = rootProp.fieldNames[0];
1215
+ break;
1216
+ }
1217
+ }
1218
+ }
1219
+ rootProp.stiFieldNameMap[meta.discriminatorValue] = prop.fieldNames[0];
1220
+ rootProp.stiFieldNames.push(...prop.fieldNames);
945
1221
  newProp.nullable = true;
946
1222
  newProp.name = name;
947
1223
  newProp.hydrate = false;
948
1224
  newProp.inherited = true;
949
1225
  return;
950
1226
  }
951
- if (prop.enum && prop.items && meta.root.properties[prop.name]?.items) {
952
- newProp.items = Utils.unique([...meta.root.properties[prop.name].items, ...prop.items]);
1227
+ if (prop.enum && prop.items && rootProp?.items) {
1228
+ newProp.items = Utils.unique([...rootProp.items, ...prop.items]);
953
1229
  }
954
1230
  newProp.nullable = true;
955
- newProp.inherited = true;
1231
+ newProp.inherited = !rootProp;
956
1232
  meta.root.addProperty(newProp);
957
1233
  });
958
- meta.collection = meta.root.collection;
1234
+ meta.tableName = meta.root.tableName;
959
1235
  meta.root.indexes = Utils.unique([...meta.root.indexes, ...meta.indexes]);
960
1236
  meta.root.uniques = Utils.unique([...meta.root.uniques, ...meta.uniques]);
961
1237
  meta.root.checks = Utils.unique([...meta.root.checks, ...meta.checks]);
962
1238
  }
1239
+ /**
1240
+ * First pass of TPT initialization: sets up hierarchy relationships
1241
+ * (inheritanceType, tptParent, tptChildren) before properties have fieldNames.
1242
+ */
1243
+ initTPTRelationships(meta, metadata) {
1244
+ if (meta.root !== meta) {
1245
+ meta.root = metadata.find(m => m.class === meta.root.class);
1246
+ }
1247
+ const inheritance = meta.inheritance;
1248
+ if (inheritance === 'tpt' && meta.root === meta) {
1249
+ meta.inheritanceType = 'tpt';
1250
+ meta.tptChildren = [];
1251
+ }
1252
+ if (meta.root.inheritanceType !== 'tpt') {
1253
+ return;
1254
+ }
1255
+ const parent = this.getTPTParent(meta, metadata);
1256
+ if (parent) {
1257
+ meta.tptParent = parent;
1258
+ meta.inheritanceType = 'tpt';
1259
+ parent.tptChildren ??= [];
1260
+ if (!parent.tptChildren.includes(meta)) {
1261
+ parent.tptChildren.push(meta);
1262
+ }
1263
+ }
1264
+ }
1265
+ /**
1266
+ * Second pass of TPT initialization: re-resolves metadata references after fieldNames
1267
+ * are set, syncs to registry metadata, and sets up discriminators.
1268
+ */
1269
+ finalizeTPTInheritance(meta, metadata) {
1270
+ if (meta.inheritanceType !== 'tpt') {
1271
+ return;
1272
+ }
1273
+ if (meta.tptParent) {
1274
+ meta.tptParent = metadata.find(m => m.class === meta.tptParent.class) ?? meta.tptParent;
1275
+ }
1276
+ if (meta.tptChildren) {
1277
+ meta.tptChildren = meta.tptChildren.map(child => metadata.find(m => m.class === child.class) ?? child);
1278
+ }
1279
+ const registryMeta = this.metadata.get(meta.class);
1280
+ if (registryMeta && registryMeta !== meta) {
1281
+ registryMeta.inheritanceType = meta.inheritanceType;
1282
+ registryMeta.tptParent = meta.tptParent ? this.metadata.get(meta.tptParent.class) : undefined;
1283
+ registryMeta.tptChildren = meta.tptChildren?.map(child => this.metadata.get(child.class));
1284
+ }
1285
+ this.initTPTDiscriminator(meta, metadata);
1286
+ }
1287
+ /**
1288
+ * Initialize TPT discriminator map and virtual discriminator property.
1289
+ * Unlike STI where the discriminator is a persisted column, TPT discriminator is computed
1290
+ * at query time using CASE WHEN expressions based on which child table has data.
1291
+ */
1292
+ initTPTDiscriminator(meta, metadata) {
1293
+ const allDescendants = this.collectAllTPTDescendants(meta, metadata);
1294
+ allDescendants.sort((a, b) => this.getTPTDepth(b) - this.getTPTDepth(a));
1295
+ meta.allTPTDescendants = allDescendants;
1296
+ if (meta.root !== meta) {
1297
+ return;
1298
+ }
1299
+ meta.root.discriminatorMap = {};
1300
+ for (const m of allDescendants) {
1301
+ const name = this.namingStrategy.classToTableName(m.className);
1302
+ meta.root.discriminatorMap[name] = m.class;
1303
+ m.discriminatorValue = name;
1304
+ }
1305
+ if (!meta.abstract) {
1306
+ const name = this.namingStrategy.classToTableName(meta.className);
1307
+ meta.root.discriminatorMap[name] = meta.class;
1308
+ meta.discriminatorValue = name;
1309
+ }
1310
+ // Virtual discriminator property - computed at query time via CASE WHEN, not persisted
1311
+ const discriminatorColumn = '__tpt_type';
1312
+ if (!meta.root.properties[discriminatorColumn]) {
1313
+ meta.root.addProperty({
1314
+ name: discriminatorColumn,
1315
+ type: 'string',
1316
+ kind: ReferenceKind.SCALAR,
1317
+ persist: false,
1318
+ userDefined: false,
1319
+ hidden: true,
1320
+ });
1321
+ }
1322
+ meta.root.tptDiscriminatorColumn = discriminatorColumn;
1323
+ }
1324
+ /**
1325
+ * Recursively collect all TPT descendants (children, grandchildren, etc.)
1326
+ */
1327
+ collectAllTPTDescendants(meta, metadata) {
1328
+ const descendants = [];
1329
+ const collect = (parent) => {
1330
+ for (const child of parent.tptChildren ?? []) {
1331
+ const resolved = metadata.find(m => m.class === child.class) ?? child;
1332
+ if (!resolved.abstract) {
1333
+ descendants.push(resolved);
1334
+ }
1335
+ collect(resolved);
1336
+ }
1337
+ };
1338
+ collect(meta);
1339
+ return descendants;
1340
+ }
1341
+ /**
1342
+ * Computes ownProps for TPT entities - only properties defined in THIS entity,
1343
+ * not inherited from parent. Also creates synthetic join properties for parent/child relationships.
1344
+ *
1345
+ * Called multiple times during discovery as metadata is progressively built.
1346
+ * Each pass overwrites earlier results to reflect the final state of properties.
1347
+ */
1348
+ computeTPTOwnProps(meta) {
1349
+ if (meta.inheritanceType !== 'tpt') {
1350
+ return;
1351
+ }
1352
+ const belongsToTable = (prop) => prop.persist !== false || prop.primary;
1353
+ // Use meta.properties (object) since meta.props (array) may not be populated yet
1354
+ const allProps = Object.values(meta.properties);
1355
+ if (!meta.tptParent) {
1356
+ meta.ownProps = allProps.filter(belongsToTable);
1357
+ return;
1358
+ }
1359
+ const parentPropNames = new Set(Object.values(meta.tptParent.properties).map(p => p.name));
1360
+ meta.ownProps = allProps.filter(prop => !parentPropNames.has(prop.name) && belongsToTable(prop));
1361
+ // Create synthetic join properties for the parent-child relationship
1362
+ const childFieldNames = meta.getPrimaryProps().flatMap(p => p.fieldNames);
1363
+ const parentFieldNames = meta.tptParent.getPrimaryProps().flatMap(p => p.fieldNames);
1364
+ meta.tptParentProp = {
1365
+ name: '[tpt:parent]',
1366
+ kind: ReferenceKind.MANY_TO_ONE,
1367
+ targetMeta: meta.tptParent,
1368
+ fieldNames: childFieldNames,
1369
+ referencedColumnNames: parentFieldNames,
1370
+ persist: false,
1371
+ };
1372
+ meta.tptInverseProp = {
1373
+ name: '[tpt:child]',
1374
+ kind: ReferenceKind.ONE_TO_ONE,
1375
+ owner: true,
1376
+ targetMeta: meta,
1377
+ fieldNames: parentFieldNames,
1378
+ joinColumns: parentFieldNames,
1379
+ referencedColumnNames: childFieldNames,
1380
+ persist: false,
1381
+ };
1382
+ }
1383
+ /** Returns the depth of a TPT entity in its hierarchy (0 for root). */
1384
+ getTPTDepth(meta) {
1385
+ let depth = 0;
1386
+ let current = meta;
1387
+ while (current.tptParent) {
1388
+ depth++;
1389
+ current = current.tptParent;
1390
+ }
1391
+ return depth;
1392
+ }
1393
+ /**
1394
+ * Find the direct TPT parent entity for the given entity.
1395
+ */
1396
+ getTPTParent(meta, metadata) {
1397
+ if (meta.root === meta) {
1398
+ return undefined;
1399
+ }
1400
+ return metadata.find(m => {
1401
+ const ext = meta.extends;
1402
+ if (ext instanceof EntitySchema) {
1403
+ return m.class === ext.meta.class || m.className === ext.meta.className;
1404
+ }
1405
+ return m.class === ext || m.className === Utils.className(ext);
1406
+ });
1407
+ }
963
1408
  createDiscriminatorProperty(meta) {
964
1409
  meta.addProperty({
965
1410
  name: meta.discriminatorColumn,
@@ -976,18 +1421,28 @@ export class MetadataDiscovery {
976
1421
  pks[0].autoincrement ??= true;
977
1422
  }
978
1423
  }
1424
+ createSchemaTable(meta) {
1425
+ const qualifiedName = meta.schema ? `${meta.schema}.${meta.tableName}` : meta.tableName;
1426
+ return {
1427
+ name: meta.tableName,
1428
+ schema: meta.schema,
1429
+ qualifiedName,
1430
+ toString: () => qualifiedName,
1431
+ };
1432
+ }
979
1433
  initCheckConstraints(meta) {
980
- const map = meta.createColumnMappingObject();
1434
+ const columns = meta.createSchemaColumnMappingObject();
1435
+ const table = this.createSchemaTable(meta);
981
1436
  for (const check of meta.checks) {
982
- const columns = check.property ? meta.properties[check.property].fieldNames : [];
983
- check.name ??= this.namingStrategy.indexName(meta.tableName, columns, 'check');
1437
+ const fieldNames = check.property ? meta.properties[check.property].fieldNames : [];
1438
+ check.name ??= this.namingStrategy.indexName(meta.tableName, fieldNames, 'check');
984
1439
  if (check.expression instanceof Function) {
985
- check.expression = check.expression(map);
1440
+ check.expression = check.expression(columns, table);
986
1441
  }
987
1442
  }
988
1443
  if (this.platform.usesEnumCheckConstraints() && !meta.embeddable) {
989
1444
  for (const prop of meta.props) {
990
- if (prop.enum && !prop.nativeEnumName && prop.items?.every(item => Utils.isString(item))) {
1445
+ if (prop.enum && prop.persist !== false && !prop.nativeEnumName && prop.items?.every(item => typeof item === 'string')) {
991
1446
  this.initFieldName(prop);
992
1447
  meta.checks.push({
993
1448
  name: this.namingStrategy.indexName(meta.tableName, prop.fieldNames, 'check'),
@@ -1012,18 +1467,18 @@ export class MetadataDiscovery {
1012
1467
  }
1013
1468
  return;
1014
1469
  }
1015
- const map = meta.createColumnMappingObject();
1016
1470
  if (prop.generated instanceof Function) {
1017
- prop.generated = prop.generated(map);
1471
+ const columns = meta.createSchemaColumnMappingObject();
1472
+ prop.generated = prop.generated(columns, this.createSchemaTable(meta));
1018
1473
  }
1019
1474
  }
1020
1475
  getDefaultVersionValue(meta, prop) {
1021
1476
  if (typeof prop.defaultRaw !== 'undefined') {
1022
1477
  return prop.defaultRaw;
1023
1478
  }
1024
- /* v8 ignore next 3 */
1479
+ /* v8 ignore next */
1025
1480
  if (prop.default != null) {
1026
- return '' + this.platform.quoteVersionValue(prop.default, prop);
1481
+ return '' + this.platform.convertVersionValue(prop.default, prop);
1027
1482
  }
1028
1483
  this.initCustomType(meta, prop, true);
1029
1484
  const type = prop.customType?.runtimeType ?? prop.runtimeType ?? prop.type;
@@ -1034,10 +1489,6 @@ export class MetadataDiscovery {
1034
1489
  return '1';
1035
1490
  }
1036
1491
  inferDefaultValue(meta, prop) {
1037
- /* v8 ignore next 3 */
1038
- if (!meta.class) {
1039
- return;
1040
- }
1041
1492
  try {
1042
1493
  // try to create two entity instances to detect the value is stable
1043
1494
  const now = Date.now();
@@ -1065,7 +1516,7 @@ export class MetadataDiscovery {
1065
1516
  return;
1066
1517
  }
1067
1518
  let val = prop.default;
1068
- const raw = RawQueryFragment.getKnownFragment(val);
1519
+ const raw = Raw.getKnownFragment(val);
1069
1520
  if (raw) {
1070
1521
  prop.defaultRaw = this.platform.formatQuery(raw.sql, raw.params);
1071
1522
  return;
@@ -1112,51 +1563,61 @@ export class MetadataDiscovery {
1112
1563
  }
1113
1564
  // `prop.type` might also be custom type class (not instance), so `typeof MyType` will give us `function`, not `object`
1114
1565
  if (typeof prop.type === 'function' && Type.isMappedType(prop.type.prototype) && !prop.customType) {
1115
- prop.customType = new prop.type();
1116
- prop.type = prop.customType.constructor.name;
1566
+ // if the type is an ORM defined mapped type without `ensureComparable: true`,
1567
+ // we use just the type name, to have more performant hydration code
1568
+ const type = Utils.keys(t).find(type => {
1569
+ return !Type.getType(t[type]).ensureComparable(meta, prop) && prop.type === t[type];
1570
+ });
1571
+ if (type) {
1572
+ prop.type = type === 'datetime' ? 'Date' : type;
1573
+ }
1574
+ else {
1575
+ prop.customType = new prop.type();
1576
+ prop.type = prop.customType.constructor.name;
1577
+ }
1117
1578
  }
1118
1579
  if (simple) {
1119
1580
  return;
1120
1581
  }
1121
1582
  if (!prop.customType && ['json', 'jsonb'].includes(prop.type?.toLowerCase())) {
1122
- prop.customType = new JsonType();
1583
+ prop.customType = new t.json();
1123
1584
  }
1124
1585
  if (prop.kind === ReferenceKind.SCALAR && !prop.customType && prop.columnTypes && ['json', 'jsonb'].includes(prop.columnTypes[0])) {
1125
- prop.customType = new JsonType();
1586
+ prop.customType = new t.json();
1126
1587
  }
1127
1588
  if (prop.kind === ReferenceKind.EMBEDDED && !prop.customType && (prop.object || prop.array)) {
1128
- prop.customType = new JsonType();
1589
+ prop.customType = new t.json();
1129
1590
  }
1130
1591
  if (!prop.customType && prop.array && prop.items) {
1131
- prop.customType = new EnumArrayType(`${meta.className}.${prop.name}`, prop.items);
1592
+ prop.customType = new t.enumArray(`${meta.className}.${prop.name}`, prop.items);
1132
1593
  }
1133
1594
  const isArray = prop.type?.toLowerCase() === 'array' || prop.type?.toString().endsWith('[]');
1134
1595
  if (objectEmbeddable && !prop.customType && isArray) {
1135
- prop.customType = new JsonType();
1596
+ prop.customType = new t.json();
1136
1597
  }
1137
1598
  // for number arrays we make sure to convert the items to numbers
1138
1599
  if (!prop.customType && prop.type === 'number[]') {
1139
- prop.customType = new ArrayType(i => +i);
1600
+ prop.customType = new t.array(i => +i);
1140
1601
  }
1141
1602
  // `string[]` can be returned via ts-morph, while reflect metadata will give us just `array`
1142
1603
  if (!prop.customType && isArray) {
1143
- prop.customType = new ArrayType();
1604
+ prop.customType = new t.array();
1144
1605
  }
1145
1606
  if (!prop.customType && prop.type?.toLowerCase() === 'buffer') {
1146
- prop.customType = new BlobType();
1607
+ prop.customType = new t.blob();
1147
1608
  }
1148
1609
  if (!prop.customType && prop.type?.toLowerCase() === 'uint8array') {
1149
- prop.customType = new Uint8ArrayType();
1610
+ prop.customType = new t.uint8array();
1150
1611
  }
1151
1612
  const mappedType = this.getMappedType(prop);
1152
1613
  if (prop.fieldNames?.length === 1 && !prop.customType) {
1153
- [BigIntType, DoubleType, DecimalType, IntervalType, DateType]
1614
+ [t.bigint, t.double, t.decimal, t.interval, t.date]
1154
1615
  .filter(type => mappedType instanceof type)
1155
1616
  .forEach((type) => prop.customType = new type());
1156
1617
  }
1157
1618
  if (prop.customType && !prop.columnTypes) {
1158
1619
  const mappedType = this.getMappedType({ columnTypes: [prop.customType.getColumnType(prop, this.platform)] });
1159
- if (prop.customType.compareAsType() === 'any' && ![JsonType].some(t => prop.customType instanceof t)) {
1620
+ if (prop.customType.compareAsType() === 'any' && ![t.json].some(t => prop.customType instanceof t)) {
1160
1621
  prop.runtimeType ??= mappedType.runtimeType;
1161
1622
  }
1162
1623
  else {
@@ -1176,16 +1637,16 @@ export class MetadataDiscovery {
1176
1637
  prop.columnTypes ??= [prop.customType.getColumnType(prop, this.platform)];
1177
1638
  prop.hasConvertToJSValueSQL = !!prop.customType.convertToJSValueSQL && prop.customType.convertToJSValueSQL('', this.platform) !== '';
1178
1639
  prop.hasConvertToDatabaseValueSQL = !!prop.customType.convertToDatabaseValueSQL && prop.customType.convertToDatabaseValueSQL('', this.platform) !== '';
1179
- if (prop.customType instanceof BigIntType && ['string', 'bigint', 'number'].includes(prop.runtimeType.toLowerCase())) {
1640
+ if (prop.customType instanceof t.bigint && ['string', 'bigint', 'number'].includes(prop.runtimeType.toLowerCase())) {
1180
1641
  prop.customType.mode = prop.runtimeType.toLowerCase();
1181
1642
  }
1182
1643
  }
1183
1644
  if (Type.isMappedType(prop.customType) && prop.kind === ReferenceKind.SCALAR && !isArray) {
1184
1645
  prop.type = prop.customType.name;
1185
1646
  }
1186
- if (!prop.customType && [ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind) && this.metadata.get(prop.type).compositePK) {
1647
+ if (!prop.customType && [ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind) && !prop.polymorphic && prop.targetMeta.compositePK) {
1187
1648
  prop.customTypes = [];
1188
- for (const pk of this.metadata.get(prop.type).getPrimaryProps()) {
1649
+ for (const pk of prop.targetMeta.getPrimaryProps()) {
1189
1650
  if (pk.customType) {
1190
1651
  prop.customTypes.push(pk.customType);
1191
1652
  prop.hasConvertToJSValueSQL ||= !!pk.customType.convertToJSValueSQL && pk.customType.convertToJSValueSQL('', this.platform) !== '';
@@ -1197,7 +1658,7 @@ export class MetadataDiscovery {
1197
1658
  }
1198
1659
  }
1199
1660
  }
1200
- if (prop.kind === ReferenceKind.SCALAR && !(mappedType instanceof UnknownType)) {
1661
+ if (prop.kind === ReferenceKind.SCALAR && !(mappedType instanceof t.unknown)) {
1201
1662
  if (!prop.columnTypes && prop.nativeEnumName && meta.schema !== this.platform.getDefaultSchemaName() && meta.schema && !prop.nativeEnumName.includes('.')) {
1202
1663
  prop.columnTypes = [`${meta.schema}.${prop.nativeEnumName}`];
1203
1664
  }
@@ -1212,22 +1673,36 @@ export class MetadataDiscovery {
1212
1673
  }
1213
1674
  }
1214
1675
  initRelation(prop) {
1215
- if (prop.kind === ReferenceKind.SCALAR) {
1676
+ if (prop.kind === ReferenceKind.SCALAR || prop.polymorphTargets) {
1216
1677
  return;
1217
1678
  }
1218
- const meta2 = this.discovered.find(m => m.className === prop.type);
1219
- prop.referencedPKs = meta2.primaryKeys;
1679
+ // 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
1680
+ const meta2 = this.metadata.find(prop.target) ?? this.metadata.getByClassName(prop.type);
1681
+ // If targetKey is specified, use that property instead of PKs for referencedPKs
1682
+ prop.referencedPKs = prop.targetKey ? [prop.targetKey] : meta2.primaryKeys;
1220
1683
  prop.targetMeta = meta2;
1684
+ if (meta2.view) {
1685
+ prop.createForeignKeyConstraint = false;
1686
+ }
1687
+ // Auto-generate formula for persist: false relations, but only for single-column FKs
1688
+ // Composite FK relations need standard JOIN conditions, not formula-based
1221
1689
  if (!prop.formula && prop.persist === false && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && !prop.embedded) {
1222
- prop.formula = a => `${a}.${this.platform.quoteIdentifier(prop.fieldNames[0])}`;
1690
+ this.initFieldName(prop);
1691
+ if (prop.fieldNames?.length === 1) {
1692
+ prop.formula = table => `${table}.${this.platform.quoteIdentifier(prop.fieldNames[0])}`;
1693
+ }
1223
1694
  }
1224
1695
  }
1225
1696
  initColumnType(prop) {
1226
1697
  this.initUnsigned(prop);
1227
- this.metadata.find(prop.type)?.getPrimaryProps().map(pk => {
1228
- prop.length ??= pk.length;
1229
- prop.precision ??= pk.precision;
1230
- prop.scale ??= pk.scale;
1698
+ // Get the target properties for FK relations - use targetKey property if specified, otherwise PKs
1699
+ const targetProps = prop.targetMeta
1700
+ ? (prop.targetKey ? [prop.targetMeta.properties[prop.targetKey]] : prop.targetMeta.getPrimaryProps())
1701
+ : [];
1702
+ targetProps.map(targetProp => {
1703
+ prop.length ??= targetProp.length;
1704
+ prop.precision ??= targetProp.precision;
1705
+ prop.scale ??= targetProp.scale;
1231
1706
  });
1232
1707
  if (prop.kind === ReferenceKind.SCALAR && (prop.type == null || prop.type === 'object') && prop.columnTypes?.[0]) {
1233
1708
  delete prop.type;
@@ -1240,8 +1715,7 @@ export class MetadataDiscovery {
1240
1715
  if (prop.kind === ReferenceKind.SCALAR) {
1241
1716
  const mappedType = this.getMappedType(prop);
1242
1717
  const SCALAR_TYPES = ['string', 'number', 'boolean', 'bigint', 'Date', 'Buffer', 'RegExp', 'any', 'unknown'];
1243
- if (mappedType instanceof UnknownType
1244
- && !prop.columnTypes
1718
+ if (mappedType instanceof t.unknown
1245
1719
  // it could be a runtime type from reflect-metadata
1246
1720
  && !SCALAR_TYPES.includes(prop.type)
1247
1721
  // or it might be inferred via ts-morph to some generic type alias
@@ -1254,23 +1728,31 @@ export class MetadataDiscovery {
1254
1728
  }
1255
1729
  return;
1256
1730
  }
1257
- if (prop.kind === ReferenceKind.EMBEDDED && prop.object && !prop.columnTypes) {
1731
+ /* v8 ignore next */
1732
+ if (prop.kind === ReferenceKind.EMBEDDED && prop.object) {
1258
1733
  prop.columnTypes = [this.platform.getJsonDeclarationSQL()];
1259
1734
  return;
1260
1735
  }
1261
- const targetMeta = this.metadata.get(prop.type);
1736
+ const targetMeta = prop.targetMeta;
1262
1737
  prop.columnTypes = [];
1263
- for (const pk of targetMeta.getPrimaryProps()) {
1264
- this.initCustomType(targetMeta, pk);
1265
- this.initColumnType(pk);
1266
- const mappedType = this.getMappedType(pk);
1267
- let columnTypes = pk.columnTypes;
1268
- if (pk.autoincrement) {
1269
- columnTypes = [mappedType.getColumnType({ ...pk, autoincrement: false }, this.platform)];
1738
+ // Use targetKey property if specified, otherwise use primary key properties
1739
+ const referencedProps = prop.targetKey
1740
+ ? [targetMeta.properties[prop.targetKey]]
1741
+ : targetMeta.getPrimaryProps();
1742
+ if (prop.polymorphic && prop.polymorphTargets) {
1743
+ prop.columnTypes.push(this.platform.getVarcharTypeDeclarationSQL(prop));
1744
+ }
1745
+ for (const referencedProp of referencedProps) {
1746
+ this.initCustomType(targetMeta, referencedProp);
1747
+ this.initColumnType(referencedProp);
1748
+ const mappedType = this.getMappedType(referencedProp);
1749
+ let columnTypes = referencedProp.columnTypes;
1750
+ if (referencedProp.autoincrement) {
1751
+ columnTypes = [mappedType.getColumnType({ ...referencedProp, autoincrement: false }, this.platform)];
1270
1752
  }
1271
1753
  prop.columnTypes.push(...columnTypes);
1272
- if (!targetMeta.compositePK) {
1273
- prop.customType = pk.customType;
1754
+ if (!targetMeta.compositePK || prop.targetKey) {
1755
+ prop.customType = referencedProp.customType;
1274
1756
  }
1275
1757
  }
1276
1758
  }
@@ -1284,7 +1766,7 @@ export class MetadataDiscovery {
1284
1766
  t = 'enum';
1285
1767
  }
1286
1768
  else if (prop.enum) {
1287
- t = prop.items?.every(item => Utils.isString(item)) ? 'enum' : 'tinyint';
1769
+ t = prop.items?.every(item => typeof item === 'string') ? 'enum' : 'tinyint';
1288
1770
  }
1289
1771
  if (t === 'Date') {
1290
1772
  t = 'datetime';
@@ -1308,8 +1790,7 @@ export class MetadataDiscovery {
1308
1790
  return;
1309
1791
  }
1310
1792
  if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
1311
- const meta2 = this.metadata.get(prop.type);
1312
- prop.unsigned = meta2.getPrimaryProps().some(pk => {
1793
+ prop.unsigned = prop.targetMeta.getPrimaryProps().some(pk => {
1313
1794
  this.initUnsigned(pk);
1314
1795
  return pk.unsigned;
1315
1796
  });
@@ -1323,30 +1804,6 @@ export class MetadataDiscovery {
1323
1804
  prop.index ??= true;
1324
1805
  }
1325
1806
  }
1326
- async getEntityClassOrSchema(path, name) {
1327
- const exports = await Utils.dynamicImport(path);
1328
- const targets = Object.values(exports)
1329
- .filter(item => item instanceof EntitySchema || (item instanceof Function && MetadataStorage.isKnownEntity(item.name)));
1330
- // ignore class implementations that are linked from an EntitySchema
1331
- for (const item of targets) {
1332
- if (item instanceof EntitySchema) {
1333
- targets.forEach((item2, idx) => {
1334
- if (item.meta.class === item2) {
1335
- targets.splice(idx, 1);
1336
- }
1337
- });
1338
- }
1339
- }
1340
- if (targets.length > 0) {
1341
- return targets;
1342
- }
1343
- const target = exports.default ?? exports[name];
1344
- /* v8 ignore next 3 */
1345
- if (!target) {
1346
- throw MetadataError.entityNotFound(name, path.replace(this.config.get('baseDir'), '.'));
1347
- }
1348
- return [target];
1349
- }
1350
1807
  shouldForceConstructorUsage(meta) {
1351
1808
  const forceConstructor = this.config.get('forceEntityConstructor');
1352
1809
  if (Array.isArray(forceConstructor)) {