@mikro-orm/core 7.0.0-dev.31 → 7.0.0-dev.311

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