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