@mikro-orm/core 7.0.0-dev.228 → 7.0.0-dev.229
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.js +5 -3
- package/entity/EntityFactory.js +15 -6
- package/entity/defineEntity.d.ts +8 -7
- package/errors.d.ts +2 -0
- package/errors.js +6 -0
- package/index.d.ts +1 -1
- package/metadata/EntitySchema.d.ts +7 -0
- package/metadata/EntitySchema.js +21 -1
- package/metadata/MetadataDiscovery.d.ts +35 -0
- package/metadata/MetadataDiscovery.js +198 -7
- package/metadata/MetadataValidator.d.ts +9 -0
- package/metadata/MetadataValidator.js +29 -1
- package/metadata/types.d.ts +4 -3
- package/package.json +1 -1
- package/typings.d.ts +61 -10
- package/typings.js +48 -4
- package/unit-of-work/ChangeSet.d.ts +2 -0
- package/unit-of-work/ChangeSetPersister.d.ts +5 -0
- package/unit-of-work/ChangeSetPersister.js +19 -0
- package/unit-of-work/UnitOfWork.d.ts +5 -0
- package/unit-of-work/UnitOfWork.js +105 -7
- package/utils/AbstractSchemaGenerator.js +12 -3
- package/utils/EntityComparator.js +1 -1
- package/utils/Utils.js +1 -1
package/EntityManager.js
CHANGED
|
@@ -297,7 +297,7 @@ export class EntityManager {
|
|
|
297
297
|
// this method only handles the problem for mongo driver, SQL drivers have their implementation inside QueryBuilder
|
|
298
298
|
applyDiscriminatorCondition(entityName, where) {
|
|
299
299
|
const meta = this.metadata.find(entityName);
|
|
300
|
-
if (!meta?.discriminatorValue) {
|
|
300
|
+
if (meta?.root.inheritanceType !== 'sti' || !meta?.discriminatorValue) {
|
|
301
301
|
return where;
|
|
302
302
|
}
|
|
303
303
|
const types = Object.values(meta.root.discriminatorMap).map(cls => this.metadata.get(cls));
|
|
@@ -1413,9 +1413,11 @@ export class EntityManager {
|
|
|
1413
1413
|
if (p.includes(':')) {
|
|
1414
1414
|
p = p.split(':', 2)[0];
|
|
1415
1415
|
}
|
|
1416
|
-
|
|
1416
|
+
// For TPT inheritance, check the entity's own properties, not just the root's
|
|
1417
|
+
// For STI, meta.properties includes all properties anyway
|
|
1418
|
+
const ret = p in meta.properties;
|
|
1417
1419
|
if (parts.length > 0) {
|
|
1418
|
-
return this.canPopulate(meta.
|
|
1420
|
+
return this.canPopulate(meta.properties[p].targetMeta.class, parts.join('.'));
|
|
1419
1421
|
}
|
|
1420
1422
|
return ret;
|
|
1421
1423
|
}
|
package/entity/EntityFactory.js
CHANGED
|
@@ -83,7 +83,7 @@ export class EntityFactory {
|
|
|
83
83
|
else {
|
|
84
84
|
this.hydrate(entity, meta2, data, options);
|
|
85
85
|
}
|
|
86
|
-
if (exists && meta.
|
|
86
|
+
if (exists && meta.root.inheritanceType && !(entity instanceof meta2.class)) {
|
|
87
87
|
Object.setPrototypeOf(entity, meta2.prototype);
|
|
88
88
|
}
|
|
89
89
|
if (options.merge && wrapped.hasPrimaryKey()) {
|
|
@@ -281,13 +281,22 @@ export class EntityFactory {
|
|
|
281
281
|
return this.unitOfWork.getById(meta.class, pks, schema);
|
|
282
282
|
}
|
|
283
283
|
processDiscriminatorColumn(meta, data) {
|
|
284
|
-
|
|
284
|
+
// Handle STI discriminator (persisted column)
|
|
285
|
+
if (meta.root.inheritanceType === 'sti') {
|
|
286
|
+
const prop = meta.properties[meta.root.discriminatorColumn];
|
|
287
|
+
const value = data[prop.name];
|
|
288
|
+
const type = meta.root.discriminatorMap[value];
|
|
289
|
+
meta = type ? this.metadata.get(type) : meta;
|
|
285
290
|
return meta;
|
|
286
291
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
292
|
+
// Handle TPT discriminator (computed at query time)
|
|
293
|
+
if (meta.root.inheritanceType === 'tpt' && meta.root.discriminatorMap) {
|
|
294
|
+
const value = data[meta.root.tptDiscriminatorColumn];
|
|
295
|
+
if (value) {
|
|
296
|
+
const type = meta.root.discriminatorMap[value];
|
|
297
|
+
meta = type ? this.metadata.get(type) : meta;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
291
300
|
return meta;
|
|
292
301
|
}
|
|
293
302
|
/**
|
package/entity/defineEntity.d.ts
CHANGED
|
@@ -429,6 +429,7 @@ export interface EntityMetadataWithProperties<TName extends string, TTableName e
|
|
|
429
429
|
primaryKeys?: TPK & InferPrimaryKey<TProperties>[];
|
|
430
430
|
hooks?: DefineEntityHooks;
|
|
431
431
|
repository?: () => TRepository;
|
|
432
|
+
inheritance?: 'tpt';
|
|
432
433
|
discriminatorColumn?: keyof TProperties;
|
|
433
434
|
versionProperty?: keyof TProperties;
|
|
434
435
|
concurrencyCheckKeys?: Set<keyof TProperties>;
|
|
@@ -579,18 +580,18 @@ type MaybeOpt<Value, Options> = Options extends {
|
|
|
579
580
|
mapToPk: true;
|
|
580
581
|
} ? Value extends Opt<infer OriginalValue> ? OriginalValue : Value : Options extends {
|
|
581
582
|
autoincrement: true;
|
|
582
|
-
}
|
|
583
|
+
} | {
|
|
583
584
|
onCreate: Function;
|
|
584
|
-
}
|
|
585
|
+
} | {
|
|
585
586
|
default: string | string[] | number | number[] | boolean | null | Date | Raw;
|
|
586
|
-
}
|
|
587
|
+
} | {
|
|
587
588
|
defaultRaw: string;
|
|
588
|
-
}
|
|
589
|
+
} | {
|
|
589
590
|
persist: false;
|
|
590
|
-
}
|
|
591
|
+
} | {
|
|
591
592
|
version: true;
|
|
592
|
-
}
|
|
593
|
-
formula: string | ((...args: any[]) =>
|
|
593
|
+
} | {
|
|
594
|
+
formula: string | ((...args: any[]) => any);
|
|
594
595
|
} ? Opt<Value> : Value;
|
|
595
596
|
type MaybeHidden<Value, Options> = Options extends {
|
|
596
597
|
hidden: true;
|
package/errors.d.ts
CHANGED
|
@@ -68,6 +68,8 @@ export declare class MetadataError<T extends AnyEntity = AnyEntity> extends Vali
|
|
|
68
68
|
static incompatiblePolymorphicTargets(meta: EntityMetadata, prop: EntityProperty, target1: EntityMetadata, target2: EntityMetadata, reason: string): MetadataError<Partial<any>>;
|
|
69
69
|
static dangerousPropertyName(meta: EntityMetadata, prop: EntityProperty): MetadataError<Partial<any>>;
|
|
70
70
|
static viewEntityWithoutExpression(meta: EntityMetadata): MetadataError;
|
|
71
|
+
static mixedInheritanceStrategies(root: EntityMetadata, child: EntityMetadata): MetadataError;
|
|
72
|
+
static tptNotSupportedByDriver(meta: EntityMetadata): MetadataError;
|
|
71
73
|
private static fromMessage;
|
|
72
74
|
}
|
|
73
75
|
export declare class NotFoundError<T extends AnyEntity = AnyEntity> extends ValidationError<T> {
|
package/errors.js
CHANGED
|
@@ -233,6 +233,12 @@ export class MetadataError extends ValidationError {
|
|
|
233
233
|
static viewEntityWithoutExpression(meta) {
|
|
234
234
|
return new MetadataError(`View entity ${meta.className} is missing 'expression'. View entities must have an expression defining the SQL query.`);
|
|
235
235
|
}
|
|
236
|
+
static mixedInheritanceStrategies(root, child) {
|
|
237
|
+
return new MetadataError(`Entity ${child.className} cannot mix STI (Single Table Inheritance) and TPT (Table-Per-Type) inheritance. Root entity ${root.className} uses STI (discriminatorColumn) but also has inheritance: 'tpt'. Choose one inheritance strategy per hierarchy.`);
|
|
238
|
+
}
|
|
239
|
+
static tptNotSupportedByDriver(meta) {
|
|
240
|
+
return new MetadataError(`Entity ${meta.className} uses TPT (Table-Per-Type) inheritance which is not supported by the current driver. TPT requires SQL JOINs and is only available with SQL drivers.`);
|
|
241
|
+
}
|
|
236
242
|
static fromMessage(meta, prop, message) {
|
|
237
243
|
return new MetadataError(`${meta.className}.${prop.name} ${message}`);
|
|
238
244
|
}
|
package/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* @module core
|
|
4
4
|
*/
|
|
5
5
|
export { EntityMetadata, PrimaryKeyProp, EntityRepositoryType, OptionalProps, EagerProps, HiddenProps, Config } from './typings.js';
|
|
6
|
-
export type { CompiledFunctions, Constructor, ConnectionType, Dictionary, Primary, IPrimaryKey, ObjectQuery, FilterQuery, IWrappedEntity, EntityName, EntityData, Highlighter, MaybePromise, AnyEntity, EntityClass, EntityProperty, PopulateOptions, Populate, Loaded, New, LoadedReference, LoadedCollection, IMigrator, IMigrationGenerator, MigratorEvent, GetRepository, MigrationObject, DeepPartial, PrimaryProperty, Cast, IsUnknown, EntityDictionary, EntityDTO, MigrationDiff, GenerateOptions, FilterObject, IEntityGenerator, ISeedManager, RequiredEntityData, CheckCallback, IndexCallback, FormulaCallback, FormulaTable, SimpleColumnMeta, Rel, Ref, ScalarRef, EntityRef, ISchemaGenerator, UmzugMigration, MigrateOptions, MigrationResult, MigrationRow, EntityKey, EntityValue, EntityDataValue, FilterKey, EntityType, FromEntityType, Selected, IsSubset, EntityProps, ExpandProperty, ExpandScalar, FilterItemValue, ExpandQuery, Scalar, ExpandHint, FilterValue, MergeLoaded, MergeSelected, TypeConfig, AnyString, ClearDatabaseOptions, CreateSchemaOptions, EnsureDatabaseOptions, UpdateSchemaOptions, DropSchemaOptions, RefreshDatabaseOptions, AutoPath, UnboxArray, MetadataProcessor, ImportsResolver, RequiredNullable, DefineConfig, Opt, Hidden, EntitySchemaWithMeta, InferEntity, CheckConstraint, GeneratedColumnCallback, FilterDef, EntityCtor, Subquery, } from './typings.js';
|
|
6
|
+
export type { CompiledFunctions, Constructor, ConnectionType, Dictionary, Primary, IPrimaryKey, ObjectQuery, FilterQuery, IWrappedEntity, EntityName, EntityData, Highlighter, MaybePromise, AnyEntity, EntityClass, EntityProperty, PopulateOptions, Populate, Loaded, New, LoadedReference, LoadedCollection, IMigrator, IMigrationGenerator, MigratorEvent, GetRepository, MigrationObject, DeepPartial, PrimaryProperty, Cast, IsUnknown, EntityDictionary, EntityDTO, MigrationDiff, GenerateOptions, FilterObject, IEntityGenerator, ISeedManager, RequiredEntityData, CheckCallback, IndexCallback, FormulaCallback, FormulaTable, SchemaTable, SchemaColumns, SimpleColumnMeta, Rel, Ref, ScalarRef, EntityRef, ISchemaGenerator, UmzugMigration, MigrateOptions, MigrationResult, MigrationRow, EntityKey, EntityValue, EntityDataValue, FilterKey, EntityType, FromEntityType, Selected, IsSubset, EntityProps, ExpandProperty, ExpandScalar, FilterItemValue, ExpandQuery, Scalar, ExpandHint, FilterValue, MergeLoaded, MergeSelected, TypeConfig, AnyString, ClearDatabaseOptions, CreateSchemaOptions, EnsureDatabaseOptions, UpdateSchemaOptions, DropSchemaOptions, RefreshDatabaseOptions, AutoPath, UnboxArray, MetadataProcessor, ImportsResolver, RequiredNullable, DefineConfig, Opt, Hidden, EntitySchemaWithMeta, InferEntity, CheckConstraint, GeneratedColumnCallback, FilterDef, EntityCtor, Subquery, } from './typings.js';
|
|
7
7
|
export * from './enums.js';
|
|
8
8
|
export * from './errors.js';
|
|
9
9
|
export * from './exceptions.js';
|
|
@@ -39,6 +39,8 @@ export type EntitySchemaMetadata<Entity, Base = never, Class extends EntityCtor
|
|
|
39
39
|
properties?: {
|
|
40
40
|
[Key in keyof OmitBaseProps<Entity, Base> as CleanKeys<OmitBaseProps<Entity, Base>, Key>]-?: EntitySchemaProperty<ExpandProperty<NonNullable<Entity[Key]>>, Entity>;
|
|
41
41
|
};
|
|
42
|
+
} & {
|
|
43
|
+
inheritance?: 'tpt';
|
|
42
44
|
};
|
|
43
45
|
export declare class EntitySchema<Entity = any, Base = never, Class extends EntityCtor = EntityCtor<Entity>> {
|
|
44
46
|
/**
|
|
@@ -78,6 +80,11 @@ export declare class EntitySchema<Entity = any, Base = never, Class extends Enti
|
|
|
78
80
|
* @internal
|
|
79
81
|
*/
|
|
80
82
|
init(): this;
|
|
83
|
+
/**
|
|
84
|
+
* Check if this entity is part of a TPT hierarchy by walking up the extends chain.
|
|
85
|
+
* This handles mid-level abstract entities (e.g., Animal -> Mammal -> Dog where Mammal is abstract).
|
|
86
|
+
*/
|
|
87
|
+
private isPartOfTPTHierarchy;
|
|
81
88
|
private initProperties;
|
|
82
89
|
private initPrimaryKeys;
|
|
83
90
|
private normalizeType;
|
package/metadata/EntitySchema.js
CHANGED
|
@@ -201,7 +201,9 @@ export class EntitySchema {
|
|
|
201
201
|
return this;
|
|
202
202
|
}
|
|
203
203
|
this.setClass(this._meta.class);
|
|
204
|
-
|
|
204
|
+
// Abstract TPT entities keep their name because they have their own table
|
|
205
|
+
const isTPT = this._meta.inheritance === 'tpt' || this.isPartOfTPTHierarchy();
|
|
206
|
+
if (this._meta.abstract && !this._meta.discriminatorColumn && !isTPT) {
|
|
205
207
|
delete this._meta.name;
|
|
206
208
|
}
|
|
207
209
|
const tableName = this._meta.collection ?? this._meta.tableName;
|
|
@@ -216,6 +218,24 @@ export class EntitySchema {
|
|
|
216
218
|
this.initialized = true;
|
|
217
219
|
return this;
|
|
218
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* Check if this entity is part of a TPT hierarchy by walking up the extends chain.
|
|
223
|
+
* This handles mid-level abstract entities (e.g., Animal -> Mammal -> Dog where Mammal is abstract).
|
|
224
|
+
*/
|
|
225
|
+
isPartOfTPTHierarchy() {
|
|
226
|
+
let parent = this._meta.extends;
|
|
227
|
+
while (parent) {
|
|
228
|
+
const parentSchema = parent instanceof EntitySchema ? parent : EntitySchema.REGISTRY.get(parent);
|
|
229
|
+
if (!parentSchema) {
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
if (parentSchema._meta.inheritance === 'tpt') {
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
parent = parentSchema._meta.extends;
|
|
236
|
+
}
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
219
239
|
initProperties() {
|
|
220
240
|
Utils.entries(this._meta.properties).forEach(([name, options]) => {
|
|
221
241
|
if (Type.isMappedType(options.type)) {
|
|
@@ -71,8 +71,43 @@ export declare class MetadataDiscovery {
|
|
|
71
71
|
private initPolymorphicRelation;
|
|
72
72
|
private initEmbeddables;
|
|
73
73
|
private initSingleTableInheritance;
|
|
74
|
+
/**
|
|
75
|
+
* First pass of TPT initialization: sets up hierarchy relationships
|
|
76
|
+
* (inheritanceType, tptParent, tptChildren) before properties have fieldNames.
|
|
77
|
+
*/
|
|
78
|
+
private initTPTRelationships;
|
|
79
|
+
/**
|
|
80
|
+
* Second pass of TPT initialization: re-resolves metadata references after fieldNames
|
|
81
|
+
* are set, syncs to registry metadata, and sets up discriminators.
|
|
82
|
+
*/
|
|
83
|
+
private finalizeTPTInheritance;
|
|
84
|
+
/**
|
|
85
|
+
* Initialize TPT discriminator map and virtual discriminator property.
|
|
86
|
+
* Unlike STI where the discriminator is a persisted column, TPT discriminator is computed
|
|
87
|
+
* at query time using CASE WHEN expressions based on which child table has data.
|
|
88
|
+
*/
|
|
89
|
+
private initTPTDiscriminator;
|
|
90
|
+
/**
|
|
91
|
+
* Recursively collect all TPT descendants (children, grandchildren, etc.)
|
|
92
|
+
*/
|
|
93
|
+
private collectAllTPTDescendants;
|
|
94
|
+
/**
|
|
95
|
+
* Computes ownProps for TPT entities - only properties defined in THIS entity,
|
|
96
|
+
* not inherited from parent. Also creates synthetic join properties for parent/child relationships.
|
|
97
|
+
*
|
|
98
|
+
* Called multiple times during discovery as metadata is progressively built.
|
|
99
|
+
* Each pass overwrites earlier results to reflect the final state of properties.
|
|
100
|
+
*/
|
|
101
|
+
private computeTPTOwnProps;
|
|
102
|
+
/** Returns the depth of a TPT entity in its hierarchy (0 for root). */
|
|
103
|
+
private getTPTDepth;
|
|
104
|
+
/**
|
|
105
|
+
* Find the direct TPT parent entity for the given entity.
|
|
106
|
+
*/
|
|
107
|
+
private getTPTParent;
|
|
74
108
|
private createDiscriminatorProperty;
|
|
75
109
|
private initAutoincrement;
|
|
110
|
+
private createSchemaTable;
|
|
76
111
|
private initCheckConstraints;
|
|
77
112
|
private initGeneratedColumn;
|
|
78
113
|
private getDefaultVersionValue;
|
|
@@ -78,6 +78,9 @@ export class MetadataDiscovery {
|
|
|
78
78
|
});
|
|
79
79
|
for (const meta of discovered) {
|
|
80
80
|
meta.root = discovered.get(meta.root.class);
|
|
81
|
+
if (meta.inheritanceType === 'tpt') {
|
|
82
|
+
this.computeTPTOwnProps(meta);
|
|
83
|
+
}
|
|
81
84
|
}
|
|
82
85
|
return discovered;
|
|
83
86
|
}
|
|
@@ -122,6 +125,7 @@ export class MetadataDiscovery {
|
|
|
122
125
|
// sort so we discover entities first to get around issues with nested embeddables
|
|
123
126
|
filtered.sort((a, b) => !a.embeddable === !b.embeddable ? 0 : (a.embeddable ? 1 : -1));
|
|
124
127
|
filtered.forEach(meta => this.initSingleTableInheritance(meta, filtered));
|
|
128
|
+
filtered.forEach(meta => this.initTPTRelationships(meta, filtered));
|
|
125
129
|
filtered.forEach(meta => this.defineBaseEntityProperties(meta));
|
|
126
130
|
filtered.forEach(meta => {
|
|
127
131
|
const newMeta = EntitySchema.fromMetadata(meta).init().meta;
|
|
@@ -136,6 +140,8 @@ export class MetadataDiscovery {
|
|
|
136
140
|
forEachProp((_m, p) => this.initRelation(p));
|
|
137
141
|
forEachProp((m, p) => this.initEmbeddables(m, p));
|
|
138
142
|
forEachProp((_m, p) => this.initFieldName(p));
|
|
143
|
+
filtered.forEach(meta => this.finalizeTPTInheritance(meta, filtered));
|
|
144
|
+
filtered.forEach(meta => this.computeTPTOwnProps(meta));
|
|
139
145
|
forEachProp((m, p) => this.initVersionProperty(m, p));
|
|
140
146
|
forEachProp((m, p) => this.initCustomType(m, p));
|
|
141
147
|
forEachProp((m, p) => this.initGeneratedColumn(m, p));
|
|
@@ -158,6 +164,9 @@ export class MetadataDiscovery {
|
|
|
158
164
|
meta = this.metadata.get(meta.class);
|
|
159
165
|
meta.sync(true);
|
|
160
166
|
this.findReferencingProperties(meta, filtered);
|
|
167
|
+
if (meta.inheritanceType === 'tpt') {
|
|
168
|
+
this.computeTPTOwnProps(meta);
|
|
169
|
+
}
|
|
161
170
|
return meta;
|
|
162
171
|
});
|
|
163
172
|
}
|
|
@@ -296,7 +305,9 @@ export class MetadataDiscovery {
|
|
|
296
305
|
return meta;
|
|
297
306
|
}
|
|
298
307
|
const root = this.getRootEntity(base);
|
|
299
|
-
|
|
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') {
|
|
300
311
|
return root;
|
|
301
312
|
}
|
|
302
313
|
return meta;
|
|
@@ -1151,6 +1162,7 @@ export class MetadataDiscovery {
|
|
|
1151
1162
|
if (!meta.root.discriminatorColumn) {
|
|
1152
1163
|
return;
|
|
1153
1164
|
}
|
|
1165
|
+
meta.root.inheritanceType = 'sti';
|
|
1154
1166
|
if (meta.root.discriminatorMap) {
|
|
1155
1167
|
const map = meta.root.discriminatorMap;
|
|
1156
1168
|
Object.keys(map)
|
|
@@ -1220,6 +1232,175 @@ export class MetadataDiscovery {
|
|
|
1220
1232
|
meta.root.uniques = Utils.unique([...meta.root.uniques, ...meta.uniques]);
|
|
1221
1233
|
meta.root.checks = Utils.unique([...meta.root.checks, ...meta.checks]);
|
|
1222
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
|
+
}
|
|
1223
1404
|
createDiscriminatorProperty(meta) {
|
|
1224
1405
|
meta.addProperty({
|
|
1225
1406
|
name: meta.discriminatorColumn,
|
|
@@ -1236,13 +1417,23 @@ export class MetadataDiscovery {
|
|
|
1236
1417
|
pks[0].autoincrement ??= true;
|
|
1237
1418
|
}
|
|
1238
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
|
+
}
|
|
1239
1429
|
initCheckConstraints(meta) {
|
|
1240
|
-
const
|
|
1430
|
+
const columns = meta.createSchemaColumnMappingObject();
|
|
1431
|
+
const table = this.createSchemaTable(meta);
|
|
1241
1432
|
for (const check of meta.checks) {
|
|
1242
|
-
const
|
|
1243
|
-
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');
|
|
1244
1435
|
if (check.expression instanceof Function) {
|
|
1245
|
-
check.expression = check.expression(
|
|
1436
|
+
check.expression = check.expression(columns, table);
|
|
1246
1437
|
}
|
|
1247
1438
|
}
|
|
1248
1439
|
if (this.platform.usesEnumCheckConstraints() && !meta.embeddable) {
|
|
@@ -1272,9 +1463,9 @@ export class MetadataDiscovery {
|
|
|
1272
1463
|
}
|
|
1273
1464
|
return;
|
|
1274
1465
|
}
|
|
1275
|
-
const map = meta.createColumnMappingObject();
|
|
1276
1466
|
if (prop.generated instanceof Function) {
|
|
1277
|
-
|
|
1467
|
+
const columns = meta.createSchemaColumnMappingObject();
|
|
1468
|
+
prop.generated = prop.generated(columns, this.createSchemaTable(meta));
|
|
1278
1469
|
}
|
|
1279
1470
|
}
|
|
1280
1471
|
getDefaultVersionValue(meta, prop) {
|
|
@@ -35,4 +35,13 @@ export declare class MetadataValidator {
|
|
|
35
35
|
* View entities must have an expression.
|
|
36
36
|
*/
|
|
37
37
|
private validateViewEntity;
|
|
38
|
+
/**
|
|
39
|
+
* Validates that STI and TPT are not mixed in the same inheritance hierarchy.
|
|
40
|
+
* An entity hierarchy can use either STI (discriminatorColumn) or TPT (inheritance: 'tpt'),
|
|
41
|
+
* but not both.
|
|
42
|
+
*
|
|
43
|
+
* Note: This validation runs before `initTablePerTypeInheritance` sets `inheritanceType`,
|
|
44
|
+
* so we check the raw `inheritance` option from the decorator/schema.
|
|
45
|
+
*/
|
|
46
|
+
private validateInheritanceStrategies;
|
|
38
47
|
}
|
|
@@ -61,6 +61,8 @@ export class MetadataValidator {
|
|
|
61
61
|
if (discovered.length === 0 && options.warnWhenNoEntities) {
|
|
62
62
|
throw MetadataError.noEntityDiscovered();
|
|
63
63
|
}
|
|
64
|
+
// Validate no mixing of STI and TPT in the same hierarchy
|
|
65
|
+
this.validateInheritanceStrategies(discovered);
|
|
64
66
|
const tableNames = discovered.filter(meta => !meta.abstract && !meta.embeddable && meta === meta.root && (meta.tableName || meta.collection) && meta.schema !== '*');
|
|
65
67
|
const duplicateTableNames = Utils.findDuplicates(tableNames.map(meta => {
|
|
66
68
|
const tableName = meta.tableName || meta.collection;
|
|
@@ -117,7 +119,7 @@ export class MetadataValidator {
|
|
|
117
119
|
if (!targetMeta) {
|
|
118
120
|
throw MetadataError.fromWrongTypeDefinition(meta, prop);
|
|
119
121
|
}
|
|
120
|
-
if (targetMeta.abstract && !targetMeta.
|
|
122
|
+
if (targetMeta.abstract && !targetMeta.root?.inheritanceType && !targetMeta.embeddable) {
|
|
121
123
|
throw MetadataError.targetIsAbstract(meta, prop);
|
|
122
124
|
}
|
|
123
125
|
if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && prop.persist === false && targetMeta.compositePK && options.checkNonPersistentCompositeProps) {
|
|
@@ -336,4 +338,30 @@ export class MetadataValidator {
|
|
|
336
338
|
// Validate property names
|
|
337
339
|
this.validatePropertyNames(meta);
|
|
338
340
|
}
|
|
341
|
+
/**
|
|
342
|
+
* Validates that STI and TPT are not mixed in the same inheritance hierarchy.
|
|
343
|
+
* An entity hierarchy can use either STI (discriminatorColumn) or TPT (inheritance: 'tpt'),
|
|
344
|
+
* but not both.
|
|
345
|
+
*
|
|
346
|
+
* Note: This validation runs before `initTablePerTypeInheritance` sets `inheritanceType`,
|
|
347
|
+
* so we check the raw `inheritance` option from the decorator/schema.
|
|
348
|
+
*/
|
|
349
|
+
validateInheritanceStrategies(discovered) {
|
|
350
|
+
const checkedRoots = new Set();
|
|
351
|
+
for (const meta of discovered) {
|
|
352
|
+
if (meta.embeddable) {
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
const root = meta.root;
|
|
356
|
+
if (checkedRoots.has(root)) {
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
checkedRoots.add(root);
|
|
360
|
+
const hasSTI = !!root.discriminatorColumn;
|
|
361
|
+
const hasTPT = root.inheritanceType === 'tpt' || root.inheritance === 'tpt';
|
|
362
|
+
if (hasSTI && hasTPT) {
|
|
363
|
+
throw MetadataError.mixedInheritanceStrategies(root, meta);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
339
367
|
}
|
package/metadata/types.d.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import type { AnyEntity, Constructor, EntityName, AnyString, CheckCallback, GeneratedColumnCallback, FormulaCallback, FilterQuery, Dictionary, AutoPath, EntityClass, IndexCallback, ObjectQuery } from '../typings.js';
|
|
1
|
+
import type { AnyEntity, Constructor, EntityName, AnyString, CheckCallback, GeneratedColumnCallback, FormulaCallback, FilterQuery, Dictionary, AutoPath, EntityClass, IndexCallback, ObjectQuery, Raw } from '../typings.js';
|
|
2
2
|
import type { Cascade, LoadStrategy, DeferMode, QueryOrderMap, EmbeddedPrefixMode } from '../enums.js';
|
|
3
3
|
import type { Type, types } from '../types/index.js';
|
|
4
4
|
import type { EntityManager } from '../EntityManager.js';
|
|
5
5
|
import type { FilterOptions, FindOptions } from '../drivers/IDatabaseDriver.js';
|
|
6
6
|
import type { SerializeOptions } from '../serialization/EntitySerializer.js';
|
|
7
|
-
import type { Raw } from '../utils/RawQueryFragment.js';
|
|
8
7
|
export type EntityOptions<T, E = T extends EntityClass<infer P> ? P : T> = {
|
|
9
8
|
/** Override default collection/table name. Alias for `collection`. */
|
|
10
9
|
tableName?: string;
|
|
@@ -18,6 +17,8 @@ export type EntityOptions<T, E = T extends EntityClass<infer P> ? P : T> = {
|
|
|
18
17
|
discriminatorMap?: Dictionary<string>;
|
|
19
18
|
/** For {@doclink inheritance-mapping#single-table-inheritance | Single Table Inheritance}. */
|
|
20
19
|
discriminatorValue?: number | string;
|
|
20
|
+
/** Set inheritance strategy: 'tpt' for {@doclink inheritance-mapping#table-per-type-inheritance-tpt | Table-Per-Type} inheritance. When set on the root entity, each entity in the hierarchy gets its own table with a FK from child PK to parent PK. */
|
|
21
|
+
inheritance?: 'tpt';
|
|
21
22
|
/** Enforce use of constructor when creating managed entity instances. */
|
|
22
23
|
forceConstructor?: boolean;
|
|
23
24
|
/** Specify constructor parameters to be used in `em.create` or when `forceConstructor` is enabled. Those should be names of declared entity properties in the same order as your constructor uses them. The ORM tries to infer those automatically, use this option in case the inference fails. */
|
|
@@ -137,7 +138,7 @@ export interface PropertyOptions<Owner> {
|
|
|
137
138
|
/**
|
|
138
139
|
* For generated columns. This will be appended to the column type after the `generated always` clause.
|
|
139
140
|
*/
|
|
140
|
-
generated?: string | GeneratedColumnCallback<Owner>;
|
|
141
|
+
generated?: string | Raw | GeneratedColumnCallback<Owner>;
|
|
141
142
|
/**
|
|
142
143
|
* Set column as nullable for {@link https://mikro-orm.io/docs/schema-generator Schema Generator}.
|
|
143
144
|
*/
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikro-orm/core",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "7.0.0-dev.
|
|
4
|
+
"version": "7.0.0-dev.229",
|
|
5
5
|
"description": "TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.",
|
|
6
6
|
"exports": {
|
|
7
7
|
"./package.json": "./package.json",
|
package/typings.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ import type { Raw } from './utils/RawQueryFragment.js';
|
|
|
18
18
|
import type { EntityManager } from './EntityManager.js';
|
|
19
19
|
import type { EventSubscriber } from './events/EventSubscriber.js';
|
|
20
20
|
import type { FilterOptions, FindOneOptions, FindOptions, LoadHint } from './drivers/IDatabaseDriver.js';
|
|
21
|
+
export type { Raw };
|
|
21
22
|
export type Constructor<T = unknown> = new (...args: any[]) => T;
|
|
22
23
|
export type Dictionary<T = any> = {
|
|
23
24
|
[k: string]: T;
|
|
@@ -400,22 +401,46 @@ export type EntityDTO<T, C extends TypeConfig = never> = {
|
|
|
400
401
|
};
|
|
401
402
|
type TargetKeys<T> = T extends EntityClass<infer P> ? keyof P : keyof T;
|
|
402
403
|
type PropertyName<T> = IsUnknown<T> extends false ? TargetKeys<T> : string;
|
|
403
|
-
type
|
|
404
|
+
export type FormulaTable = {
|
|
405
|
+
alias: string;
|
|
404
406
|
name: string;
|
|
405
407
|
schema?: string;
|
|
408
|
+
qualifiedName: string;
|
|
406
409
|
toString: () => string;
|
|
407
410
|
};
|
|
408
|
-
|
|
409
|
-
|
|
411
|
+
/**
|
|
412
|
+
* Table reference for schema callbacks (indexes, checks, generated columns).
|
|
413
|
+
* Unlike FormulaTable, this has no alias since schema generation doesn't use query aliases.
|
|
414
|
+
*/
|
|
415
|
+
export type SchemaTable = {
|
|
410
416
|
name: string;
|
|
411
417
|
schema?: string;
|
|
412
418
|
qualifiedName: string;
|
|
413
419
|
toString: () => string;
|
|
414
420
|
};
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
421
|
+
/**
|
|
422
|
+
* Column mapping for formula callbacks. Maps property names to fully-qualified alias.fieldName.
|
|
423
|
+
* Has toString() returning the main alias for backwards compatibility with old formula syntax.
|
|
424
|
+
* @example
|
|
425
|
+
* // New recommended syntax - use cols.propName for fully-qualified references
|
|
426
|
+
* formula: cols => `${cols.firstName} || ' ' || ${cols.lastName}`
|
|
427
|
+
*
|
|
428
|
+
* // Old syntax still works - cols.toString() returns the alias
|
|
429
|
+
* formula: cols => `${cols}.first_name || ' ' || ${cols}.last_name`
|
|
430
|
+
*/
|
|
431
|
+
export type FormulaColumns<T> = Record<PropertyName<T>, string> & {
|
|
432
|
+
toString(): string;
|
|
433
|
+
};
|
|
434
|
+
/**
|
|
435
|
+
* Column mapping for schema callbacks (indexes, checks, generated columns).
|
|
436
|
+
* Maps property names to field names. For TPT entities, only includes properties
|
|
437
|
+
* that belong to the current table (not inherited properties from parent tables).
|
|
438
|
+
*/
|
|
439
|
+
export type SchemaColumns<T> = Record<PropertyName<T>, string>;
|
|
440
|
+
export type IndexCallback<T> = (columns: Record<PropertyName<T>, string>, table: SchemaTable, indexName: string) => string | Raw;
|
|
441
|
+
export type FormulaCallback<T> = (columns: FormulaColumns<T>, table: FormulaTable) => string | Raw;
|
|
442
|
+
export type CheckCallback<T> = (columns: Record<PropertyName<T>, string>, table: SchemaTable) => string | Raw;
|
|
443
|
+
export type GeneratedColumnCallback<T> = (columns: Record<PropertyName<T>, string>, table: SchemaTable) => string | Raw;
|
|
419
444
|
export interface CheckConstraint<T = any> {
|
|
420
445
|
name?: string;
|
|
421
446
|
property?: string;
|
|
@@ -430,7 +455,7 @@ export interface EntityProperty<Owner = any, Target = any> {
|
|
|
430
455
|
runtimeType: 'number' | 'string' | 'boolean' | 'bigint' | 'Buffer' | 'Date' | 'object' | 'any' | AnyString;
|
|
431
456
|
targetMeta?: EntityMetadata<Target>;
|
|
432
457
|
columnTypes: string[];
|
|
433
|
-
generated?: string | GeneratedColumnCallback<Owner>;
|
|
458
|
+
generated?: string | Raw | GeneratedColumnCallback<Owner>;
|
|
434
459
|
customType?: Type<any>;
|
|
435
460
|
customTypes: (Type<any> | undefined)[];
|
|
436
461
|
hasConvertToJSValueSQL: boolean;
|
|
@@ -533,7 +558,18 @@ export declare class EntityMetadata<Entity = any, Class extends EntityCtor<Entit
|
|
|
533
558
|
removeProperty(name: string, sync?: boolean): void;
|
|
534
559
|
getPrimaryProps(flatten?: boolean): EntityProperty<Entity>[];
|
|
535
560
|
getPrimaryProp(): EntityProperty<Entity>;
|
|
536
|
-
|
|
561
|
+
/**
|
|
562
|
+
* Creates a mapping from property names to field names.
|
|
563
|
+
* @param alias - Optional alias to prefix field names. Can be a string (same for all) or a function (per-property).
|
|
564
|
+
* When provided, also adds toString() returning the alias for backwards compatibility with formulas.
|
|
565
|
+
* @param toStringAlias - Optional alias to return from toString(). Defaults to `alias` when it's a string.
|
|
566
|
+
*/
|
|
567
|
+
createColumnMappingObject(alias?: string | ((prop: EntityProperty<Entity>) => string), toStringAlias?: string): FormulaColumns<Entity>;
|
|
568
|
+
/**
|
|
569
|
+
* Creates a column mapping for schema callbacks (indexes, checks, generated columns).
|
|
570
|
+
* For TPT entities, only includes properties that belong to the current table (ownProps).
|
|
571
|
+
*/
|
|
572
|
+
createSchemaColumnMappingObject(): SchemaColumns<Entity>;
|
|
537
573
|
get tableName(): string;
|
|
538
574
|
set tableName(name: string);
|
|
539
575
|
get uniqueName(): string;
|
|
@@ -628,6 +664,22 @@ export interface EntityMetadata<Entity = any, Class extends EntityCtor<Entity> =
|
|
|
628
664
|
definedProperties: Dictionary;
|
|
629
665
|
/** For polymorphic M:N pivot tables, maps discriminator values to entity classes */
|
|
630
666
|
polymorphicDiscriminatorMap?: Dictionary<EntityClass>;
|
|
667
|
+
/** Inheritance type: 'sti' (Single Table Inheritance) or 'tpt' (Table-Per-Type). Only set on root entities. */
|
|
668
|
+
inheritanceType?: 'sti' | 'tpt';
|
|
669
|
+
/** For TPT: direct parent entity metadata (the entity this one extends). */
|
|
670
|
+
tptParent?: EntityMetadata;
|
|
671
|
+
/** For TPT: direct child entities (entities that extend this one). */
|
|
672
|
+
tptChildren?: EntityMetadata[];
|
|
673
|
+
/** For TPT: all non-abstract descendants, sorted by depth (deepest first). Precomputed during discovery. */
|
|
674
|
+
allTPTDescendants?: EntityMetadata[];
|
|
675
|
+
/** For TPT: synthetic property representing the join to the parent table (child PK → parent PK). */
|
|
676
|
+
tptParentProp?: EntityProperty;
|
|
677
|
+
/** For TPT: inverse of tptParentProp, used for joining from parent to child (parent PK → child PK). */
|
|
678
|
+
tptInverseProp?: EntityProperty;
|
|
679
|
+
/** For TPT: virtual discriminator property name (computed at query time, not persisted). */
|
|
680
|
+
tptDiscriminatorColumn?: string;
|
|
681
|
+
/** For TPT: properties defined only in THIS entity (not inherited from parent). */
|
|
682
|
+
ownProps?: EntityProperty<Entity>[];
|
|
631
683
|
hasTriggers?: boolean;
|
|
632
684
|
/** @internal can be used for computed numeric cache keys */
|
|
633
685
|
readonly _id: number;
|
|
@@ -997,4 +1049,3 @@ export interface EntitySchemaWithMeta<TName extends string = string, TTableName
|
|
|
997
1049
|
export type InferEntity<Schema> = Schema extends {
|
|
998
1050
|
'~entity': infer E;
|
|
999
1051
|
} ? E : Schema extends EntitySchema<infer Entity> ? Entity : Schema extends EntityClass<infer Entity> ? Entity : Schema;
|
|
1000
|
-
export {};
|
package/typings.js
CHANGED
|
@@ -64,8 +64,52 @@ export class EntityMetadata {
|
|
|
64
64
|
getPrimaryProp() {
|
|
65
65
|
return this.properties[this.primaryKeys[0]];
|
|
66
66
|
}
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Creates a mapping from property names to field names.
|
|
69
|
+
* @param alias - Optional alias to prefix field names. Can be a string (same for all) or a function (per-property).
|
|
70
|
+
* When provided, also adds toString() returning the alias for backwards compatibility with formulas.
|
|
71
|
+
* @param toStringAlias - Optional alias to return from toString(). Defaults to `alias` when it's a string.
|
|
72
|
+
*/
|
|
73
|
+
createColumnMappingObject(alias, toStringAlias) {
|
|
74
|
+
const resolveAlias = typeof alias === 'function' ? alias : () => alias;
|
|
75
|
+
const defaultAlias = toStringAlias ?? (typeof alias === 'string' ? alias : undefined);
|
|
76
|
+
const result = Object.values(this.properties).reduce((o, prop) => {
|
|
77
|
+
if (prop.fieldNames) {
|
|
78
|
+
const propAlias = resolveAlias(prop);
|
|
79
|
+
o[prop.name] = propAlias ? `${propAlias}.${prop.fieldNames[0]}` : prop.fieldNames[0];
|
|
80
|
+
}
|
|
81
|
+
return o;
|
|
82
|
+
}, {});
|
|
83
|
+
// Add toString() for backwards compatibility when alias is provided
|
|
84
|
+
Object.defineProperty(result, 'toString', {
|
|
85
|
+
value: () => defaultAlias ?? '',
|
|
86
|
+
enumerable: false,
|
|
87
|
+
});
|
|
88
|
+
// Wrap in Proxy to detect old formula signature usage where the first param was FormulaTable.
|
|
89
|
+
// If user accesses `.alias` or `.qualifiedName` (FormulaTable-only properties), warn them.
|
|
90
|
+
const warnedProps = new Set(['alias', 'qualifiedName']);
|
|
91
|
+
return new Proxy(result, {
|
|
92
|
+
get(target, prop, receiver) {
|
|
93
|
+
if (typeof prop === 'string' && warnedProps.has(prop) && !(prop in target)) {
|
|
94
|
+
// eslint-disable-next-line no-console
|
|
95
|
+
console.warn(`[MikroORM] Detected old formula callback signature. The first parameter is now 'columns', not 'table'. ` +
|
|
96
|
+
`Accessing '.${prop}' on the columns object will return undefined. ` +
|
|
97
|
+
`Update your formula: formula(cols => quote\`\${cols.propName} ...\`). See the v7 upgrade guide.`);
|
|
98
|
+
}
|
|
99
|
+
return Reflect.get(target, prop, receiver);
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Creates a column mapping for schema callbacks (indexes, checks, generated columns).
|
|
105
|
+
* For TPT entities, only includes properties that belong to the current table (ownProps).
|
|
106
|
+
*/
|
|
107
|
+
createSchemaColumnMappingObject() {
|
|
108
|
+
// For TPT entities, only include properties that belong to this entity's table
|
|
109
|
+
const props = this.inheritanceType === 'tpt' && this.ownProps
|
|
110
|
+
? this.ownProps
|
|
111
|
+
: Object.values(this.properties);
|
|
112
|
+
return props.reduce((o, prop) => {
|
|
69
113
|
if (prop.fieldNames) {
|
|
70
114
|
o[prop.name] = prop.fieldNames[0];
|
|
71
115
|
}
|
|
@@ -149,8 +193,8 @@ export class EntityMetadata {
|
|
|
149
193
|
this.props.forEach(prop => this.initIndexes(prop));
|
|
150
194
|
}
|
|
151
195
|
this.definedProperties = this.trackingProps.reduce((o, prop) => {
|
|
152
|
-
const
|
|
153
|
-
if (
|
|
196
|
+
const hasInverse = (prop.inversedBy || prop.mappedBy) && !prop.mapToPk;
|
|
197
|
+
if (hasInverse) {
|
|
154
198
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
155
199
|
const meta = this;
|
|
156
200
|
o[prop.name] = {
|
|
@@ -19,6 +19,8 @@ export interface ChangeSet<T> {
|
|
|
19
19
|
payload: EntityDictionary<T>;
|
|
20
20
|
persisted: boolean;
|
|
21
21
|
originalEntity?: EntityData<T>;
|
|
22
|
+
/** For TPT: changesets for parent tables, ordered from immediate parent to root */
|
|
23
|
+
tptChangeSets?: ChangeSet<T>[];
|
|
22
24
|
}
|
|
23
25
|
export declare enum ChangeSetType {
|
|
24
26
|
CREATE = "create",
|
|
@@ -43,6 +43,11 @@ export declare class ChangeSetPersister {
|
|
|
43
43
|
* so we use a single query in case of both versioning and default values is used.
|
|
44
44
|
*/
|
|
45
45
|
private reloadVersionValues;
|
|
46
|
+
/**
|
|
47
|
+
* For TPT child tables, resolve EntityIdentifier values in PK fields.
|
|
48
|
+
* The parent table insert assigns the actual PK value, which the child table references.
|
|
49
|
+
*/
|
|
50
|
+
private resolveTPTIdentifiers;
|
|
46
51
|
private processProperty;
|
|
47
52
|
/**
|
|
48
53
|
* Maps values returned via `returning` statement (postgres) or the inserted id (other sql drivers).
|
|
@@ -116,6 +116,7 @@ export class ChangeSetPersister {
|
|
|
116
116
|
options = this.prepareOptions(meta, options, {
|
|
117
117
|
convertCustomTypes: false,
|
|
118
118
|
});
|
|
119
|
+
this.resolveTPTIdentifiers(changeSet);
|
|
119
120
|
// Use changeSet's own meta for STI entities to get correct field mappings
|
|
120
121
|
const res = await this.driver.nativeInsertMany(changeSet.meta.class, [changeSet.payload], options);
|
|
121
122
|
if (!wrapped.hasPrimaryKey()) {
|
|
@@ -154,6 +155,9 @@ export class ChangeSetPersister {
|
|
|
154
155
|
convertCustomTypes: false,
|
|
155
156
|
processCollections: false,
|
|
156
157
|
});
|
|
158
|
+
for (const changeSet of changeSets) {
|
|
159
|
+
this.resolveTPTIdentifiers(changeSet);
|
|
160
|
+
}
|
|
157
161
|
const res = await this.driver.nativeInsertMany(meta.class, changeSets.map(cs => cs.payload), options);
|
|
158
162
|
for (let i = 0; i < changeSets.length; i++) {
|
|
159
163
|
const changeSet = changeSets[i];
|
|
@@ -370,6 +374,21 @@ export class ChangeSetPersister {
|
|
|
370
374
|
Object.assign(changeSet.payload, data); // merge to the changeset payload, so it gets saved to the entity snapshot
|
|
371
375
|
}
|
|
372
376
|
}
|
|
377
|
+
/**
|
|
378
|
+
* For TPT child tables, resolve EntityIdentifier values in PK fields.
|
|
379
|
+
* The parent table insert assigns the actual PK value, which the child table references.
|
|
380
|
+
*/
|
|
381
|
+
resolveTPTIdentifiers(changeSet) {
|
|
382
|
+
if (changeSet.meta.inheritanceType !== 'tpt' || !changeSet.meta.tptParent) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
for (const pk of changeSet.meta.primaryKeys) {
|
|
386
|
+
const value = changeSet.payload[pk];
|
|
387
|
+
if (value instanceof EntityIdentifier) {
|
|
388
|
+
changeSet.payload[pk] = value.getValue();
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
373
392
|
processProperty(changeSet, prop) {
|
|
374
393
|
const meta = changeSet.meta;
|
|
375
394
|
const value = changeSet.payload[prop.name]; // for inline embeddables
|
|
@@ -96,6 +96,11 @@ export declare class UnitOfWork {
|
|
|
96
96
|
getOrphanRemoveStack(): Set<AnyEntity>;
|
|
97
97
|
getChangeSetPersister(): ChangeSetPersister;
|
|
98
98
|
private findNewEntities;
|
|
99
|
+
/**
|
|
100
|
+
* For TPT inheritance, creates separate changesets for each table in the hierarchy.
|
|
101
|
+
* Uses the same entity instance for all changesets - only the metadata and payload differ.
|
|
102
|
+
*/
|
|
103
|
+
private createTPTChangeSets;
|
|
99
104
|
/**
|
|
100
105
|
* Returns `true` when the change set should be skipped as it will be empty after the extra update.
|
|
101
106
|
*/
|
|
@@ -559,7 +559,68 @@ export class UnitOfWork {
|
|
|
559
559
|
}
|
|
560
560
|
const changeSet = this.changeSetComputer.computeChangeSet(entity);
|
|
561
561
|
if (changeSet && !this.checkUniqueProps(changeSet)) {
|
|
562
|
-
|
|
562
|
+
// For TPT child entities, create changesets for each table in hierarchy
|
|
563
|
+
if (wrapped.__meta.inheritanceType === 'tpt' && wrapped.__meta.tptParent) {
|
|
564
|
+
this.createTPTChangeSets(entity, changeSet);
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
this.changeSets.set(entity, changeSet);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* For TPT inheritance, creates separate changesets for each table in the hierarchy.
|
|
573
|
+
* Uses the same entity instance for all changesets - only the metadata and payload differ.
|
|
574
|
+
*/
|
|
575
|
+
createTPTChangeSets(entity, originalChangeSet) {
|
|
576
|
+
const meta = helper(entity).__meta;
|
|
577
|
+
const isCreate = originalChangeSet.type === ChangeSetType.CREATE;
|
|
578
|
+
let current = meta;
|
|
579
|
+
let leafCs;
|
|
580
|
+
const parentChangeSets = [];
|
|
581
|
+
while (current) {
|
|
582
|
+
const isRoot = !current.tptParent;
|
|
583
|
+
const payload = {};
|
|
584
|
+
for (const prop of current.ownProps) {
|
|
585
|
+
if (prop.name in originalChangeSet.payload) {
|
|
586
|
+
payload[prop.name] = originalChangeSet.payload[prop.name];
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
// For CREATE on non-root tables, include the PK (EntityIdentifier for deferred resolution)
|
|
590
|
+
if (isCreate && !isRoot) {
|
|
591
|
+
const wrapped = helper(entity);
|
|
592
|
+
const identifier = wrapped.__identifier;
|
|
593
|
+
const identifiers = Array.isArray(identifier) ? identifier : [identifier];
|
|
594
|
+
for (let i = 0; i < current.primaryKeys.length; i++) {
|
|
595
|
+
const pk = current.primaryKeys[i];
|
|
596
|
+
payload[pk] = identifiers[i] ?? originalChangeSet.payload[pk];
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
if (!isCreate && Object.keys(payload).length === 0) {
|
|
600
|
+
current = current.tptParent;
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
const cs = new ChangeSet(entity, originalChangeSet.type, payload, current);
|
|
604
|
+
if (current === meta) {
|
|
605
|
+
cs.originalEntity = originalChangeSet.originalEntity;
|
|
606
|
+
leafCs = cs;
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
parentChangeSets.push(cs);
|
|
610
|
+
}
|
|
611
|
+
current = current.tptParent;
|
|
612
|
+
}
|
|
613
|
+
// When only parent properties changed (UPDATE), leaf payload is empty—create a stub anchor
|
|
614
|
+
if (!leafCs && parentChangeSets.length > 0) {
|
|
615
|
+
leafCs = new ChangeSet(entity, originalChangeSet.type, {}, meta);
|
|
616
|
+
leafCs.originalEntity = originalChangeSet.originalEntity;
|
|
617
|
+
}
|
|
618
|
+
// Store the leaf changeset in the main map (entity as key), with parent CSs attached
|
|
619
|
+
if (leafCs) {
|
|
620
|
+
if (parentChangeSets.length > 0) {
|
|
621
|
+
leafCs.tptChangeSets = parentChangeSets;
|
|
622
|
+
}
|
|
623
|
+
this.changeSets.set(entity, leafCs);
|
|
563
624
|
}
|
|
564
625
|
}
|
|
565
626
|
/**
|
|
@@ -876,8 +937,20 @@ export class UnitOfWork {
|
|
|
876
937
|
}
|
|
877
938
|
return false;
|
|
878
939
|
});
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
const refEntity = Reference.unwrapReference(ref);
|
|
943
|
+
// For mapToPk properties, the value is a primitive (string/array), not an entity
|
|
944
|
+
if (!Utils.isEntity(refEntity)) {
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
// For TPT entities, check if the ROOT table's changeset has been persisted
|
|
948
|
+
// (since the FK is to the root table, not the concrete entity's table)
|
|
949
|
+
let cs = this.changeSets.get(refEntity);
|
|
950
|
+
if (cs?.tptChangeSets?.length) {
|
|
951
|
+
// Root table changeset is the last one (ordered immediate parent → root)
|
|
952
|
+
cs = cs.tptChangeSets[cs.tptChangeSets.length - 1];
|
|
879
953
|
}
|
|
880
|
-
const cs = this.changeSets.get(Reference.unwrapReference(ref));
|
|
881
954
|
const isScheduledForInsert = cs?.type === ChangeSetType.CREATE && !cs.persisted;
|
|
882
955
|
if (isScheduledForInsert) {
|
|
883
956
|
this.scheduleExtraUpdate(changeSet, [prop]);
|
|
@@ -1000,12 +1073,23 @@ export class UnitOfWork {
|
|
|
1000
1073
|
[ChangeSetType.UPDATE_EARLY]: new Map(),
|
|
1001
1074
|
[ChangeSetType.DELETE_EARLY]: new Map(),
|
|
1002
1075
|
};
|
|
1003
|
-
|
|
1076
|
+
const addToGroup = (cs) => {
|
|
1077
|
+
// Skip stub TPT changesets with empty payload (e.g. leaf with no own-property changes on UPDATE)
|
|
1078
|
+
if ((cs.type === ChangeSetType.UPDATE || cs.type === ChangeSetType.UPDATE_EARLY) && !Utils.hasObjectKeys(cs.payload)) {
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1004
1081
|
const group = groups[cs.type];
|
|
1005
|
-
const
|
|
1082
|
+
const groupKey = cs.meta.inheritanceType === 'tpt' ? cs.meta : cs.rootMeta;
|
|
1083
|
+
const classGroup = group.get(groupKey) ?? [];
|
|
1006
1084
|
classGroup.push(cs);
|
|
1007
|
-
if (!group.has(
|
|
1008
|
-
group.set(
|
|
1085
|
+
if (!group.has(groupKey)) {
|
|
1086
|
+
group.set(groupKey, classGroup);
|
|
1087
|
+
}
|
|
1088
|
+
};
|
|
1089
|
+
for (const cs of this.changeSets.values()) {
|
|
1090
|
+
addToGroup(cs);
|
|
1091
|
+
for (const parentCs of cs.tptChangeSets ?? []) {
|
|
1092
|
+
addToGroup(parentCs);
|
|
1009
1093
|
}
|
|
1010
1094
|
}
|
|
1011
1095
|
return groups;
|
|
@@ -1013,7 +1097,17 @@ export class UnitOfWork {
|
|
|
1013
1097
|
getCommitOrder() {
|
|
1014
1098
|
const calc = new CommitOrderCalculator();
|
|
1015
1099
|
const set = new Set();
|
|
1016
|
-
this.changeSets.forEach(cs =>
|
|
1100
|
+
this.changeSets.forEach(cs => {
|
|
1101
|
+
if (cs.meta.inheritanceType === 'tpt') {
|
|
1102
|
+
set.add(cs.meta);
|
|
1103
|
+
for (const parentCs of cs.tptChangeSets ?? []) {
|
|
1104
|
+
set.add(parentCs.meta);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
else {
|
|
1108
|
+
set.add(cs.rootMeta);
|
|
1109
|
+
}
|
|
1110
|
+
});
|
|
1017
1111
|
set.forEach(meta => calc.addNode(meta._id));
|
|
1018
1112
|
for (const meta of set) {
|
|
1019
1113
|
for (const prop of meta.relations) {
|
|
@@ -1026,6 +1120,10 @@ export class UnitOfWork {
|
|
|
1026
1120
|
calc.discoverProperty(prop, meta._id);
|
|
1027
1121
|
}
|
|
1028
1122
|
}
|
|
1123
|
+
// For TPT, parent table must be inserted BEFORE child tables
|
|
1124
|
+
if (meta.inheritanceType === 'tpt' && meta.tptParent && set.has(meta.tptParent)) {
|
|
1125
|
+
calc.addDependency(meta.tptParent._id, meta._id, 1);
|
|
1126
|
+
}
|
|
1029
1127
|
}
|
|
1030
1128
|
return calc.sort().map(id => this.metadata.getById(id));
|
|
1031
1129
|
}
|
|
@@ -92,14 +92,23 @@ export class AbstractSchemaGenerator {
|
|
|
92
92
|
getOrderedMetadata(schema) {
|
|
93
93
|
const metadata = [...this.metadata.getAll().values()].filter(meta => {
|
|
94
94
|
const isRootEntity = meta.root.class === meta.class;
|
|
95
|
-
|
|
95
|
+
const isTPTChild = meta.inheritanceType === 'tpt' && meta.tptParent;
|
|
96
|
+
return (isRootEntity || isTPTChild) && !meta.embeddable && !meta.virtual;
|
|
96
97
|
});
|
|
97
98
|
const calc = new CommitOrderCalculator();
|
|
98
|
-
metadata.forEach(meta =>
|
|
99
|
+
metadata.forEach(meta => {
|
|
100
|
+
const nodeId = meta.inheritanceType === 'tpt' && meta.tptParent ? meta._id : meta.root._id;
|
|
101
|
+
calc.addNode(nodeId);
|
|
102
|
+
});
|
|
99
103
|
let meta = metadata.pop();
|
|
100
104
|
while (meta) {
|
|
105
|
+
const nodeId = meta.inheritanceType === 'tpt' && meta.tptParent ? meta._id : meta.root._id;
|
|
101
106
|
for (const prop of meta.relations) {
|
|
102
|
-
calc.discoverProperty(prop,
|
|
107
|
+
calc.discoverProperty(prop, nodeId);
|
|
108
|
+
}
|
|
109
|
+
if (meta.inheritanceType === 'tpt' && meta.tptParent) {
|
|
110
|
+
const parentId = meta.tptParent._id;
|
|
111
|
+
calc.addDependency(parentId, nodeId, 1);
|
|
103
112
|
}
|
|
104
113
|
meta = metadata.pop();
|
|
105
114
|
}
|
|
@@ -215,7 +215,7 @@ export class EntityComparator {
|
|
|
215
215
|
const context = new Map();
|
|
216
216
|
context.set('clone', clone);
|
|
217
217
|
context.set('cloneEmbeddable', (o) => this.platform.cloneEmbeddable(o)); // do not clone prototypes
|
|
218
|
-
if (meta.discriminatorValue) {
|
|
218
|
+
if (meta.root.inheritanceType === 'sti' && meta.discriminatorValue) {
|
|
219
219
|
lines.push(` ret${this.wrap(meta.root.discriminatorColumn)} = '${meta.discriminatorValue}'`);
|
|
220
220
|
}
|
|
221
221
|
const getRootProperty = (prop) => prop.embedded ? getRootProperty(meta.properties[prop.embedded[0]]) : prop;
|
package/utils/Utils.js
CHANGED
|
@@ -123,7 +123,7 @@ export function parseJsonSafe(value) {
|
|
|
123
123
|
}
|
|
124
124
|
export class Utils {
|
|
125
125
|
static PK_SEPARATOR = '~~~';
|
|
126
|
-
static #ORM_VERSION = '7.0.0-dev.
|
|
126
|
+
static #ORM_VERSION = '7.0.0-dev.229';
|
|
127
127
|
/**
|
|
128
128
|
* Checks if the argument is instance of `Object`. Returns false for arrays.
|
|
129
129
|
*/
|