@mikro-orm/core 7.0.0-dev.99 → 7.0.0-rc.1
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.
- package/EntityManager.d.ts +34 -17
- package/EntityManager.js +95 -103
- package/MikroORM.d.ts +5 -5
- package/MikroORM.js +25 -20
- package/cache/FileCacheAdapter.js +11 -3
- package/connections/Connection.d.ts +3 -2
- package/connections/Connection.js +4 -3
- package/drivers/DatabaseDriver.d.ts +11 -11
- package/drivers/DatabaseDriver.js +91 -25
- package/drivers/IDatabaseDriver.d.ts +50 -20
- package/entity/BaseEntity.d.ts +61 -1
- package/entity/Collection.d.ts +8 -1
- package/entity/Collection.js +12 -13
- package/entity/EntityAssigner.js +9 -9
- package/entity/EntityFactory.d.ts +6 -1
- package/entity/EntityFactory.js +40 -22
- package/entity/EntityHelper.d.ts +2 -2
- package/entity/EntityHelper.js +27 -4
- package/entity/EntityLoader.d.ts +5 -4
- package/entity/EntityLoader.js +193 -80
- package/entity/EntityRepository.d.ts +27 -7
- package/entity/EntityRepository.js +8 -2
- package/entity/PolymorphicRef.d.ts +12 -0
- package/entity/PolymorphicRef.js +18 -0
- package/entity/WrappedEntity.d.ts +2 -2
- package/entity/WrappedEntity.js +1 -1
- package/entity/defineEntity.d.ts +89 -50
- package/entity/defineEntity.js +12 -0
- package/entity/index.d.ts +1 -0
- package/entity/index.js +1 -0
- package/entity/utils.d.ts +6 -1
- package/entity/utils.js +33 -0
- package/entity/validators.js +2 -2
- package/enums.d.ts +2 -2
- package/enums.js +1 -0
- package/errors.d.ts +16 -8
- package/errors.js +40 -13
- package/hydration/ObjectHydrator.js +63 -21
- package/index.d.ts +1 -1
- package/logging/colors.d.ts +1 -1
- package/logging/colors.js +7 -6
- package/logging/inspect.js +1 -6
- package/metadata/EntitySchema.d.ts +43 -13
- package/metadata/EntitySchema.js +82 -27
- package/metadata/MetadataDiscovery.d.ts +60 -3
- package/metadata/MetadataDiscovery.js +665 -154
- package/metadata/MetadataProvider.js +3 -1
- package/metadata/MetadataStorage.d.ts +13 -6
- package/metadata/MetadataStorage.js +64 -19
- package/metadata/MetadataValidator.d.ts +32 -2
- package/metadata/MetadataValidator.js +196 -31
- package/metadata/discover-entities.js +5 -5
- package/metadata/types.d.ts +111 -14
- package/naming-strategy/AbstractNamingStrategy.d.ts +11 -3
- package/naming-strategy/AbstractNamingStrategy.js +12 -0
- package/naming-strategy/EntityCaseNamingStrategy.d.ts +3 -3
- package/naming-strategy/EntityCaseNamingStrategy.js +6 -5
- package/naming-strategy/MongoNamingStrategy.d.ts +3 -3
- package/naming-strategy/MongoNamingStrategy.js +6 -6
- package/naming-strategy/NamingStrategy.d.ts +17 -3
- package/naming-strategy/UnderscoreNamingStrategy.d.ts +3 -3
- package/naming-strategy/UnderscoreNamingStrategy.js +6 -6
- package/package.json +2 -2
- package/platforms/Platform.d.ts +4 -2
- package/platforms/Platform.js +5 -2
- package/serialization/EntitySerializer.d.ts +3 -0
- package/serialization/EntitySerializer.js +15 -13
- package/serialization/EntityTransformer.js +6 -6
- package/serialization/SerializationContext.d.ts +6 -6
- package/typings.d.ts +325 -110
- package/typings.js +84 -17
- package/unit-of-work/ChangeSet.d.ts +4 -3
- package/unit-of-work/ChangeSet.js +2 -3
- package/unit-of-work/ChangeSetComputer.d.ts +3 -6
- package/unit-of-work/ChangeSetComputer.js +34 -13
- package/unit-of-work/ChangeSetPersister.d.ts +12 -10
- package/unit-of-work/ChangeSetPersister.js +55 -25
- package/unit-of-work/CommitOrderCalculator.d.ts +12 -10
- package/unit-of-work/CommitOrderCalculator.js +13 -13
- package/unit-of-work/IdentityMap.d.ts +12 -0
- package/unit-of-work/IdentityMap.js +39 -1
- package/unit-of-work/UnitOfWork.d.ts +21 -3
- package/unit-of-work/UnitOfWork.js +203 -56
- package/utils/AbstractSchemaGenerator.js +17 -8
- package/utils/AsyncContext.d.ts +6 -0
- package/utils/AsyncContext.js +42 -0
- package/utils/Configuration.d.ts +52 -11
- package/utils/Configuration.js +12 -8
- package/utils/Cursor.js +21 -8
- package/utils/DataloaderUtils.js +13 -11
- package/utils/EntityComparator.d.ts +14 -7
- package/utils/EntityComparator.js +132 -46
- package/utils/QueryHelper.d.ts +16 -6
- package/utils/QueryHelper.js +53 -18
- package/utils/RawQueryFragment.d.ts +28 -23
- package/utils/RawQueryFragment.js +34 -56
- package/utils/RequestContext.js +2 -2
- package/utils/TransactionContext.js +2 -2
- package/utils/TransactionManager.js +1 -1
- package/utils/Utils.d.ts +7 -26
- package/utils/Utils.js +25 -79
- package/utils/clone.js +7 -21
- package/utils/env-vars.d.ts +4 -0
- package/utils/env-vars.js +13 -3
- package/utils/fs-utils.d.ts +21 -0
- package/utils/fs-utils.js +106 -11
- package/utils/upsert-utils.d.ts +4 -4
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { EntityMetadata, } from '../typings.js';
|
|
2
|
-
import { Utils } from '../utils/Utils.js';
|
|
2
|
+
import { compareArrays, Utils } from '../utils/Utils.js';
|
|
3
|
+
import { QueryHelper } from '../utils/QueryHelper.js';
|
|
3
4
|
import { MetadataValidator } from './MetadataValidator.js';
|
|
5
|
+
import { MetadataProvider } from './MetadataProvider.js';
|
|
4
6
|
import { MetadataStorage } from './MetadataStorage.js';
|
|
5
7
|
import { EntitySchema } from './EntitySchema.js';
|
|
6
8
|
import { Cascade, ReferenceKind } from '../enums.js';
|
|
7
9
|
import { MetadataError } from '../errors.js';
|
|
8
10
|
import { t, Type } from '../types/index.js';
|
|
9
11
|
import { colors } from '../logging/colors.js';
|
|
10
|
-
import { raw,
|
|
12
|
+
import { raw, Raw } from '../utils/RawQueryFragment.js';
|
|
13
|
+
import { BaseEntity } from '../entity/BaseEntity.js';
|
|
11
14
|
export class MetadataDiscovery {
|
|
12
15
|
metadata;
|
|
13
16
|
platform;
|
|
@@ -30,7 +33,8 @@ export class MetadataDiscovery {
|
|
|
30
33
|
async discover(preferTs = true) {
|
|
31
34
|
this.discovered.length = 0;
|
|
32
35
|
const startTime = Date.now();
|
|
33
|
-
this.
|
|
36
|
+
const suffix = this.metadataProvider.constructor === MetadataProvider ? '' : `, using ${colors.cyan(this.metadataProvider.constructor.name)}`;
|
|
37
|
+
this.logger.log('discovery', `ORM entity discovery started${suffix}`);
|
|
34
38
|
await this.findEntities(preferTs);
|
|
35
39
|
for (const meta of this.discovered) {
|
|
36
40
|
/* v8 ignore next */
|
|
@@ -47,7 +51,8 @@ export class MetadataDiscovery {
|
|
|
47
51
|
discoverSync() {
|
|
48
52
|
this.discovered.length = 0;
|
|
49
53
|
const startTime = Date.now();
|
|
50
|
-
this.
|
|
54
|
+
const suffix = this.metadataProvider.constructor === MetadataProvider ? '' : `, using ${colors.cyan(this.metadataProvider.constructor.name)}`;
|
|
55
|
+
this.logger.log('discovery', `ORM entity discovery started${suffix} in sync mode`);
|
|
51
56
|
const refs = this.config.get('entities');
|
|
52
57
|
this.discoverReferences(refs);
|
|
53
58
|
for (const meta of this.discovered) {
|
|
@@ -62,9 +67,6 @@ export class MetadataDiscovery {
|
|
|
62
67
|
void this.config.get('discovery').afterDiscovered?.(storage, this.platform);
|
|
63
68
|
return storage;
|
|
64
69
|
}
|
|
65
|
-
validateDiscovered(metadata) {
|
|
66
|
-
return this.validator.validateDiscovered(metadata, this.config.get('discovery'));
|
|
67
|
-
}
|
|
68
70
|
mapDiscoveredEntities() {
|
|
69
71
|
const discovered = new MetadataStorage();
|
|
70
72
|
this.discovered
|
|
@@ -72,8 +74,14 @@ export class MetadataDiscovery {
|
|
|
72
74
|
.sort((a, b) => b.root.name.localeCompare(a.root.name))
|
|
73
75
|
.forEach(meta => {
|
|
74
76
|
this.platform.validateMetadata(meta);
|
|
75
|
-
discovered.set(meta.
|
|
77
|
+
discovered.set(meta.class, meta);
|
|
76
78
|
});
|
|
79
|
+
for (const meta of discovered) {
|
|
80
|
+
meta.root = discovered.get(meta.root.class);
|
|
81
|
+
if (meta.inheritanceType === 'tpt') {
|
|
82
|
+
this.computeTPTOwnProps(meta);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
77
85
|
return discovered;
|
|
78
86
|
}
|
|
79
87
|
initAccessors(meta) {
|
|
@@ -117,35 +125,48 @@ export class MetadataDiscovery {
|
|
|
117
125
|
// sort so we discover entities first to get around issues with nested embeddables
|
|
118
126
|
filtered.sort((a, b) => !a.embeddable === !b.embeddable ? 0 : (a.embeddable ? 1 : -1));
|
|
119
127
|
filtered.forEach(meta => this.initSingleTableInheritance(meta, filtered));
|
|
128
|
+
filtered.forEach(meta => this.initTPTRelationships(meta, filtered));
|
|
120
129
|
filtered.forEach(meta => this.defineBaseEntityProperties(meta));
|
|
121
|
-
filtered.forEach(meta =>
|
|
130
|
+
filtered.forEach(meta => {
|
|
131
|
+
const newMeta = EntitySchema.fromMetadata(meta).init().meta;
|
|
132
|
+
return this.metadata.set(newMeta.class, newMeta);
|
|
133
|
+
});
|
|
122
134
|
filtered.forEach(meta => this.initAutoincrement(meta));
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
135
|
+
const forEachProp = (cb) => {
|
|
136
|
+
filtered.forEach(meta => Object.values(meta.properties).forEach(prop => cb(meta, prop)));
|
|
137
|
+
};
|
|
138
|
+
forEachProp((m, p) => this.initFactoryField(m, p));
|
|
139
|
+
forEachProp((m, p) => this.initPolymorphicRelation(m, p, filtered));
|
|
140
|
+
forEachProp((_m, p) => this.initRelation(p));
|
|
141
|
+
forEachProp((m, p) => this.initEmbeddables(m, p));
|
|
142
|
+
forEachProp((_m, p) => this.initFieldName(p));
|
|
143
|
+
filtered.forEach(meta => this.finalizeTPTInheritance(meta, filtered));
|
|
144
|
+
filtered.forEach(meta => this.computeTPTOwnProps(meta));
|
|
145
|
+
forEachProp((m, p) => this.initVersionProperty(m, p));
|
|
146
|
+
forEachProp((m, p) => this.initCustomType(m, p));
|
|
147
|
+
forEachProp((m, p) => this.initGeneratedColumn(m, p));
|
|
129
148
|
filtered.forEach(meta => this.initAutoincrement(meta)); // once again after we init custom types
|
|
130
149
|
filtered.forEach(meta => this.initCheckConstraints(meta));
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
filtered.forEach(meta => Object.values(meta.properties).forEach(prop => this.initIndexes(meta, prop)));
|
|
150
|
+
forEachProp((_m, p) => {
|
|
151
|
+
this.initDefaultValue(p);
|
|
152
|
+
this.inferTypeFromDefault(p);
|
|
153
|
+
this.initRelation(p);
|
|
154
|
+
this.initColumnType(p);
|
|
155
|
+
});
|
|
156
|
+
forEachProp((m, p) => this.initIndexes(m, p));
|
|
139
157
|
filtered.forEach(meta => this.autoWireBidirectionalProperties(meta));
|
|
140
|
-
filtered.forEach(meta => this.findReferencingProperties(meta, filtered));
|
|
141
158
|
for (const meta of filtered) {
|
|
142
159
|
discovered.push(...this.processEntity(meta));
|
|
143
160
|
}
|
|
144
161
|
discovered.forEach(meta => meta.sync(true));
|
|
145
162
|
this.metadataProvider.combineCache();
|
|
146
163
|
return discovered.map(meta => {
|
|
147
|
-
meta = this.metadata.get(meta.
|
|
164
|
+
meta = this.metadata.get(meta.class);
|
|
148
165
|
meta.sync(true);
|
|
166
|
+
this.findReferencingProperties(meta, filtered);
|
|
167
|
+
if (meta.inheritanceType === 'tpt') {
|
|
168
|
+
this.computeTPTOwnProps(meta);
|
|
169
|
+
}
|
|
149
170
|
return meta;
|
|
150
171
|
});
|
|
151
172
|
}
|
|
@@ -153,15 +174,19 @@ export class MetadataDiscovery {
|
|
|
153
174
|
const { entities, entitiesTs, baseDir } = this.config.getAll();
|
|
154
175
|
const targets = (preferTs && entitiesTs.length > 0) ? entitiesTs : entities;
|
|
155
176
|
const processed = [];
|
|
177
|
+
const paths = [];
|
|
156
178
|
for (const entity of targets) {
|
|
157
179
|
if (typeof entity === 'string') {
|
|
158
|
-
|
|
159
|
-
processed.push(...await discoverEntities(entity, { baseDir }));
|
|
180
|
+
paths.push(entity);
|
|
160
181
|
}
|
|
161
182
|
else {
|
|
162
183
|
processed.push(entity);
|
|
163
184
|
}
|
|
164
185
|
}
|
|
186
|
+
if (paths.length > 0) {
|
|
187
|
+
const { discoverEntities } = await import('@mikro-orm/core/file-discovery');
|
|
188
|
+
processed.push(...await discoverEntities(paths, { baseDir }));
|
|
189
|
+
}
|
|
165
190
|
return this.discoverReferences(processed);
|
|
166
191
|
}
|
|
167
192
|
discoverMissingTargets() {
|
|
@@ -171,17 +196,22 @@ export class MetadataDiscovery {
|
|
|
171
196
|
.replace(/\((.*)\)/, '$1'); // unwrap union types
|
|
172
197
|
const missing = [];
|
|
173
198
|
this.discovered.forEach(meta => Object.values(meta.properties).forEach(prop => {
|
|
174
|
-
if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.pivotEntity
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
199
|
+
if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.pivotEntity) {
|
|
200
|
+
const pivotEntity = prop.pivotEntity;
|
|
201
|
+
const target = typeof pivotEntity === 'function' && !pivotEntity.prototype
|
|
202
|
+
? pivotEntity()
|
|
203
|
+
: pivotEntity;
|
|
204
|
+
if (!this.discovered.find(m => m.className === Utils.className(target))) {
|
|
205
|
+
missing.push(target);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (prop.kind !== ReferenceKind.SCALAR) {
|
|
209
|
+
const target = typeof prop.entity === 'function' && !prop.entity.prototype
|
|
182
210
|
? prop.entity()
|
|
183
211
|
: prop.type;
|
|
184
|
-
|
|
212
|
+
if (!unwrap(prop.type).split(/ ?\| ?/).every(type => this.discovered.find(m => m.className === type))) {
|
|
213
|
+
missing.push(...Utils.asArray(target));
|
|
214
|
+
}
|
|
185
215
|
}
|
|
186
216
|
}));
|
|
187
217
|
if (missing.length > 0) {
|
|
@@ -190,10 +220,15 @@ export class MetadataDiscovery {
|
|
|
190
220
|
}
|
|
191
221
|
tryDiscoverTargets(targets) {
|
|
192
222
|
for (const target of targets) {
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
223
|
+
const schema = target instanceof EntitySchema ? target : undefined;
|
|
224
|
+
const isDiscoverable = typeof target === 'function' || schema;
|
|
225
|
+
if (isDiscoverable && target.name) {
|
|
226
|
+
// Get the actual class for EntitySchema, or use target directly for classes
|
|
227
|
+
const targetClass = schema ? schema.meta.class : target;
|
|
228
|
+
if (!this.metadata.has(targetClass)) {
|
|
229
|
+
this.discoverReferences([target], false);
|
|
230
|
+
this.discoverMissingTargets();
|
|
231
|
+
}
|
|
197
232
|
}
|
|
198
233
|
}
|
|
199
234
|
}
|
|
@@ -205,16 +240,16 @@ export class MetadataDiscovery {
|
|
|
205
240
|
}
|
|
206
241
|
const schema = this.getSchema(entity);
|
|
207
242
|
const meta = schema.init().meta;
|
|
208
|
-
this.metadata.set(meta.
|
|
243
|
+
this.metadata.set(meta.class, meta);
|
|
209
244
|
found.push(schema);
|
|
210
245
|
}
|
|
211
246
|
// discover parents (base entities) automatically
|
|
212
247
|
for (const meta of this.metadata) {
|
|
213
248
|
let parent = meta.extends;
|
|
214
|
-
if (parent instanceof EntitySchema && !this.metadata.has(parent.meta.
|
|
249
|
+
if (parent instanceof EntitySchema && !this.metadata.has(parent.init().meta.class)) {
|
|
215
250
|
this.discoverReferences([parent], false);
|
|
216
251
|
}
|
|
217
|
-
if (typeof parent === 'function' && parent.name && !this.metadata.has(parent
|
|
252
|
+
if (typeof parent === 'function' && parent.name && !this.metadata.has(parent)) {
|
|
218
253
|
this.discoverReferences([parent], false);
|
|
219
254
|
}
|
|
220
255
|
/* v8 ignore next */
|
|
@@ -222,7 +257,8 @@ export class MetadataDiscovery {
|
|
|
222
257
|
continue;
|
|
223
258
|
}
|
|
224
259
|
parent = Object.getPrototypeOf(meta.class);
|
|
225
|
-
if
|
|
260
|
+
// Skip if parent is the auto-generated base class for the same entity (from setClass usage)
|
|
261
|
+
if (parent.name !== '' && parent.name !== meta.className && !this.metadata.has(parent) && parent !== BaseEntity) {
|
|
226
262
|
this.discoverReferences([parent], false);
|
|
227
263
|
}
|
|
228
264
|
}
|
|
@@ -231,14 +267,14 @@ export class MetadataDiscovery {
|
|
|
231
267
|
}
|
|
232
268
|
this.discoverMissingTargets();
|
|
233
269
|
if (validate) {
|
|
234
|
-
this.validateDiscovered(this.discovered);
|
|
270
|
+
this.validator.validateDiscovered(this.discovered, this.config.get('discovery'));
|
|
235
271
|
}
|
|
236
272
|
return this.discovered.filter(meta => found.find(m => m.name === meta.className));
|
|
237
273
|
}
|
|
238
|
-
reset(
|
|
239
|
-
const exists = this.discovered.findIndex(m => m.className === className);
|
|
274
|
+
reset(entityName) {
|
|
275
|
+
const exists = this.discovered.findIndex(m => m.class === entityName || m.className === Utils.className(entityName));
|
|
240
276
|
if (exists !== -1) {
|
|
241
|
-
this.metadata.reset(this.discovered[exists].
|
|
277
|
+
this.metadata.reset(this.discovered[exists].class);
|
|
242
278
|
this.discovered.splice(exists, 1);
|
|
243
279
|
}
|
|
244
280
|
}
|
|
@@ -253,23 +289,25 @@ export class MetadataDiscovery {
|
|
|
253
289
|
const path = entity[MetadataStorage.PATH_SYMBOL];
|
|
254
290
|
if (path) {
|
|
255
291
|
const meta = Utils.copy(MetadataStorage.getMetadata(entity.name, path), false);
|
|
256
|
-
meta.path =
|
|
257
|
-
this.metadata.set(entity
|
|
292
|
+
meta.path = path;
|
|
293
|
+
this.metadata.set(entity, meta);
|
|
258
294
|
}
|
|
259
|
-
const exists = this.metadata.has(entity
|
|
260
|
-
const meta = this.metadata.get(entity
|
|
295
|
+
const exists = this.metadata.has(entity);
|
|
296
|
+
const meta = this.metadata.get(entity, true);
|
|
261
297
|
meta.abstract ??= !(exists && meta.name);
|
|
262
298
|
const schema = EntitySchema.fromMetadata(meta);
|
|
263
299
|
schema.setClass(entity);
|
|
264
300
|
return schema;
|
|
265
301
|
}
|
|
266
302
|
getRootEntity(meta) {
|
|
267
|
-
const base = meta.extends && this.metadata.find(
|
|
303
|
+
const base = meta.extends && this.metadata.find(meta.extends);
|
|
268
304
|
if (!base || base === meta) { // make sure we do not fall into infinite loop
|
|
269
305
|
return meta;
|
|
270
306
|
}
|
|
271
307
|
const root = this.getRootEntity(base);
|
|
272
|
-
|
|
308
|
+
// For STI or TPT, use the root entity.
|
|
309
|
+
// Check both `inheritanceType` (set during discovery) and raw `inheritance` option (set before discovery).
|
|
310
|
+
if (root.discriminatorColumn || root.inheritanceType || root.inheritance === 'tpt') {
|
|
273
311
|
return root;
|
|
274
312
|
}
|
|
275
313
|
return meta;
|
|
@@ -279,7 +317,7 @@ export class MetadataDiscovery {
|
|
|
279
317
|
const path = meta.path;
|
|
280
318
|
this.logger.log('discovery', `- processing entity ${colors.cyan(meta.className)}${colors.grey(path ? ` (${path})` : '')}`);
|
|
281
319
|
const root = this.getRootEntity(meta);
|
|
282
|
-
schema.meta.path =
|
|
320
|
+
schema.meta.path = meta.path;
|
|
283
321
|
const cache = this.metadataProvider.getCachedMetadata(meta, root);
|
|
284
322
|
if (cache) {
|
|
285
323
|
this.logger.log('discovery', `- using cached metadata for entity ${colors.cyan(meta.className)}`);
|
|
@@ -292,9 +330,9 @@ export class MetadataDiscovery {
|
|
|
292
330
|
}
|
|
293
331
|
// if the definition is using EntitySchema we still want it to go through the metadata provider to validate no types are missing
|
|
294
332
|
this.metadataProvider.loadEntityMetadata(meta);
|
|
295
|
-
if (!meta.
|
|
333
|
+
if (!meta.tableName && meta.name) {
|
|
296
334
|
const entityName = root.discriminatorColumn ? root.name : meta.name;
|
|
297
|
-
meta.
|
|
335
|
+
meta.tableName = this.namingStrategy.classToTableName(entityName);
|
|
298
336
|
}
|
|
299
337
|
this.metadataProvider.saveToCache(meta);
|
|
300
338
|
meta.root = root;
|
|
@@ -326,6 +364,12 @@ export class MetadataDiscovery {
|
|
|
326
364
|
if (!prop.joinColumns || !prop.columnTypes || prop.ownColumns || ![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
|
|
327
365
|
continue;
|
|
328
366
|
}
|
|
367
|
+
// For polymorphic relations, ownColumns should include all fieldNames
|
|
368
|
+
// (discriminator + ID columns) since they are all owned by this relation
|
|
369
|
+
if (prop.polymorphic) {
|
|
370
|
+
prop.ownColumns = prop.fieldNames;
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
329
373
|
if (prop.joinColumns.length > 1) {
|
|
330
374
|
prop.ownColumns = prop.joinColumns.filter(col => {
|
|
331
375
|
return !meta.props.find(p => p.name !== prop.name && (!p.fieldNames || p.fieldNames.includes(col)));
|
|
@@ -356,30 +400,30 @@ export class MetadataDiscovery {
|
|
|
356
400
|
if (prop.kind === ReferenceKind.SCALAR || prop.kind === ReferenceKind.EMBEDDED) {
|
|
357
401
|
prop.fieldNames = [this.namingStrategy.propertyToColumnName(prop.name, object)];
|
|
358
402
|
}
|
|
359
|
-
else if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
|
|
403
|
+
else if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && !prop.polymorphic) {
|
|
360
404
|
prop.fieldNames = this.initManyToOneFieldName(prop, prop.name);
|
|
361
405
|
}
|
|
362
406
|
else if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.owner) {
|
|
363
407
|
prop.fieldNames = this.initManyToManyFieldName(prop, prop.name);
|
|
364
408
|
}
|
|
365
409
|
}
|
|
366
|
-
initManyToOneFieldName(prop, name) {
|
|
367
|
-
const meta2 =
|
|
410
|
+
initManyToOneFieldName(prop, name, tableName) {
|
|
411
|
+
const meta2 = prop.targetMeta;
|
|
368
412
|
const ret = [];
|
|
369
413
|
for (const primaryKey of meta2.primaryKeys) {
|
|
370
414
|
this.initFieldName(meta2.properties[primaryKey]);
|
|
371
415
|
for (const fieldName of meta2.properties[primaryKey].fieldNames) {
|
|
372
|
-
ret.push(this.namingStrategy.joinKeyColumnName(name, fieldName, meta2.compositePK));
|
|
416
|
+
ret.push(this.namingStrategy.joinKeyColumnName(name, fieldName, meta2.compositePK, tableName));
|
|
373
417
|
}
|
|
374
418
|
}
|
|
375
419
|
return ret;
|
|
376
420
|
}
|
|
377
421
|
initManyToManyFieldName(prop, name) {
|
|
378
|
-
const meta2 =
|
|
422
|
+
const meta2 = prop.targetMeta;
|
|
379
423
|
return meta2.primaryKeys.map(() => this.namingStrategy.propertyToColumnName(name));
|
|
380
424
|
}
|
|
381
425
|
initManyToManyFields(meta, prop) {
|
|
382
|
-
const meta2 =
|
|
426
|
+
const meta2 = prop.targetMeta;
|
|
383
427
|
Utils.defaultValue(prop, 'fixedOrder', !!prop.fixedOrderColumn);
|
|
384
428
|
const pivotMeta = this.metadata.find(prop.pivotEntity);
|
|
385
429
|
const props = Object.values(pivotMeta?.properties ?? {});
|
|
@@ -400,35 +444,77 @@ export class MetadataDiscovery {
|
|
|
400
444
|
prop.inverseJoinColumns ??= second.fieldNames;
|
|
401
445
|
}
|
|
402
446
|
if (!prop.pivotTable && prop.owner && this.platform.usesPivotTable()) {
|
|
403
|
-
prop.pivotTable = this.namingStrategy.joinTableName(meta.
|
|
447
|
+
prop.pivotTable = this.namingStrategy.joinTableName(meta.className, meta2.tableName, prop.name, meta.tableName);
|
|
404
448
|
}
|
|
405
449
|
if (prop.mappedBy) {
|
|
406
450
|
const prop2 = meta2.properties[prop.mappedBy];
|
|
407
451
|
this.initManyToManyFields(meta2, prop2);
|
|
408
452
|
prop.pivotTable = prop2.pivotTable;
|
|
409
|
-
prop.pivotEntity = prop2.pivotEntity
|
|
453
|
+
prop.pivotEntity = prop2.pivotEntity;
|
|
410
454
|
prop.fixedOrder = prop2.fixedOrder;
|
|
411
455
|
prop.fixedOrderColumn = prop2.fixedOrderColumn;
|
|
412
456
|
prop.joinColumns = prop2.inverseJoinColumns;
|
|
413
457
|
prop.inverseJoinColumns = prop2.joinColumns;
|
|
458
|
+
prop.polymorphic = prop2.polymorphic;
|
|
459
|
+
prop.discriminator = prop2.discriminator;
|
|
460
|
+
prop.discriminatorColumn = prop2.discriminatorColumn;
|
|
461
|
+
prop.discriminatorValue = prop2.discriminatorValue;
|
|
414
462
|
}
|
|
415
463
|
prop.referencedColumnNames ??= Utils.flatten(meta.primaryKeys.map(primaryKey => meta.properties[primaryKey].fieldNames));
|
|
416
|
-
|
|
417
|
-
prop.
|
|
464
|
+
// For polymorphic M:N, use discriminator base name for FK column (e.g., taggable_id instead of post_id)
|
|
465
|
+
if (prop.polymorphic && prop.discriminator) {
|
|
466
|
+
prop.joinColumns ??= prop.referencedColumnNames.map(referencedColumnName => this.namingStrategy.joinKeyColumnName(prop.discriminator, referencedColumnName, prop.referencedColumnNames.length > 1));
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
const ownerTableName = this.isExplicitTableName(meta.root) ? meta.root.tableName : undefined;
|
|
470
|
+
prop.joinColumns ??= prop.referencedColumnNames.map(referencedColumnName => this.namingStrategy.joinKeyColumnName(meta.root.className, referencedColumnName, meta.compositePK, ownerTableName));
|
|
471
|
+
}
|
|
472
|
+
const inverseTableName = this.isExplicitTableName(meta2.root) ? meta2.root.tableName : undefined;
|
|
473
|
+
prop.inverseJoinColumns ??= this.initManyToOneFieldName(prop, meta2.root.className, inverseTableName);
|
|
474
|
+
}
|
|
475
|
+
isExplicitTableName(meta) {
|
|
476
|
+
return meta.tableName !== this.namingStrategy.classToTableName(meta.className);
|
|
418
477
|
}
|
|
419
478
|
initManyToOneFields(prop) {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
479
|
+
if (prop.polymorphic && prop.polymorphTargets) {
|
|
480
|
+
const fieldNames1 = prop.targetMeta.getPrimaryProps().flatMap(pk => pk.fieldNames);
|
|
481
|
+
const idColumns = fieldNames1.map(fieldName => this.namingStrategy.joinKeyColumnName(prop.discriminator, fieldName, fieldNames1.length > 1));
|
|
482
|
+
prop.fieldNames ??= [prop.discriminatorColumn, ...idColumns];
|
|
483
|
+
prop.joinColumns ??= idColumns;
|
|
484
|
+
prop.referencedColumnNames ??= fieldNames1;
|
|
485
|
+
prop.referencedTableName ??= prop.targetMeta.tableName;
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
const meta2 = prop.targetMeta;
|
|
489
|
+
let fieldNames;
|
|
490
|
+
// If targetKey is specified, use that property's field names instead of PKs
|
|
491
|
+
if (prop.targetKey) {
|
|
492
|
+
const targetProp = meta2.properties[prop.targetKey];
|
|
493
|
+
fieldNames = targetProp.fieldNames;
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
fieldNames = Utils.flatten(meta2.primaryKeys.map(primaryKey => meta2.properties[primaryKey].fieldNames));
|
|
497
|
+
}
|
|
498
|
+
Utils.defaultValue(prop, 'referencedTableName', meta2.tableName);
|
|
423
499
|
if (!prop.joinColumns) {
|
|
424
500
|
prop.joinColumns = fieldNames.map(fieldName => this.namingStrategy.joinKeyColumnName(prop.name, fieldName, fieldNames.length > 1));
|
|
425
501
|
}
|
|
426
502
|
if (!prop.referencedColumnNames) {
|
|
427
503
|
prop.referencedColumnNames = fieldNames;
|
|
428
504
|
}
|
|
505
|
+
// Relations to composite PK targets need cascade update by default,
|
|
506
|
+
// since composite PKs are more likely to have mutable components
|
|
507
|
+
if (meta2.compositePK) {
|
|
508
|
+
prop.updateRule ??= 'cascade';
|
|
509
|
+
}
|
|
510
|
+
// Nullable relations default to 'set null' on delete - when the referenced
|
|
511
|
+
// entity is deleted, set the FK to null rather than failing
|
|
512
|
+
if (prop.nullable) {
|
|
513
|
+
prop.deleteRule ??= 'set null';
|
|
514
|
+
}
|
|
429
515
|
}
|
|
430
516
|
initOneToManyFields(prop) {
|
|
431
|
-
const meta2 =
|
|
517
|
+
const meta2 = prop.targetMeta;
|
|
432
518
|
if (!prop.joinColumns) {
|
|
433
519
|
prop.joinColumns = [this.namingStrategy.joinColumnName(prop.name)];
|
|
434
520
|
}
|
|
@@ -441,12 +527,17 @@ export class MetadataDiscovery {
|
|
|
441
527
|
const pks = Object.values(meta.properties).filter(prop => prop.primary);
|
|
442
528
|
meta.primaryKeys = pks.map(prop => prop.name);
|
|
443
529
|
meta.compositePK = pks.length > 1;
|
|
444
|
-
// FK used as PK, we need to cascade
|
|
445
|
-
|
|
446
|
-
|
|
530
|
+
// FK used as PK, we need to cascade - applies to both single FK-as-PK
|
|
531
|
+
// and composite PKs where all PKs are FKs (e.g., pivot-like entities)
|
|
532
|
+
const fkPks = pks.filter(pk => pk.kind !== ReferenceKind.SCALAR);
|
|
533
|
+
if (fkPks.length > 0 && fkPks.length === pks.length) {
|
|
534
|
+
for (const pk of fkPks) {
|
|
535
|
+
pk.deleteRule ??= 'cascade';
|
|
536
|
+
pk.updateRule ??= 'cascade';
|
|
537
|
+
}
|
|
447
538
|
}
|
|
448
539
|
meta.forceConstructor ??= this.shouldForceConstructorUsage(meta);
|
|
449
|
-
this.validator.validateEntityDefinition(this.metadata, meta.
|
|
540
|
+
this.validator.validateEntityDefinition(this.metadata, meta.class, this.config.get('discovery'));
|
|
450
541
|
for (const prop of Object.values(meta.properties)) {
|
|
451
542
|
this.initNullability(prop);
|
|
452
543
|
this.applyNamingStrategy(meta, prop);
|
|
@@ -466,7 +557,14 @@ export class MetadataDiscovery {
|
|
|
466
557
|
if (this.platform.usesPivotTable()) {
|
|
467
558
|
return Object.values(meta.properties)
|
|
468
559
|
.filter(prop => prop.kind === ReferenceKind.MANY_TO_MANY && prop.owner && prop.pivotTable)
|
|
469
|
-
.map(prop =>
|
|
560
|
+
.map(prop => {
|
|
561
|
+
const pivotMeta = this.definePivotTableEntity(meta, prop);
|
|
562
|
+
prop.pivotEntity = pivotMeta.class;
|
|
563
|
+
if (prop.inversedBy) {
|
|
564
|
+
prop.targetMeta.properties[prop.inversedBy].pivotEntity = pivotMeta.class;
|
|
565
|
+
}
|
|
566
|
+
return pivotMeta;
|
|
567
|
+
});
|
|
470
568
|
}
|
|
471
569
|
return [];
|
|
472
570
|
}
|
|
@@ -483,8 +581,11 @@ export class MetadataDiscovery {
|
|
|
483
581
|
['mappedBy', 'inversedBy', 'pivotEntity'].forEach(type => {
|
|
484
582
|
const value = prop[type];
|
|
485
583
|
if (value instanceof Function) {
|
|
486
|
-
const meta2 = this.metadata.get(prop.
|
|
584
|
+
const meta2 = prop.targetMeta ?? this.metadata.get(prop.target);
|
|
487
585
|
prop[type] = value(meta2.properties)?.name;
|
|
586
|
+
if (type === 'pivotEntity' && value) {
|
|
587
|
+
prop[type] = value(meta2.properties);
|
|
588
|
+
}
|
|
488
589
|
if (prop[type] == null) {
|
|
489
590
|
throw MetadataError.fromWrongReference(meta, prop, type);
|
|
490
591
|
}
|
|
@@ -500,9 +601,9 @@ export class MetadataDiscovery {
|
|
|
500
601
|
}
|
|
501
602
|
else if (fks.length >= 2) {
|
|
502
603
|
[first, second] = fks;
|
|
503
|
-
/* v8 ignore next */
|
|
504
604
|
}
|
|
505
605
|
else {
|
|
606
|
+
/* v8 ignore next */
|
|
506
607
|
return [];
|
|
507
608
|
}
|
|
508
609
|
// wrong FK order, first FK needs to point to the owning side
|
|
@@ -516,7 +617,9 @@ export class MetadataDiscovery {
|
|
|
516
617
|
return [first, second];
|
|
517
618
|
}
|
|
518
619
|
definePivotTableEntity(meta, prop) {
|
|
519
|
-
const pivotMeta =
|
|
620
|
+
const pivotMeta = prop.pivotEntity
|
|
621
|
+
? this.metadata.find(prop.pivotEntity)
|
|
622
|
+
: this.metadata.getByClassName(prop.pivotTable, false);
|
|
520
623
|
// ensure inverse side exists so we can join it when populating via pivot tables
|
|
521
624
|
if (!prop.inversedBy && prop.targetMeta) {
|
|
522
625
|
const inverseName = `${meta.className}_${prop.name}__inverse`;
|
|
@@ -525,6 +628,8 @@ export class MetadataDiscovery {
|
|
|
525
628
|
name: inverseName,
|
|
526
629
|
kind: ReferenceKind.MANY_TO_MANY,
|
|
527
630
|
type: meta.className,
|
|
631
|
+
target: meta.class,
|
|
632
|
+
targetMeta: meta,
|
|
528
633
|
mappedBy: prop.name,
|
|
529
634
|
pivotEntity: prop.pivotEntity,
|
|
530
635
|
pivotTable: prop.pivotTable,
|
|
@@ -533,55 +638,190 @@ export class MetadataDiscovery {
|
|
|
533
638
|
};
|
|
534
639
|
this.applyNamingStrategy(prop.targetMeta, inverseProp);
|
|
535
640
|
this.initCustomType(prop.targetMeta, inverseProp);
|
|
536
|
-
this.initRelation(inverseProp);
|
|
537
641
|
prop.targetMeta.properties[inverseName] = inverseProp;
|
|
538
642
|
}
|
|
539
643
|
if (pivotMeta) {
|
|
644
|
+
prop.pivotEntity = pivotMeta.class;
|
|
540
645
|
this.ensureCorrectFKOrderInPivotEntity(pivotMeta, prop);
|
|
646
|
+
if (prop.polymorphic && prop.discriminatorValue) {
|
|
647
|
+
pivotMeta.polymorphicDiscriminatorMap ??= {};
|
|
648
|
+
pivotMeta.polymorphicDiscriminatorMap[prop.discriminatorValue] = meta.class;
|
|
649
|
+
// For composite PK entities sharing a polymorphic pivot table,
|
|
650
|
+
// we need to add columns for each entity type's PKs
|
|
651
|
+
this.addPolymorphicPivotColumns(pivotMeta, meta, prop);
|
|
652
|
+
// Add virtual M:1 relation for this polymorphic owner (for join loading)
|
|
653
|
+
const ownerRelationName = `${prop.discriminator}_${meta.tableName}`;
|
|
654
|
+
if (!pivotMeta.properties[ownerRelationName]) {
|
|
655
|
+
pivotMeta.properties[ownerRelationName] = this.definePolymorphicOwnerRelation(prop, ownerRelationName, meta);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
541
658
|
return pivotMeta;
|
|
542
659
|
}
|
|
543
|
-
const exists = this.metadata.find(prop.pivotTable);
|
|
544
|
-
if (exists) {
|
|
545
|
-
prop.pivotEntity = exists.className;
|
|
546
|
-
return exists;
|
|
547
|
-
}
|
|
548
660
|
let tableName = prop.pivotTable;
|
|
549
661
|
let schemaName;
|
|
550
662
|
if (prop.pivotTable.includes('.')) {
|
|
551
663
|
[schemaName, tableName] = prop.pivotTable.split('.');
|
|
552
664
|
}
|
|
553
665
|
schemaName ??= meta.schema;
|
|
554
|
-
const
|
|
555
|
-
const
|
|
666
|
+
const targetMeta = prop.targetMeta;
|
|
667
|
+
const targetType = targetMeta.className;
|
|
668
|
+
const pivotMeta2 = new EntityMetadata({
|
|
556
669
|
name: prop.pivotTable,
|
|
557
670
|
className: prop.pivotTable,
|
|
558
671
|
collection: tableName,
|
|
559
672
|
schema: schemaName,
|
|
560
673
|
pivotTable: true,
|
|
561
674
|
});
|
|
562
|
-
prop.pivotEntity =
|
|
675
|
+
prop.pivotEntity = pivotMeta2.class;
|
|
563
676
|
if (prop.fixedOrder) {
|
|
564
|
-
const primaryProp = this.defineFixedOrderProperty(prop,
|
|
565
|
-
|
|
677
|
+
const primaryProp = this.defineFixedOrderProperty(prop, targetMeta);
|
|
678
|
+
pivotMeta2.properties[primaryProp.name] = primaryProp;
|
|
566
679
|
}
|
|
567
680
|
else {
|
|
568
|
-
|
|
681
|
+
pivotMeta2.compositePK = true;
|
|
569
682
|
}
|
|
570
683
|
// handle self-referenced m:n with same default field names
|
|
571
684
|
if (meta.className === targetType && prop.joinColumns.every((joinColumn, idx) => joinColumn === prop.inverseJoinColumns[idx])) {
|
|
572
|
-
|
|
573
|
-
|
|
685
|
+
// use tableName only when explicitly provided by user, otherwise use className for backwards compatibility
|
|
686
|
+
const baseName = this.isExplicitTableName(meta) ? meta.tableName : meta.className;
|
|
687
|
+
prop.joinColumns = prop.referencedColumnNames.map(name => this.namingStrategy.joinKeyColumnName(baseName + '_1', name, meta.compositePK));
|
|
688
|
+
prop.inverseJoinColumns = prop.referencedColumnNames.map(name => this.namingStrategy.joinKeyColumnName(baseName + '_2', name, meta.compositePK));
|
|
574
689
|
if (prop.inversedBy) {
|
|
575
|
-
const prop2 =
|
|
690
|
+
const prop2 = targetMeta.properties[prop.inversedBy];
|
|
576
691
|
prop2.inverseJoinColumns = prop.joinColumns;
|
|
577
692
|
prop2.joinColumns = prop.inverseJoinColumns;
|
|
578
693
|
}
|
|
694
|
+
// propagate updated joinColumns to all child entities that inherit this property (STI)
|
|
695
|
+
for (const childMeta of this.discovered.filter(m => m.root === meta && m !== meta)) {
|
|
696
|
+
const childProp = childMeta.properties[prop.name];
|
|
697
|
+
if (childProp) {
|
|
698
|
+
childProp.joinColumns = prop.joinColumns;
|
|
699
|
+
childProp.inverseJoinColumns = prop.inverseJoinColumns;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
// For polymorphic M:N, create discriminator column and polymorphic FK
|
|
704
|
+
if (prop.polymorphic && prop.discriminatorColumn) {
|
|
705
|
+
this.definePolymorphicPivotProperties(pivotMeta2, meta, prop, targetMeta);
|
|
706
|
+
}
|
|
707
|
+
else {
|
|
708
|
+
pivotMeta2.properties[meta.name + '_owner'] = this.definePivotProperty(prop, meta.name + '_owner', meta.class, targetType + '_inverse', true, meta.className === targetType);
|
|
709
|
+
pivotMeta2.properties[targetType + '_inverse'] = this.definePivotProperty(prop, targetType + '_inverse', targetMeta.class, meta.name + '_owner', false, meta.className === targetType);
|
|
579
710
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
711
|
+
return this.metadata.set(pivotMeta2.class, EntitySchema.fromMetadata(pivotMeta2).init().meta);
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Create a scalar property for a pivot table column.
|
|
715
|
+
*/
|
|
716
|
+
createPivotScalarProperty(name, columnTypes, fieldNames = [name], options = {}) {
|
|
717
|
+
return {
|
|
718
|
+
name,
|
|
719
|
+
fieldNames,
|
|
720
|
+
columnTypes,
|
|
721
|
+
type: options.type ?? 'number',
|
|
722
|
+
kind: ReferenceKind.SCALAR,
|
|
723
|
+
primary: options.primary ?? false,
|
|
724
|
+
nullable: options.nullable ?? true,
|
|
725
|
+
...(options.persist !== undefined && { persist: options.persist }),
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Get column types for an entity's primary keys, initializing them if needed.
|
|
730
|
+
*/
|
|
731
|
+
getPrimaryKeyColumnTypes(meta) {
|
|
732
|
+
const columnTypes = [];
|
|
733
|
+
for (const pk of meta.primaryKeys) {
|
|
734
|
+
const pkProp = meta.properties[pk];
|
|
735
|
+
this.initCustomType(meta, pkProp);
|
|
736
|
+
this.initColumnType(pkProp);
|
|
737
|
+
columnTypes.push(...pkProp.columnTypes);
|
|
738
|
+
}
|
|
739
|
+
return columnTypes;
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Add missing FK columns for a polymorphic entity to an existing pivot table.
|
|
743
|
+
*/
|
|
744
|
+
addPolymorphicPivotColumns(pivotMeta, meta, prop) {
|
|
745
|
+
const existingFieldNames = new Set(Object.values(pivotMeta.properties).flatMap(p => p.fieldNames ?? []));
|
|
746
|
+
const columnTypes = this.getPrimaryKeyColumnTypes(meta);
|
|
747
|
+
for (let i = 0; i < prop.joinColumns.length; i++) {
|
|
748
|
+
const joinColumn = prop.joinColumns[i];
|
|
749
|
+
if (!existingFieldNames.has(joinColumn)) {
|
|
750
|
+
pivotMeta.properties[joinColumn] = this.createPivotScalarProperty(joinColumn, [columnTypes[i]]);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Define properties for a polymorphic pivot table.
|
|
756
|
+
*/
|
|
757
|
+
definePolymorphicPivotProperties(pivotMeta, meta, prop, targetMeta) {
|
|
758
|
+
const discriminatorColumn = prop.discriminatorColumn;
|
|
759
|
+
const isCompositePK = meta.compositePK;
|
|
760
|
+
// For composite PK polymorphic M:N, we need fixedOrder (auto-increment PK)
|
|
761
|
+
if (isCompositePK && !prop.fixedOrder) {
|
|
762
|
+
prop.fixedOrder = true;
|
|
763
|
+
const primaryProp = this.defineFixedOrderProperty(prop, targetMeta);
|
|
764
|
+
pivotMeta.properties[primaryProp.name] = primaryProp;
|
|
765
|
+
pivotMeta.compositePK = false;
|
|
766
|
+
}
|
|
767
|
+
const discriminatorProp = this.createPivotScalarProperty(discriminatorColumn, [this.platform.getVarcharTypeDeclarationSQL(prop)], [discriminatorColumn], { type: 'string', primary: !isCompositePK, nullable: false });
|
|
768
|
+
this.initFieldName(discriminatorProp);
|
|
769
|
+
pivotMeta.properties[discriminatorColumn] = discriminatorProp;
|
|
770
|
+
const columnTypes = this.getPrimaryKeyColumnTypes(meta);
|
|
771
|
+
if (isCompositePK) {
|
|
772
|
+
// Create separate properties for each PK column (nullable for other entity types)
|
|
773
|
+
for (let i = 0; i < prop.joinColumns.length; i++) {
|
|
774
|
+
pivotMeta.properties[prop.joinColumns[i]] = this.createPivotScalarProperty(prop.joinColumns[i], [columnTypes[i]]);
|
|
775
|
+
}
|
|
776
|
+
// Virtual property combining all columns (for compatibility)
|
|
777
|
+
pivotMeta.properties[prop.discriminator] = this.createPivotScalarProperty(prop.discriminator, columnTypes, [...prop.joinColumns], { type: meta.className, persist: false });
|
|
778
|
+
}
|
|
779
|
+
else {
|
|
780
|
+
pivotMeta.properties[prop.discriminator] = this.createPivotScalarProperty(prop.discriminator, columnTypes, [...prop.joinColumns], { type: meta.className, primary: true, nullable: false });
|
|
781
|
+
}
|
|
782
|
+
pivotMeta.properties[targetMeta.className + '_inverse'] = this.definePivotProperty(prop, targetMeta.className + '_inverse', targetMeta.class, prop.discriminator, false, false);
|
|
783
|
+
// Create virtual M:1 relation to the polymorphic owner for single-query join loading
|
|
784
|
+
const ownerRelationName = `${prop.discriminator}_${meta.tableName}`;
|
|
785
|
+
pivotMeta.properties[ownerRelationName] = this.definePolymorphicOwnerRelation(prop, ownerRelationName, meta);
|
|
786
|
+
pivotMeta.polymorphicDiscriminatorMap ??= {};
|
|
787
|
+
pivotMeta.polymorphicDiscriminatorMap[prop.discriminatorValue] = meta.class;
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Create a virtual M:1 relation from pivot to a polymorphic owner entity.
|
|
791
|
+
* This enables single-query join loading for inverse-side polymorphic M:N.
|
|
792
|
+
*/
|
|
793
|
+
definePolymorphicOwnerRelation(prop, name, targetMeta) {
|
|
794
|
+
const ret = {
|
|
795
|
+
name,
|
|
796
|
+
type: targetMeta.className,
|
|
797
|
+
target: targetMeta.class,
|
|
798
|
+
kind: ReferenceKind.MANY_TO_ONE,
|
|
799
|
+
nullable: true,
|
|
800
|
+
owner: true,
|
|
801
|
+
primary: false,
|
|
802
|
+
createForeignKeyConstraint: false,
|
|
803
|
+
persist: false,
|
|
804
|
+
index: false,
|
|
805
|
+
};
|
|
806
|
+
ret.targetMeta = targetMeta;
|
|
807
|
+
ret.fieldNames = ret.joinColumns = ret.ownColumns = [...prop.joinColumns];
|
|
808
|
+
ret.referencedColumnNames = [];
|
|
809
|
+
ret.inverseJoinColumns = [];
|
|
810
|
+
for (const primaryKey of targetMeta.primaryKeys) {
|
|
811
|
+
const pkProp = targetMeta.properties[primaryKey];
|
|
812
|
+
ret.referencedColumnNames.push(...pkProp.fieldNames);
|
|
813
|
+
ret.inverseJoinColumns.push(...pkProp.fieldNames);
|
|
814
|
+
ret.length = pkProp.length;
|
|
815
|
+
ret.precision = pkProp.precision;
|
|
816
|
+
ret.scale = pkProp.scale;
|
|
817
|
+
}
|
|
818
|
+
const schema = targetMeta.schema ?? this.config.get('schema') ?? this.platform.getDefaultSchemaName();
|
|
819
|
+
ret.referencedTableName = schema && schema !== '*' ? schema + '.' + targetMeta.tableName : targetMeta.tableName;
|
|
820
|
+
this.initColumnType(ret);
|
|
821
|
+
this.initRelation(ret);
|
|
822
|
+
return ret;
|
|
583
823
|
}
|
|
584
|
-
defineFixedOrderProperty(prop,
|
|
824
|
+
defineFixedOrderProperty(prop, targetMeta) {
|
|
585
825
|
const pk = prop.fixedOrderColumn || this.namingStrategy.referenceColumnName();
|
|
586
826
|
const primaryProp = {
|
|
587
827
|
name: pk,
|
|
@@ -595,7 +835,7 @@ export class MetadataDiscovery {
|
|
|
595
835
|
this.initColumnType(primaryProp);
|
|
596
836
|
prop.fixedOrderColumn = pk;
|
|
597
837
|
if (prop.inversedBy) {
|
|
598
|
-
const prop2 =
|
|
838
|
+
const prop2 = targetMeta.properties[prop.inversedBy];
|
|
599
839
|
prop2.fixedOrder = true;
|
|
600
840
|
prop2.fixedOrderColumn = pk;
|
|
601
841
|
}
|
|
@@ -604,7 +844,8 @@ export class MetadataDiscovery {
|
|
|
604
844
|
definePivotProperty(prop, name, type, inverse, owner, selfReferencing) {
|
|
605
845
|
const ret = {
|
|
606
846
|
name,
|
|
607
|
-
type,
|
|
847
|
+
type: Utils.className(type),
|
|
848
|
+
target: type,
|
|
608
849
|
kind: ReferenceKind.MANY_TO_ONE,
|
|
609
850
|
cascade: [Cascade.ALL],
|
|
610
851
|
fixedOrder: prop.fixedOrder,
|
|
@@ -616,10 +857,9 @@ export class MetadataDiscovery {
|
|
|
616
857
|
deleteRule: prop.deleteRule,
|
|
617
858
|
createForeignKeyConstraint: prop.createForeignKeyConstraint,
|
|
618
859
|
};
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
}
|
|
860
|
+
const defaultRule = selfReferencing && !this.platform.supportsMultipleCascadePaths() ? 'no action' : 'cascade';
|
|
861
|
+
ret.updateRule ??= defaultRule;
|
|
862
|
+
ret.deleteRule ??= defaultRule;
|
|
623
863
|
const meta = this.metadata.get(type);
|
|
624
864
|
ret.targetMeta = meta;
|
|
625
865
|
ret.joinColumns = [];
|
|
@@ -662,7 +902,7 @@ export class MetadataDiscovery {
|
|
|
662
902
|
Object.values(meta.properties)
|
|
663
903
|
.filter(prop => prop.kind !== ReferenceKind.SCALAR && !prop.owner && prop.mappedBy)
|
|
664
904
|
.forEach(prop => {
|
|
665
|
-
const meta2 =
|
|
905
|
+
const meta2 = prop.targetMeta;
|
|
666
906
|
const prop2 = meta2.properties[prop.mappedBy];
|
|
667
907
|
if (prop2 && !prop2.inversedBy) {
|
|
668
908
|
prop2.inversedBy = prop.name;
|
|
@@ -670,7 +910,7 @@ export class MetadataDiscovery {
|
|
|
670
910
|
});
|
|
671
911
|
}
|
|
672
912
|
defineBaseEntityProperties(meta) {
|
|
673
|
-
const base = meta.extends && this.metadata.get(
|
|
913
|
+
const base = meta.extends && this.metadata.get(meta.extends);
|
|
674
914
|
if (!base || base === meta) { // make sure we do not fall into infinite loop
|
|
675
915
|
return 0;
|
|
676
916
|
}
|
|
@@ -760,6 +1000,54 @@ export class MetadataDiscovery {
|
|
|
760
1000
|
polymorphs.forEach(meta => meta.root = embeddable);
|
|
761
1001
|
}
|
|
762
1002
|
}
|
|
1003
|
+
initPolymorphicRelation(meta, prop, discovered) {
|
|
1004
|
+
if (!prop.discriminator && !prop.discriminatorColumn && !prop.discriminatorMap && !Array.isArray(prop.target)) {
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
prop.polymorphic = true;
|
|
1008
|
+
prop.discriminator ??= prop.name;
|
|
1009
|
+
prop.discriminatorColumn ??= this.namingStrategy.discriminatorColumnName(prop.discriminator);
|
|
1010
|
+
prop.createForeignKeyConstraint = false;
|
|
1011
|
+
const isToOne = [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind);
|
|
1012
|
+
if (isToOne) {
|
|
1013
|
+
const types = prop.type.split(/ ?\| ?/);
|
|
1014
|
+
prop.polymorphTargets = discovered.filter(m => types.includes(m.className) && !m.embeddable);
|
|
1015
|
+
prop.targetMeta = prop.polymorphTargets[0];
|
|
1016
|
+
prop.referencedPKs = prop.targetMeta?.primaryKeys;
|
|
1017
|
+
}
|
|
1018
|
+
if (prop.discriminatorMap) {
|
|
1019
|
+
const normalizedMap = {};
|
|
1020
|
+
for (const [key, value] of Object.entries(prop.discriminatorMap)) {
|
|
1021
|
+
const targetMeta = this.metadata.getByClassName(value, false);
|
|
1022
|
+
if (!targetMeta) {
|
|
1023
|
+
throw MetadataError.fromUnknownEntity(value, `${meta.className}.${prop.name} discriminatorMap`);
|
|
1024
|
+
}
|
|
1025
|
+
normalizedMap[key] = targetMeta.class;
|
|
1026
|
+
if (targetMeta.class === meta.class) {
|
|
1027
|
+
prop.discriminatorValue = key;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
prop.discriminatorMap = normalizedMap;
|
|
1031
|
+
}
|
|
1032
|
+
else if (isToOne) {
|
|
1033
|
+
prop.discriminatorMap = {};
|
|
1034
|
+
const tableNameToTarget = new Map();
|
|
1035
|
+
for (const target of prop.polymorphTargets) {
|
|
1036
|
+
const existing = tableNameToTarget.get(target.tableName);
|
|
1037
|
+
if (existing) {
|
|
1038
|
+
throw MetadataError.incompatiblePolymorphicTargets(meta, prop, existing, target, `both use table '${target.tableName}'. Use separate properties instead of a single polymorphic relation.`);
|
|
1039
|
+
}
|
|
1040
|
+
tableNameToTarget.set(target.tableName, target);
|
|
1041
|
+
prop.discriminatorMap[target.tableName] = target.class;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
else {
|
|
1045
|
+
prop.discriminatorValue ??= meta.tableName;
|
|
1046
|
+
if (!prop.discriminatorMap) {
|
|
1047
|
+
prop.discriminatorMap = { [prop.discriminatorValue]: meta.class };
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
763
1051
|
initEmbeddables(meta, embeddedProp, visited = new Set()) {
|
|
764
1052
|
if (embeddedProp.kind !== ReferenceKind.EMBEDDED || visited.has(embeddedProp)) {
|
|
765
1053
|
return;
|
|
@@ -795,7 +1083,7 @@ export class MetadataDiscovery {
|
|
|
795
1083
|
const glue = object ? '~' : '_';
|
|
796
1084
|
for (const prop of Object.values(embeddable.properties)) {
|
|
797
1085
|
const name = (embeddedProp.embeddedPath?.join(glue) ?? embeddedProp.fieldNames[0] + glue) + prop.name;
|
|
798
|
-
meta.properties[name] = Utils.copy(prop
|
|
1086
|
+
meta.properties[name] = Utils.copy(prop);
|
|
799
1087
|
meta.properties[name].name = name;
|
|
800
1088
|
meta.properties[name].embedded = [embeddedProp.name, prop.name];
|
|
801
1089
|
meta.propertyOrder.set(name, (order += 0.01));
|
|
@@ -832,10 +1120,12 @@ export class MetadataDiscovery {
|
|
|
832
1120
|
path = [embeddedProp.fieldNames[0]];
|
|
833
1121
|
}
|
|
834
1122
|
this.initFieldName(prop, true);
|
|
1123
|
+
this.initRelation(prop);
|
|
835
1124
|
path.push(prop.fieldNames[0]);
|
|
836
1125
|
meta.properties[name].fieldNames = prop.fieldNames;
|
|
837
1126
|
meta.properties[name].embeddedPath = path;
|
|
838
|
-
const
|
|
1127
|
+
const targetProp = prop.targetMeta?.getPrimaryProp() ?? prop;
|
|
1128
|
+
const fieldName = raw(this.platform.getSearchJsonPropertySQL(path.join('->'), targetProp.runtimeType ?? targetProp.type, true));
|
|
839
1129
|
meta.properties[name].fieldNameRaw = fieldName.sql; // for querying in SQL drivers
|
|
840
1130
|
meta.properties[name].persist = false; // only virtual as we store the whole object
|
|
841
1131
|
meta.properties[name].userDefined = false; // mark this as a generated/internal property, so we can distinguish from user-defined non-persist properties
|
|
@@ -863,7 +1153,7 @@ export class MetadataDiscovery {
|
|
|
863
1153
|
}
|
|
864
1154
|
initSingleTableInheritance(meta, metadata) {
|
|
865
1155
|
if (meta.root !== meta && !meta.__processed) {
|
|
866
|
-
meta.root = metadata.find(m => m.
|
|
1156
|
+
meta.root = metadata.find(m => m.class === meta.root.class);
|
|
867
1157
|
meta.root.__processed = true;
|
|
868
1158
|
}
|
|
869
1159
|
else {
|
|
@@ -872,17 +1162,24 @@ export class MetadataDiscovery {
|
|
|
872
1162
|
if (!meta.root.discriminatorColumn) {
|
|
873
1163
|
return;
|
|
874
1164
|
}
|
|
875
|
-
|
|
1165
|
+
meta.root.inheritanceType = 'sti';
|
|
1166
|
+
if (meta.root.discriminatorMap) {
|
|
1167
|
+
const map = meta.root.discriminatorMap;
|
|
1168
|
+
Object.keys(map)
|
|
1169
|
+
.filter(key => typeof map[key] === 'string')
|
|
1170
|
+
.forEach(key => map[key] = this.metadata.getByClassName(map[key]).class);
|
|
1171
|
+
}
|
|
1172
|
+
else {
|
|
876
1173
|
meta.root.discriminatorMap = {};
|
|
877
1174
|
const children = metadata
|
|
878
|
-
.filter(m => m.root.
|
|
1175
|
+
.filter(m => m.root.class === meta.root.class && !m.abstract)
|
|
879
1176
|
.sort((a, b) => a.className.localeCompare(b.className));
|
|
880
1177
|
for (const m of children) {
|
|
881
1178
|
const name = m.discriminatorValue ?? this.namingStrategy.classToTableName(m.className);
|
|
882
|
-
meta.root.discriminatorMap[name] = m.
|
|
1179
|
+
meta.root.discriminatorMap[name] = m.class;
|
|
883
1180
|
}
|
|
884
1181
|
}
|
|
885
|
-
meta.discriminatorValue =
|
|
1182
|
+
meta.discriminatorValue = QueryHelper.findDiscriminatorValue(meta.root.discriminatorMap, meta.class);
|
|
886
1183
|
if (!meta.root.properties[meta.root.discriminatorColumn]) {
|
|
887
1184
|
this.createDiscriminatorProperty(meta.root);
|
|
888
1185
|
}
|
|
@@ -891,32 +1188,219 @@ export class MetadataDiscovery {
|
|
|
891
1188
|
if (meta.root === meta) {
|
|
892
1189
|
return;
|
|
893
1190
|
}
|
|
894
|
-
let i = 1;
|
|
895
1191
|
Object.values(meta.properties).forEach(prop => {
|
|
896
1192
|
const newProp = { ...prop };
|
|
897
|
-
|
|
1193
|
+
const rootProp = meta.root.properties[prop.name];
|
|
1194
|
+
if (rootProp && (rootProp.type !== prop.type || (rootProp.fieldNames && prop.fieldNames && !compareArrays(rootProp.fieldNames, prop.fieldNames)))) {
|
|
898
1195
|
const name = newProp.name;
|
|
899
1196
|
this.initFieldName(newProp, newProp.object);
|
|
900
|
-
newProp.
|
|
1197
|
+
newProp.renamedFrom = name;
|
|
1198
|
+
newProp.name = `${name}_${meta._id}`;
|
|
901
1199
|
meta.root.addProperty(newProp);
|
|
1200
|
+
this.initFieldName(prop, prop.object);
|
|
1201
|
+
// Track all field variants and map discriminator values to field names
|
|
1202
|
+
if (!rootProp.stiFieldNames) {
|
|
1203
|
+
this.initFieldName(rootProp, rootProp.object);
|
|
1204
|
+
rootProp.stiFieldNames = [...rootProp.fieldNames];
|
|
1205
|
+
rootProp.stiFieldNameMap = {};
|
|
1206
|
+
// Find which discriminator owns the original fieldNames
|
|
1207
|
+
for (const [discValue, childClass] of Object.entries(meta.root.discriminatorMap)) {
|
|
1208
|
+
const childMeta = this.metadata.find(childClass);
|
|
1209
|
+
if (childMeta?.properties[prop.name]?.fieldNames && compareArrays(childMeta.properties[prop.name].fieldNames, rootProp.fieldNames)) {
|
|
1210
|
+
rootProp.stiFieldNameMap[discValue] = rootProp.fieldNames[0];
|
|
1211
|
+
break;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
rootProp.stiFieldNameMap[meta.discriminatorValue] = prop.fieldNames[0];
|
|
1216
|
+
rootProp.stiFieldNames.push(...prop.fieldNames);
|
|
902
1217
|
newProp.nullable = true;
|
|
903
1218
|
newProp.name = name;
|
|
904
1219
|
newProp.hydrate = false;
|
|
905
1220
|
newProp.inherited = true;
|
|
906
1221
|
return;
|
|
907
1222
|
}
|
|
908
|
-
if (prop.enum && prop.items &&
|
|
909
|
-
newProp.items = Utils.unique([...
|
|
1223
|
+
if (prop.enum && prop.items && rootProp?.items) {
|
|
1224
|
+
newProp.items = Utils.unique([...rootProp.items, ...prop.items]);
|
|
910
1225
|
}
|
|
911
1226
|
newProp.nullable = true;
|
|
912
|
-
newProp.inherited = !
|
|
1227
|
+
newProp.inherited = !rootProp;
|
|
913
1228
|
meta.root.addProperty(newProp);
|
|
914
1229
|
});
|
|
915
|
-
meta.
|
|
1230
|
+
meta.tableName = meta.root.tableName;
|
|
916
1231
|
meta.root.indexes = Utils.unique([...meta.root.indexes, ...meta.indexes]);
|
|
917
1232
|
meta.root.uniques = Utils.unique([...meta.root.uniques, ...meta.uniques]);
|
|
918
1233
|
meta.root.checks = Utils.unique([...meta.root.checks, ...meta.checks]);
|
|
919
1234
|
}
|
|
1235
|
+
/**
|
|
1236
|
+
* First pass of TPT initialization: sets up hierarchy relationships
|
|
1237
|
+
* (inheritanceType, tptParent, tptChildren) before properties have fieldNames.
|
|
1238
|
+
*/
|
|
1239
|
+
initTPTRelationships(meta, metadata) {
|
|
1240
|
+
if (meta.root !== meta) {
|
|
1241
|
+
meta.root = metadata.find(m => m.class === meta.root.class);
|
|
1242
|
+
}
|
|
1243
|
+
const inheritance = meta.inheritance;
|
|
1244
|
+
if (inheritance === 'tpt' && meta.root === meta) {
|
|
1245
|
+
meta.inheritanceType = 'tpt';
|
|
1246
|
+
meta.tptChildren = [];
|
|
1247
|
+
}
|
|
1248
|
+
if (meta.root.inheritanceType !== 'tpt') {
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
const parent = this.getTPTParent(meta, metadata);
|
|
1252
|
+
if (parent) {
|
|
1253
|
+
meta.tptParent = parent;
|
|
1254
|
+
meta.inheritanceType = 'tpt';
|
|
1255
|
+
parent.tptChildren ??= [];
|
|
1256
|
+
if (!parent.tptChildren.includes(meta)) {
|
|
1257
|
+
parent.tptChildren.push(meta);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
/**
|
|
1262
|
+
* Second pass of TPT initialization: re-resolves metadata references after fieldNames
|
|
1263
|
+
* are set, syncs to registry metadata, and sets up discriminators.
|
|
1264
|
+
*/
|
|
1265
|
+
finalizeTPTInheritance(meta, metadata) {
|
|
1266
|
+
if (meta.inheritanceType !== 'tpt') {
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
if (meta.tptParent) {
|
|
1270
|
+
meta.tptParent = metadata.find(m => m.class === meta.tptParent.class) ?? meta.tptParent;
|
|
1271
|
+
}
|
|
1272
|
+
if (meta.tptChildren) {
|
|
1273
|
+
meta.tptChildren = meta.tptChildren.map(child => metadata.find(m => m.class === child.class) ?? child);
|
|
1274
|
+
}
|
|
1275
|
+
const registryMeta = this.metadata.get(meta.class);
|
|
1276
|
+
if (registryMeta && registryMeta !== meta) {
|
|
1277
|
+
registryMeta.inheritanceType = meta.inheritanceType;
|
|
1278
|
+
registryMeta.tptParent = meta.tptParent ? this.metadata.get(meta.tptParent.class) : undefined;
|
|
1279
|
+
registryMeta.tptChildren = meta.tptChildren?.map(child => this.metadata.get(child.class));
|
|
1280
|
+
}
|
|
1281
|
+
this.initTPTDiscriminator(meta, metadata);
|
|
1282
|
+
}
|
|
1283
|
+
/**
|
|
1284
|
+
* Initialize TPT discriminator map and virtual discriminator property.
|
|
1285
|
+
* Unlike STI where the discriminator is a persisted column, TPT discriminator is computed
|
|
1286
|
+
* at query time using CASE WHEN expressions based on which child table has data.
|
|
1287
|
+
*/
|
|
1288
|
+
initTPTDiscriminator(meta, metadata) {
|
|
1289
|
+
const allDescendants = this.collectAllTPTDescendants(meta, metadata);
|
|
1290
|
+
allDescendants.sort((a, b) => this.getTPTDepth(b) - this.getTPTDepth(a));
|
|
1291
|
+
meta.allTPTDescendants = allDescendants;
|
|
1292
|
+
if (meta.root !== meta) {
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
meta.root.discriminatorMap = {};
|
|
1296
|
+
for (const m of allDescendants) {
|
|
1297
|
+
const name = this.namingStrategy.classToTableName(m.className);
|
|
1298
|
+
meta.root.discriminatorMap[name] = m.class;
|
|
1299
|
+
m.discriminatorValue = name;
|
|
1300
|
+
}
|
|
1301
|
+
if (!meta.abstract) {
|
|
1302
|
+
const name = this.namingStrategy.classToTableName(meta.className);
|
|
1303
|
+
meta.root.discriminatorMap[name] = meta.class;
|
|
1304
|
+
meta.discriminatorValue = name;
|
|
1305
|
+
}
|
|
1306
|
+
// Virtual discriminator property - computed at query time via CASE WHEN, not persisted
|
|
1307
|
+
const discriminatorColumn = '__tpt_type';
|
|
1308
|
+
if (!meta.root.properties[discriminatorColumn]) {
|
|
1309
|
+
meta.root.addProperty({
|
|
1310
|
+
name: discriminatorColumn,
|
|
1311
|
+
type: 'string',
|
|
1312
|
+
kind: ReferenceKind.SCALAR,
|
|
1313
|
+
persist: false,
|
|
1314
|
+
userDefined: false,
|
|
1315
|
+
hidden: true,
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
meta.root.tptDiscriminatorColumn = discriminatorColumn;
|
|
1319
|
+
}
|
|
1320
|
+
/**
|
|
1321
|
+
* Recursively collect all TPT descendants (children, grandchildren, etc.)
|
|
1322
|
+
*/
|
|
1323
|
+
collectAllTPTDescendants(meta, metadata) {
|
|
1324
|
+
const descendants = [];
|
|
1325
|
+
const collect = (parent) => {
|
|
1326
|
+
for (const child of parent.tptChildren ?? []) {
|
|
1327
|
+
const resolved = metadata.find(m => m.class === child.class) ?? child;
|
|
1328
|
+
if (!resolved.abstract) {
|
|
1329
|
+
descendants.push(resolved);
|
|
1330
|
+
}
|
|
1331
|
+
collect(resolved);
|
|
1332
|
+
}
|
|
1333
|
+
};
|
|
1334
|
+
collect(meta);
|
|
1335
|
+
return descendants;
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Computes ownProps for TPT entities - only properties defined in THIS entity,
|
|
1339
|
+
* not inherited from parent. Also creates synthetic join properties for parent/child relationships.
|
|
1340
|
+
*
|
|
1341
|
+
* Called multiple times during discovery as metadata is progressively built.
|
|
1342
|
+
* Each pass overwrites earlier results to reflect the final state of properties.
|
|
1343
|
+
*/
|
|
1344
|
+
computeTPTOwnProps(meta) {
|
|
1345
|
+
if (meta.inheritanceType !== 'tpt') {
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
const belongsToTable = (prop) => prop.persist !== false || prop.primary;
|
|
1349
|
+
// Use meta.properties (object) since meta.props (array) may not be populated yet
|
|
1350
|
+
const allProps = Object.values(meta.properties);
|
|
1351
|
+
if (!meta.tptParent) {
|
|
1352
|
+
meta.ownProps = allProps.filter(belongsToTable);
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
const parentPropNames = new Set(Object.values(meta.tptParent.properties).map(p => p.name));
|
|
1356
|
+
meta.ownProps = allProps.filter(prop => !parentPropNames.has(prop.name) && belongsToTable(prop));
|
|
1357
|
+
// Create synthetic join properties for the parent-child relationship
|
|
1358
|
+
const childFieldNames = meta.getPrimaryProps().flatMap(p => p.fieldNames);
|
|
1359
|
+
const parentFieldNames = meta.tptParent.getPrimaryProps().flatMap(p => p.fieldNames);
|
|
1360
|
+
meta.tptParentProp = {
|
|
1361
|
+
name: '[tpt:parent]',
|
|
1362
|
+
kind: ReferenceKind.MANY_TO_ONE,
|
|
1363
|
+
targetMeta: meta.tptParent,
|
|
1364
|
+
fieldNames: childFieldNames,
|
|
1365
|
+
referencedColumnNames: parentFieldNames,
|
|
1366
|
+
persist: false,
|
|
1367
|
+
};
|
|
1368
|
+
meta.tptInverseProp = {
|
|
1369
|
+
name: '[tpt:child]',
|
|
1370
|
+
kind: ReferenceKind.ONE_TO_ONE,
|
|
1371
|
+
owner: true,
|
|
1372
|
+
targetMeta: meta,
|
|
1373
|
+
fieldNames: parentFieldNames,
|
|
1374
|
+
joinColumns: parentFieldNames,
|
|
1375
|
+
referencedColumnNames: childFieldNames,
|
|
1376
|
+
persist: false,
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
/** Returns the depth of a TPT entity in its hierarchy (0 for root). */
|
|
1380
|
+
getTPTDepth(meta) {
|
|
1381
|
+
let depth = 0;
|
|
1382
|
+
let current = meta;
|
|
1383
|
+
while (current.tptParent) {
|
|
1384
|
+
depth++;
|
|
1385
|
+
current = current.tptParent;
|
|
1386
|
+
}
|
|
1387
|
+
return depth;
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Find the direct TPT parent entity for the given entity.
|
|
1391
|
+
*/
|
|
1392
|
+
getTPTParent(meta, metadata) {
|
|
1393
|
+
if (meta.root === meta) {
|
|
1394
|
+
return undefined;
|
|
1395
|
+
}
|
|
1396
|
+
return metadata.find(m => {
|
|
1397
|
+
const ext = meta.extends;
|
|
1398
|
+
if (ext instanceof EntitySchema) {
|
|
1399
|
+
return m.class === ext.meta.class || m.className === ext.meta.className;
|
|
1400
|
+
}
|
|
1401
|
+
return m.class === ext || m.className === Utils.className(ext);
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
920
1404
|
createDiscriminatorProperty(meta) {
|
|
921
1405
|
meta.addProperty({
|
|
922
1406
|
name: meta.discriminatorColumn,
|
|
@@ -933,13 +1417,23 @@ export class MetadataDiscovery {
|
|
|
933
1417
|
pks[0].autoincrement ??= true;
|
|
934
1418
|
}
|
|
935
1419
|
}
|
|
1420
|
+
createSchemaTable(meta) {
|
|
1421
|
+
const qualifiedName = meta.schema ? `${meta.schema}.${meta.tableName}` : meta.tableName;
|
|
1422
|
+
return {
|
|
1423
|
+
name: meta.tableName,
|
|
1424
|
+
schema: meta.schema,
|
|
1425
|
+
qualifiedName,
|
|
1426
|
+
toString: () => qualifiedName,
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
936
1429
|
initCheckConstraints(meta) {
|
|
937
|
-
const
|
|
1430
|
+
const columns = meta.createSchemaColumnMappingObject();
|
|
1431
|
+
const table = this.createSchemaTable(meta);
|
|
938
1432
|
for (const check of meta.checks) {
|
|
939
|
-
const
|
|
940
|
-
check.name ??= this.namingStrategy.indexName(meta.tableName,
|
|
1433
|
+
const fieldNames = check.property ? meta.properties[check.property].fieldNames : [];
|
|
1434
|
+
check.name ??= this.namingStrategy.indexName(meta.tableName, fieldNames, 'check');
|
|
941
1435
|
if (check.expression instanceof Function) {
|
|
942
|
-
check.expression = check.expression(
|
|
1436
|
+
check.expression = check.expression(columns, table);
|
|
943
1437
|
}
|
|
944
1438
|
}
|
|
945
1439
|
if (this.platform.usesEnumCheckConstraints() && !meta.embeddable) {
|
|
@@ -969,9 +1463,9 @@ export class MetadataDiscovery {
|
|
|
969
1463
|
}
|
|
970
1464
|
return;
|
|
971
1465
|
}
|
|
972
|
-
const map = meta.createColumnMappingObject();
|
|
973
1466
|
if (prop.generated instanceof Function) {
|
|
974
|
-
|
|
1467
|
+
const columns = meta.createSchemaColumnMappingObject();
|
|
1468
|
+
prop.generated = prop.generated(columns, this.createSchemaTable(meta));
|
|
975
1469
|
}
|
|
976
1470
|
}
|
|
977
1471
|
getDefaultVersionValue(meta, prop) {
|
|
@@ -991,9 +1485,6 @@ export class MetadataDiscovery {
|
|
|
991
1485
|
return '1';
|
|
992
1486
|
}
|
|
993
1487
|
inferDefaultValue(meta, prop) {
|
|
994
|
-
if (!meta.class) {
|
|
995
|
-
return;
|
|
996
|
-
}
|
|
997
1488
|
try {
|
|
998
1489
|
// try to create two entity instances to detect the value is stable
|
|
999
1490
|
const now = Date.now();
|
|
@@ -1021,7 +1512,7 @@ export class MetadataDiscovery {
|
|
|
1021
1512
|
return;
|
|
1022
1513
|
}
|
|
1023
1514
|
let val = prop.default;
|
|
1024
|
-
const raw =
|
|
1515
|
+
const raw = Raw.getKnownFragment(val);
|
|
1025
1516
|
if (raw) {
|
|
1026
1517
|
prop.defaultRaw = this.platform.formatQuery(raw.sql, raw.params);
|
|
1027
1518
|
return;
|
|
@@ -1149,9 +1640,9 @@ export class MetadataDiscovery {
|
|
|
1149
1640
|
if (Type.isMappedType(prop.customType) && prop.kind === ReferenceKind.SCALAR && !isArray) {
|
|
1150
1641
|
prop.type = prop.customType.name;
|
|
1151
1642
|
}
|
|
1152
|
-
if (!prop.customType && [ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind) &&
|
|
1643
|
+
if (!prop.customType && [ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind) && !prop.polymorphic && prop.targetMeta.compositePK) {
|
|
1153
1644
|
prop.customTypes = [];
|
|
1154
|
-
for (const pk of
|
|
1645
|
+
for (const pk of prop.targetMeta.getPrimaryProps()) {
|
|
1155
1646
|
if (pk.customType) {
|
|
1156
1647
|
prop.customTypes.push(pk.customType);
|
|
1157
1648
|
prop.hasConvertToJSValueSQL ||= !!pk.customType.convertToJSValueSQL && pk.customType.convertToJSValueSQL('', this.platform) !== '';
|
|
@@ -1178,22 +1669,36 @@ export class MetadataDiscovery {
|
|
|
1178
1669
|
}
|
|
1179
1670
|
}
|
|
1180
1671
|
initRelation(prop) {
|
|
1181
|
-
if (prop.kind === ReferenceKind.SCALAR) {
|
|
1672
|
+
if (prop.kind === ReferenceKind.SCALAR || prop.polymorphTargets) {
|
|
1182
1673
|
return;
|
|
1183
1674
|
}
|
|
1184
|
-
|
|
1185
|
-
prop.
|
|
1675
|
+
// 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
|
|
1676
|
+
const meta2 = this.metadata.find(prop.target) ?? this.metadata.getByClassName(prop.type);
|
|
1677
|
+
// If targetKey is specified, use that property instead of PKs for referencedPKs
|
|
1678
|
+
prop.referencedPKs = prop.targetKey ? [prop.targetKey] : meta2.primaryKeys;
|
|
1186
1679
|
prop.targetMeta = meta2;
|
|
1680
|
+
if (meta2.view) {
|
|
1681
|
+
prop.createForeignKeyConstraint = false;
|
|
1682
|
+
}
|
|
1683
|
+
// Auto-generate formula for persist: false relations, but only for single-column FKs
|
|
1684
|
+
// Composite FK relations need standard JOIN conditions, not formula-based
|
|
1187
1685
|
if (!prop.formula && prop.persist === false && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && !prop.embedded) {
|
|
1188
|
-
|
|
1686
|
+
this.initFieldName(prop);
|
|
1687
|
+
if (prop.fieldNames?.length === 1) {
|
|
1688
|
+
prop.formula = table => `${table}.${this.platform.quoteIdentifier(prop.fieldNames[0])}`;
|
|
1689
|
+
}
|
|
1189
1690
|
}
|
|
1190
1691
|
}
|
|
1191
1692
|
initColumnType(prop) {
|
|
1192
1693
|
this.initUnsigned(prop);
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
prop.
|
|
1196
|
-
|
|
1694
|
+
// Get the target properties for FK relations - use targetKey property if specified, otherwise PKs
|
|
1695
|
+
const targetProps = prop.targetMeta
|
|
1696
|
+
? (prop.targetKey ? [prop.targetMeta.properties[prop.targetKey]] : prop.targetMeta.getPrimaryProps())
|
|
1697
|
+
: [];
|
|
1698
|
+
targetProps.map(targetProp => {
|
|
1699
|
+
prop.length ??= targetProp.length;
|
|
1700
|
+
prop.precision ??= targetProp.precision;
|
|
1701
|
+
prop.scale ??= targetProp.scale;
|
|
1197
1702
|
});
|
|
1198
1703
|
if (prop.kind === ReferenceKind.SCALAR && (prop.type == null || prop.type === 'object') && prop.columnTypes?.[0]) {
|
|
1199
1704
|
delete prop.type;
|
|
@@ -1207,7 +1712,6 @@ export class MetadataDiscovery {
|
|
|
1207
1712
|
const mappedType = this.getMappedType(prop);
|
|
1208
1713
|
const SCALAR_TYPES = ['string', 'number', 'boolean', 'bigint', 'Date', 'Buffer', 'RegExp', 'any', 'unknown'];
|
|
1209
1714
|
if (mappedType instanceof t.unknown
|
|
1210
|
-
&& !prop.columnTypes
|
|
1211
1715
|
// it could be a runtime type from reflect-metadata
|
|
1212
1716
|
&& !SCALAR_TYPES.includes(prop.type)
|
|
1213
1717
|
// or it might be inferred via ts-morph to some generic type alias
|
|
@@ -1220,23 +1724,31 @@ export class MetadataDiscovery {
|
|
|
1220
1724
|
}
|
|
1221
1725
|
return;
|
|
1222
1726
|
}
|
|
1223
|
-
|
|
1727
|
+
/* v8 ignore next */
|
|
1728
|
+
if (prop.kind === ReferenceKind.EMBEDDED && prop.object) {
|
|
1224
1729
|
prop.columnTypes = [this.platform.getJsonDeclarationSQL()];
|
|
1225
1730
|
return;
|
|
1226
1731
|
}
|
|
1227
|
-
const targetMeta =
|
|
1732
|
+
const targetMeta = prop.targetMeta;
|
|
1228
1733
|
prop.columnTypes = [];
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1734
|
+
// Use targetKey property if specified, otherwise use primary key properties
|
|
1735
|
+
const referencedProps = prop.targetKey
|
|
1736
|
+
? [targetMeta.properties[prop.targetKey]]
|
|
1737
|
+
: targetMeta.getPrimaryProps();
|
|
1738
|
+
if (prop.polymorphic && prop.polymorphTargets) {
|
|
1739
|
+
prop.columnTypes.push(this.platform.getVarcharTypeDeclarationSQL(prop));
|
|
1740
|
+
}
|
|
1741
|
+
for (const referencedProp of referencedProps) {
|
|
1742
|
+
this.initCustomType(targetMeta, referencedProp);
|
|
1743
|
+
this.initColumnType(referencedProp);
|
|
1744
|
+
const mappedType = this.getMappedType(referencedProp);
|
|
1745
|
+
let columnTypes = referencedProp.columnTypes;
|
|
1746
|
+
if (referencedProp.autoincrement) {
|
|
1747
|
+
columnTypes = [mappedType.getColumnType({ ...referencedProp, autoincrement: false }, this.platform)];
|
|
1236
1748
|
}
|
|
1237
1749
|
prop.columnTypes.push(...columnTypes);
|
|
1238
|
-
if (!targetMeta.compositePK) {
|
|
1239
|
-
prop.customType =
|
|
1750
|
+
if (!targetMeta.compositePK || prop.targetKey) {
|
|
1751
|
+
prop.customType = referencedProp.customType;
|
|
1240
1752
|
}
|
|
1241
1753
|
}
|
|
1242
1754
|
}
|
|
@@ -1274,8 +1786,7 @@ export class MetadataDiscovery {
|
|
|
1274
1786
|
return;
|
|
1275
1787
|
}
|
|
1276
1788
|
if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
|
|
1277
|
-
|
|
1278
|
-
prop.unsigned = meta2.getPrimaryProps().some(pk => {
|
|
1789
|
+
prop.unsigned = prop.targetMeta.getPrimaryProps().some(pk => {
|
|
1279
1790
|
this.initUnsigned(pk);
|
|
1280
1791
|
return pk.unsigned;
|
|
1281
1792
|
});
|