@mikro-orm/core 7.1.0-dev.6 → 7.1.0-dev.8
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/entity/Collection.d.ts +2 -2
- package/entity/EntityLoader.js +6 -1
- package/entity/defineEntity.d.ts +11 -2
- package/errors.d.ts +2 -0
- package/errors.js +4 -0
- package/index.d.ts +1 -1
- package/metadata/MetadataDiscovery.d.ts +11 -0
- package/metadata/MetadataDiscovery.js +95 -8
- package/metadata/MetadataValidator.js +9 -0
- package/metadata/types.d.ts +4 -2
- package/naming-strategy/AbstractNamingStrategy.d.ts +1 -1
- package/naming-strategy/NamingStrategy.d.ts +1 -1
- package/package.json +1 -1
- package/typings.d.ts +25 -0
- package/typings.js +1 -0
- package/utils/AbstractMigrator.d.ts +11 -1
- package/utils/AbstractMigrator.js +203 -0
- package/utils/QueryHelper.d.ts +16 -0
- package/utils/QueryHelper.js +15 -0
- package/utils/Utils.js +1 -1
- package/utils/fs-utils.d.ts +2 -0
- package/utils/fs-utils.js +7 -1
package/entity/Collection.d.ts
CHANGED
|
@@ -48,14 +48,14 @@ export declare class Collection<T extends object, O extends object = object> {
|
|
|
48
48
|
/** Serializes the collection items to plain JSON objects. Returns an empty array if not initialized. */
|
|
49
49
|
toJSON<TT extends T>(): EntityDTO<TT>[];
|
|
50
50
|
/** Adds one or more items to the collection, propagating the change to the inverse side. Returns the number of items added. */
|
|
51
|
-
add<TT extends T>(entity: TT | Reference<TT> | Iterable<TT | Reference<TT>>, ...entities: (
|
|
51
|
+
add<TT extends T>(entity: TT | Reference<TT> | Iterable<TT | Reference<TT>>, ...entities: (T | Reference<T>)[]): number;
|
|
52
52
|
/**
|
|
53
53
|
* Remove specified item(s) from the collection. Note that removing item from collection does not necessarily imply deleting the target entity,
|
|
54
54
|
* it means we are disconnecting the relation - removing items from collection, not removing entities from database - `Collection.remove()`
|
|
55
55
|
* is not the same as `em.remove()`. If we want to delete the entity by removing it from collection, we need to enable `orphanRemoval: true`,
|
|
56
56
|
* which tells the ORM we don't want orphaned entities to exist, so we know those should be removed.
|
|
57
57
|
*/
|
|
58
|
-
remove<TT extends T>(entity: TT | Reference<TT> | Iterable<TT | Reference<TT>> | ((item:
|
|
58
|
+
remove<TT extends T>(entity: TT | Reference<TT> | Iterable<TT | Reference<TT>> | ((item: T) => boolean), ...entities: (T | Reference<T>)[]): number;
|
|
59
59
|
/** Checks whether the collection contains the given item. */
|
|
60
60
|
contains<TT extends T>(item: TT | Reference<TT>, check?: boolean): boolean;
|
|
61
61
|
/** Returns the number of items in the collection. Throws if the collection is not initialized. */
|
package/entity/EntityLoader.js
CHANGED
|
@@ -587,6 +587,7 @@ export class EntityLoader {
|
|
|
587
587
|
}
|
|
588
588
|
const map = await this.#driver.loadFromPivotTable(prop, ids, where, orderBy, this.#em.getTransactionContext(), options2, pivotJoin);
|
|
589
589
|
const children = [];
|
|
590
|
+
const isUnionTargetMN = QueryHelper.isUnionTargetPolymorphic(prop);
|
|
590
591
|
for (let i = 0; i < filtered.length; i++) {
|
|
591
592
|
const entity = filtered[i];
|
|
592
593
|
const items = map[Utils.getPrimaryKeyHash(ids[i])].map(item => {
|
|
@@ -596,7 +597,11 @@ export class EntityLoader {
|
|
|
596
597
|
schema: options.schema ?? this.#em.config.get('schema'),
|
|
597
598
|
});
|
|
598
599
|
}
|
|
599
|
-
|
|
600
|
+
// Union-target items carry their concrete class via `constructor` — dispatch to the right factory call.
|
|
601
|
+
const targetClass = isUnionTargetMN && item.constructor !== Object
|
|
602
|
+
? item.constructor
|
|
603
|
+
: prop.targetMeta.class;
|
|
604
|
+
const entity = this.#em.getEntityFactory().create(targetClass, item, {
|
|
600
605
|
refresh,
|
|
601
606
|
merge: true,
|
|
602
607
|
convertCustomTypes: true,
|
package/entity/defineEntity.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { EntityManager } from '../EntityManager.js';
|
|
2
2
|
import type { ColumnType, PropertyOptions, ReferenceOptions, EnumOptions, EmbeddedOptions, ManyToOneOptions, OneToManyOptions, OneToOneOptions, ManyToManyOptions, IndexColumnOptions } from '../metadata/types.js';
|
|
3
|
-
import type { AnyString, GeneratedColumnCallback, Constructor, CheckCallback, FilterQuery, EntityName, Dictionary, EntityMetadata, PrimaryKeyProp, EntityRepositoryType, Hidden, Opt, Primary, EntityClass, EntitySchemaWithMeta, InferEntity, MaybeReturnType, Ref, IndexCallback, FormulaCallback, EntityCtor, IsNever, IWrappedEntity, DefineConfig, Config, MaybePromise, IndexHints } from '../typings.js';
|
|
3
|
+
import type { AnyString, GeneratedColumnCallback, Constructor, CheckCallback, FilterQuery, EntityName, Dictionary, EntityMetadata, PrimaryKeyProp, EntityRepositoryType, Hidden, Opt, Primary, EntityClass, EntitySchemaWithMeta, InferEntity, MaybeReturnType, Ref, IndexCallback, TriggerCallback, FormulaCallback, EntityCtor, IsNever, IWrappedEntity, DefineConfig, Config, MaybePromise, IndexHints } from '../typings.js';
|
|
4
4
|
import type { Raw } from '../utils/RawQueryFragment.js';
|
|
5
5
|
import type { ScalarReference } from './Reference.js';
|
|
6
6
|
import type { SerializeOptions } from '../serialization/EntitySerializer.js';
|
|
@@ -576,7 +576,7 @@ export type PropertyBuilders = {
|
|
|
576
576
|
/** Own keys + base entity keys (when TBase is not `never`). Guards against `keyof never = string | number | symbol`. */
|
|
577
577
|
type AllKeys<TProperties, TBase> = keyof TProperties | (IsNever<TBase> extends true ? never : keyof TBase);
|
|
578
578
|
/** Metadata descriptor for `defineEntity()`, combining entity options with property definitions. */
|
|
579
|
-
export interface EntityMetadataWithProperties<TName extends string, TTableName extends string, TProperties extends Record<string, any>, TPK extends (keyof TProperties)[] | undefined = undefined, TBase = never, TRepository = never, TForceObject extends boolean = false> extends Omit<Partial<EntityMetadata<InferEntityFromProperties<TProperties, TPK, TBase, TRepository>>>, 'properties' | 'extends' | 'primaryKeys' | 'hooks' | 'discriminatorColumn' | 'versionProperty' | 'concurrencyCheckKeys' | 'serializedPrimaryKey' | 'indexes' | 'uniques' | 'repository' | 'filters' | 'orderBy'> {
|
|
579
|
+
export interface EntityMetadataWithProperties<TName extends string, TTableName extends string, TProperties extends Record<string, any>, TPK extends (keyof TProperties)[] | undefined = undefined, TBase = never, TRepository = never, TForceObject extends boolean = false> extends Omit<Partial<EntityMetadata<InferEntityFromProperties<TProperties, TPK, TBase, TRepository>>>, 'properties' | 'extends' | 'primaryKeys' | 'hooks' | 'discriminatorColumn' | 'versionProperty' | 'concurrencyCheckKeys' | 'serializedPrimaryKey' | 'indexes' | 'uniques' | 'triggers' | 'repository' | 'filters' | 'orderBy'> {
|
|
580
580
|
name: TName;
|
|
581
581
|
tableName?: TTableName;
|
|
582
582
|
extends?: {
|
|
@@ -629,6 +629,15 @@ export interface EntityMetadataWithProperties<TName extends string, TTableName e
|
|
|
629
629
|
fillFactor?: number;
|
|
630
630
|
disabled?: boolean;
|
|
631
631
|
}[];
|
|
632
|
+
triggers?: {
|
|
633
|
+
name?: string;
|
|
634
|
+
timing: 'before' | 'after' | 'instead of';
|
|
635
|
+
events: ('insert' | 'update' | 'delete' | 'truncate')[];
|
|
636
|
+
forEach?: 'row' | 'statement';
|
|
637
|
+
body?: string | Raw | TriggerCallback<InferEntityFromProperties<TProperties, TPK, TBase>>;
|
|
638
|
+
when?: string;
|
|
639
|
+
expression?: string;
|
|
640
|
+
}[];
|
|
632
641
|
}
|
|
633
642
|
/** Defines an entity schema using property builders, with full type inference from the property definitions. */
|
|
634
643
|
export declare function defineEntity<const TName extends string, const TTableName extends string, const TProperties extends Record<string, any>, const TPK extends (keyof TProperties)[] | undefined = undefined, const TBase = never, const TRepository = never, const TForceObject extends boolean = false>(meta: EntityMetadataWithProperties<TName, TTableName, TProperties, TPK, TBase, TRepository, TForceObject>): EntitySchemaWithMeta<TName, TTableName, InferEntityFromProperties<TProperties, TPK, TBase, TRepository, TForceObject>, TBase, TProperties>;
|
package/errors.d.ts
CHANGED
|
@@ -74,6 +74,8 @@ export declare class MetadataError<T extends AnyEntity = AnyEntity> extends Vali
|
|
|
74
74
|
static viewEntityWithoutExpression(meta: EntityMetadata): MetadataError;
|
|
75
75
|
static mixedInheritanceStrategies(root: EntityMetadata, child: EntityMetadata): MetadataError;
|
|
76
76
|
static tptNotSupportedByDriver(meta: EntityMetadata): MetadataError;
|
|
77
|
+
/** Thrown when database triggers are defined on an entity using a driver that does not support them. */
|
|
78
|
+
static triggersNotSupportedByDriver(meta: EntityMetadata): MetadataError;
|
|
77
79
|
private static fromMessage;
|
|
78
80
|
}
|
|
79
81
|
/** Error thrown when an entity lookup fails to find the expected result. */
|
package/errors.js
CHANGED
|
@@ -243,6 +243,10 @@ export class MetadataError extends ValidationError {
|
|
|
243
243
|
static tptNotSupportedByDriver(meta) {
|
|
244
244
|
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.`);
|
|
245
245
|
}
|
|
246
|
+
/** Thrown when database triggers are defined on an entity using a driver that does not support them. */
|
|
247
|
+
static triggersNotSupportedByDriver(meta) {
|
|
248
|
+
return new MetadataError(`Entity ${meta.className} defines database triggers which are not supported by the current driver. Triggers are only available with SQL drivers.`);
|
|
249
|
+
}
|
|
246
250
|
static fromMessage(meta, prop, message) {
|
|
247
251
|
return new MetadataError(`${meta.className}.${prop.name} ${message}`);
|
|
248
252
|
}
|
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, EntityName, IndexHints, } from './typings.js';
|
|
6
|
-
export type { CompiledFunctions, Constructor, ConnectionType, Dictionary, Primary, IPrimaryKey, ObjectQuery, FilterQuery, IWrappedEntity, InferEntityName, EntityData, Highlighter, MaybePromise, AnyEntity, EntityClass, EntityProperty, PopulateOptions, Populate, Loaded, New, LoadedReference, LoadedCollection, IMigrator, IMigrationGenerator, MigratorEvent, GetRepository, MigrationObject, DeepPartial, PrimaryProperty, Cast, IsUnknown, EntityDictionary, EntityDTO, EntityDTOFlat, EntityDTOProp, SerializeDTO, MigrationDiff, GenerateOptions, FilterObject, IndexFilterQuery, ExtractIndexHints, IndexName, IndexColumns, WithUsingOptions, IMigrationRunner, IEntityGenerator, ISeedManager, SeederObject, IMigratorStorage, RequiredEntityData, CheckCallback, IndexCallback, FormulaCallback, FormulaTable, SchemaTable, SchemaColumns, SimpleColumnMeta, Rel, Ref, ScalarRef, EntityRef, ISchemaGenerator, MigrationInfo, 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, PopulateHintOptions, Prefixes, } from './typings.js';
|
|
6
|
+
export type { CompiledFunctions, Constructor, ConnectionType, Dictionary, Primary, IPrimaryKey, ObjectQuery, FilterQuery, IWrappedEntity, InferEntityName, EntityData, Highlighter, MaybePromise, AnyEntity, EntityClass, EntityProperty, PopulateOptions, Populate, Loaded, New, LoadedReference, LoadedCollection, IMigrator, IMigrationGenerator, MigratorEvent, GetRepository, MigrationObject, DeepPartial, PrimaryProperty, Cast, IsUnknown, EntityDictionary, EntityDTO, EntityDTOFlat, EntityDTOProp, SerializeDTO, MigrationDiff, GenerateOptions, FilterObject, IndexFilterQuery, ExtractIndexHints, IndexName, IndexColumns, WithUsingOptions, IMigrationRunner, IEntityGenerator, ISeedManager, SeederObject, IMigratorStorage, RequiredEntityData, CheckCallback, TriggerCallback, IndexCallback, FormulaCallback, FormulaTable, SchemaTable, SchemaColumns, SimpleColumnMeta, Rel, Ref, ScalarRef, EntityRef, ISchemaGenerator, MigrationInfo, 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, TriggerDef, GeneratedColumnCallback, FilterDef, EntityCtor, Subquery, PopulateHintOptions, Prefixes, } from './typings.js';
|
|
7
7
|
export * from './enums.js';
|
|
8
8
|
export * from './errors.js';
|
|
9
9
|
export * from './exceptions.js';
|
|
@@ -54,6 +54,16 @@ export declare class MetadataDiscovery {
|
|
|
54
54
|
* Define properties for a polymorphic pivot table.
|
|
55
55
|
*/
|
|
56
56
|
private definePolymorphicPivotProperties;
|
|
57
|
+
/**
|
|
58
|
+
* Mirror of definePolymorphicPivotProperties for union-target M:N
|
|
59
|
+
* (e.g. Post.attachments -> Image | Video via shared pivot with a target-side discriminator).
|
|
60
|
+
*
|
|
61
|
+
* Pivot shape:
|
|
62
|
+
* (owner_fk..., discriminator_column, target_fk...)
|
|
63
|
+
* - owner side is a normal M:1 to the single owner entity
|
|
64
|
+
* - target side is a discriminator column + per-target-type virtual M:1 relations
|
|
65
|
+
*/
|
|
66
|
+
private defineUnionTargetPolymorphicPivotProperties;
|
|
57
67
|
/**
|
|
58
68
|
* Create a virtual M:1 relation from pivot to a polymorphic owner entity.
|
|
59
69
|
* This enables single-query join loading for inverse-side polymorphic M:N.
|
|
@@ -105,6 +115,7 @@ export declare class MetadataDiscovery {
|
|
|
105
115
|
private initAutoincrement;
|
|
106
116
|
private createSchemaTable;
|
|
107
117
|
private initCheckConstraints;
|
|
118
|
+
private initTriggers;
|
|
108
119
|
private initGeneratedColumn;
|
|
109
120
|
private getDefaultVersionValue;
|
|
110
121
|
private inferDefaultValue;
|
|
@@ -159,6 +159,7 @@ export class MetadataDiscovery {
|
|
|
159
159
|
forEachProp((m, p) => this.initGeneratedColumn(m, p));
|
|
160
160
|
filtered.forEach(meta => this.initAutoincrement(meta)); // once again after we init custom types
|
|
161
161
|
filtered.forEach(meta => this.initCheckConstraints(meta));
|
|
162
|
+
filtered.forEach(meta => this.initTriggers(meta));
|
|
162
163
|
forEachProp((_m, p) => {
|
|
163
164
|
this.initDefaultValue(p);
|
|
164
165
|
this.inferTypeFromDefault(p);
|
|
@@ -479,17 +480,28 @@ export class MetadataDiscovery {
|
|
|
479
480
|
prop.polymorphic = prop2.polymorphic;
|
|
480
481
|
prop.discriminator = prop2.discriminator;
|
|
481
482
|
prop.discriminatorColumn = prop2.discriminatorColumn;
|
|
482
|
-
|
|
483
|
+
// For a union-target pivot each inverse side sits on one specific target class, so its
|
|
484
|
+
// discriminator value is that class's tableName. For Rails-style, prop2 has a single fixed value.
|
|
485
|
+
prop.discriminatorValue = QueryHelper.isUnionTargetPolymorphic(prop2) ? meta.tableName : prop2.discriminatorValue;
|
|
483
486
|
}
|
|
484
487
|
prop.referencedColumnNames ??= Utils.flatten(meta.primaryKeys.map(primaryKey => meta.properties[primaryKey].fieldNames));
|
|
485
|
-
//
|
|
486
|
-
|
|
488
|
+
// Union-target polymorphic M:N: owner side is fixed (real FK), target side uses discriminator-derived names.
|
|
489
|
+
const isUnionTargetMN = QueryHelper.isUnionTargetPolymorphic(prop);
|
|
490
|
+
if (prop.polymorphic && prop.discriminator && !isUnionTargetMN) {
|
|
491
|
+
// Rails-style: owner side is polymorphic, uses discriminator base name (e.g. taggable_id instead of post_id)
|
|
487
492
|
prop.joinColumns ??= prop.referencedColumnNames.map(referencedColumnName => this.#namingStrategy.joinKeyColumnName(prop.discriminator, referencedColumnName, prop.referencedColumnNames.length > 1));
|
|
488
493
|
}
|
|
489
494
|
else {
|
|
490
495
|
prop.joinColumns ??= prop.referencedColumnNames.map(referencedColumnName => this.#namingStrategy.joinKeyColumnName(meta.root.className, referencedColumnName, meta.compositePK));
|
|
491
496
|
}
|
|
492
|
-
|
|
497
|
+
if (isUnionTargetMN) {
|
|
498
|
+
// Target side uses discriminator base name (e.g. attachable_id — shared across Image/Video)
|
|
499
|
+
const targetPkCols = Utils.flatten(meta2.primaryKeys.map(pk => meta2.properties[pk].fieldNames));
|
|
500
|
+
prop.inverseJoinColumns ??= targetPkCols.map(fieldName => this.#namingStrategy.joinKeyColumnName(prop.discriminator, fieldName, targetPkCols.length > 1));
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
prop.inverseJoinColumns ??= this.initManyToOneFieldName(prop, meta2.root.className);
|
|
504
|
+
}
|
|
493
505
|
}
|
|
494
506
|
isExplicitTableName(meta) {
|
|
495
507
|
return meta.tableName !== this.#namingStrategy.classToTableName(meta.className);
|
|
@@ -583,6 +595,24 @@ export class MetadataDiscovery {
|
|
|
583
595
|
if (prop.inversedBy) {
|
|
584
596
|
prop.targetMeta.properties[prop.inversedBy].pivotEntity = pivotMeta.class;
|
|
585
597
|
}
|
|
598
|
+
// Propagate pivotEntity to ALL inverse collections using mappedBy pointing at this
|
|
599
|
+
// owner prop. Covers three cases:
|
|
600
|
+
// - regular inverse (Tag.posts mappedBy Post.tags) — handled by inversedBy above
|
|
601
|
+
// - union-target inverse (Image.posts mappedBy Post.attachments) — on each polymorph target
|
|
602
|
+
// - merged inverse (Tag.owners mappedBy [Post,Video].tags) — union collection on the target
|
|
603
|
+
const inverseCandidates = QueryHelper.isUnionTargetPolymorphic(prop)
|
|
604
|
+
? prop.polymorphTargets
|
|
605
|
+
: [prop.targetMeta];
|
|
606
|
+
for (const targetMeta of inverseCandidates) {
|
|
607
|
+
for (const inverseProp of Object.values(targetMeta.properties)) {
|
|
608
|
+
if (inverseProp.kind === ReferenceKind.MANY_TO_MANY &&
|
|
609
|
+
inverseProp.mappedBy === prop.name &&
|
|
610
|
+
!inverseProp.pivotEntity) {
|
|
611
|
+
inverseProp.pivotEntity = pivotMeta.class;
|
|
612
|
+
inverseProp.pivotTable = pivotMeta.tableName;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
586
616
|
return pivotMeta;
|
|
587
617
|
});
|
|
588
618
|
}
|
|
@@ -721,8 +751,12 @@ export class MetadataDiscovery {
|
|
|
721
751
|
}
|
|
722
752
|
}
|
|
723
753
|
}
|
|
724
|
-
//
|
|
725
|
-
if (prop.
|
|
754
|
+
// Union-target polymorphic M:N: discriminator + target FK share the pivot across multiple target types
|
|
755
|
+
if (prop.discriminatorColumn && QueryHelper.isUnionTargetPolymorphic(prop)) {
|
|
756
|
+
this.defineUnionTargetPolymorphicPivotProperties(pivotMeta2, meta, prop);
|
|
757
|
+
}
|
|
758
|
+
else if (prop.polymorphic && prop.discriminatorColumn) {
|
|
759
|
+
// Rails-style polymorphic M:N: multiple owners share the pivot, single target type
|
|
726
760
|
this.definePolymorphicPivotProperties(pivotMeta2, meta, prop, targetMeta);
|
|
727
761
|
}
|
|
728
762
|
else {
|
|
@@ -809,6 +843,33 @@ export class MetadataDiscovery {
|
|
|
809
843
|
pivotMeta.polymorphicDiscriminatorMap ??= {};
|
|
810
844
|
pivotMeta.polymorphicDiscriminatorMap[prop.discriminatorValue] = meta.class;
|
|
811
845
|
}
|
|
846
|
+
/**
|
|
847
|
+
* Mirror of definePolymorphicPivotProperties for union-target M:N
|
|
848
|
+
* (e.g. Post.attachments -> Image | Video via shared pivot with a target-side discriminator).
|
|
849
|
+
*
|
|
850
|
+
* Pivot shape:
|
|
851
|
+
* (owner_fk..., discriminator_column, target_fk...)
|
|
852
|
+
* - owner side is a normal M:1 to the single owner entity
|
|
853
|
+
* - target side is a discriminator column + per-target-type virtual M:1 relations
|
|
854
|
+
*/
|
|
855
|
+
defineUnionTargetPolymorphicPivotProperties(pivotMeta, meta, prop) {
|
|
856
|
+
const discriminatorColumn = prop.discriminatorColumn;
|
|
857
|
+
const targets = prop.polymorphTargets;
|
|
858
|
+
pivotMeta.properties[meta.name + '_owner'] = this.definePivotProperty(prop, meta.name + '_owner', meta.class, prop.discriminator, true, false);
|
|
859
|
+
const discriminatorProp = this.createPivotScalarProperty(discriminatorColumn, [this.#platform.getVarcharTypeDeclarationSQL(prop)], [discriminatorColumn], { type: 'string', primary: true, nullable: false });
|
|
860
|
+
this.initFieldName(discriminatorProp);
|
|
861
|
+
pivotMeta.properties[discriminatorColumn] = discriminatorProp;
|
|
862
|
+
const firstTargetColumnTypes = this.getPrimaryKeyColumnTypes(targets[0]);
|
|
863
|
+
pivotMeta.properties[prop.discriminator] = this.createPivotScalarProperty(prop.discriminator, firstTargetColumnTypes, [...prop.inverseJoinColumns], { type: targets[0].className, primary: true, nullable: false });
|
|
864
|
+
pivotMeta.polymorphicDiscriminatorMap ??= {};
|
|
865
|
+
for (const targetMeta of targets) {
|
|
866
|
+
const relationName = `${prop.discriminator}_${targetMeta.tableName}`;
|
|
867
|
+
const relation = this.definePolymorphicOwnerRelation(prop, relationName, targetMeta);
|
|
868
|
+
relation.joinColumns = relation.fieldNames = relation.ownColumns = [...prop.inverseJoinColumns];
|
|
869
|
+
pivotMeta.properties[relationName] = relation;
|
|
870
|
+
pivotMeta.polymorphicDiscriminatorMap[targetMeta.tableName] = targetMeta.class;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
812
873
|
/**
|
|
813
874
|
* Create a virtual M:1 relation from pivot to a polymorphic owner entity.
|
|
814
875
|
* This enables single-query join loading for inverse-side polymorphic M:N.
|
|
@@ -961,6 +1022,7 @@ export class MetadataDiscovery {
|
|
|
961
1022
|
meta.indexes = Utils.unique([...base.indexes, ...meta.indexes]);
|
|
962
1023
|
meta.uniques = Utils.unique([...base.uniques, ...meta.uniques]);
|
|
963
1024
|
meta.checks = Utils.unique([...base.checks, ...meta.checks]);
|
|
1025
|
+
meta.triggers = Utils.unique([...base.triggers, ...meta.triggers]);
|
|
964
1026
|
const pks = Object.values(meta.properties)
|
|
965
1027
|
.filter(p => p.primary)
|
|
966
1028
|
.map(p => p.name);
|
|
@@ -1045,11 +1107,15 @@ export class MetadataDiscovery {
|
|
|
1045
1107
|
prop.discriminatorColumn ??= this.#namingStrategy.discriminatorColumnName(prop.discriminator);
|
|
1046
1108
|
prop.createForeignKeyConstraint = false;
|
|
1047
1109
|
const isToOne = [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind);
|
|
1048
|
-
|
|
1110
|
+
const isUnionTargetMN = prop.kind === ReferenceKind.MANY_TO_MANY && Array.isArray(prop.target);
|
|
1111
|
+
if (isToOne || isUnionTargetMN) {
|
|
1049
1112
|
const types = prop.type.split(/ ?\| ?/);
|
|
1050
1113
|
prop.polymorphTargets = discovered.filter(m => types.includes(m.className) && !m.embeddable);
|
|
1051
1114
|
prop.targetMeta = prop.polymorphTargets[0];
|
|
1052
1115
|
prop.referencedPKs = prop.targetMeta?.primaryKeys;
|
|
1116
|
+
if (isUnionTargetMN && prop.polymorphTargets.length < 2) {
|
|
1117
|
+
throw new MetadataError(`${meta.className}.${prop.name} union-target polymorphic M:N requires at least two target entity types; use a regular M:N relation for a single target.`);
|
|
1118
|
+
}
|
|
1053
1119
|
}
|
|
1054
1120
|
if (prop.discriminatorMap) {
|
|
1055
1121
|
const normalizedMap = {};
|
|
@@ -1065,7 +1131,7 @@ export class MetadataDiscovery {
|
|
|
1065
1131
|
}
|
|
1066
1132
|
prop.discriminatorMap = normalizedMap;
|
|
1067
1133
|
}
|
|
1068
|
-
else if (isToOne) {
|
|
1134
|
+
else if (isToOne || isUnionTargetMN) {
|
|
1069
1135
|
prop.discriminatorMap = {};
|
|
1070
1136
|
const tableNameToTarget = new Map();
|
|
1071
1137
|
for (const target of prop.polymorphTargets) {
|
|
@@ -1520,6 +1586,27 @@ export class MetadataDiscovery {
|
|
|
1520
1586
|
}
|
|
1521
1587
|
}
|
|
1522
1588
|
}
|
|
1589
|
+
initTriggers(meta) {
|
|
1590
|
+
if (meta.triggers.length === 0) {
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
const columns = meta.createSchemaColumnMappingObject();
|
|
1594
|
+
const table = this.createSchemaTable(meta);
|
|
1595
|
+
for (const trigger of meta.triggers) {
|
|
1596
|
+
if (trigger.body && trigger.expression) {
|
|
1597
|
+
throw new MetadataError(`Trigger "${trigger.name ?? '(unnamed)'}" on entity ${meta.className} defines both 'body' and 'expression'. Use one or the other.`);
|
|
1598
|
+
}
|
|
1599
|
+
if (!trigger.body && !trigger.expression) {
|
|
1600
|
+
throw new MetadataError(`Trigger "${trigger.name ?? '(unnamed)'}" on entity ${meta.className} must define either 'body' or 'expression'.`);
|
|
1601
|
+
}
|
|
1602
|
+
trigger.name ??= this.#namingStrategy.indexName(meta.tableName, trigger.events, 'trigger');
|
|
1603
|
+
trigger.forEach ??= 'row';
|
|
1604
|
+
if (trigger.body instanceof Function) {
|
|
1605
|
+
trigger.body = trigger.body(columns, table);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
meta.hasTriggers = true;
|
|
1609
|
+
}
|
|
1523
1610
|
initGeneratedColumn(meta, prop) {
|
|
1524
1611
|
if (!prop.generated && prop.columnTypes) {
|
|
1525
1612
|
const match = /(.*) generated always as (.*)/i.exec(prop.columnTypes[0]);
|
|
@@ -171,6 +171,15 @@ export class MetadataValidator {
|
|
|
171
171
|
}
|
|
172
172
|
validatePolymorphicTargets(meta, prop) {
|
|
173
173
|
const targets = prop.polymorphTargets;
|
|
174
|
+
// Union-target M:N stores one scalar target FK per pivot row, so composite-PK targets
|
|
175
|
+
// can't round-trip through this schema.
|
|
176
|
+
if (prop.kind === ReferenceKind.MANY_TO_MANY && targets.length > 1) {
|
|
177
|
+
for (const target of targets) {
|
|
178
|
+
if (target.compositePK) {
|
|
179
|
+
throw MetadataError.incompatiblePolymorphicTargets(meta, prop, targets[0], target, `${target.className} has a composite primary key; union-target polymorphic M:N does not support composite-PK targets.`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
174
183
|
// Validate targetKey exists and is compatible across all targets
|
|
175
184
|
if (prop.targetKey) {
|
|
176
185
|
for (const target of targets) {
|
package/metadata/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AnyEntity, Constructor, EntityName, AnyString, CheckCallback, GeneratedColumnCallback, FormulaCallback, FilterQuery, Dictionary, AutoPath, EntityClass, IndexCallback, ObjectQuery, Raw } from '../typings.js';
|
|
1
|
+
import type { AnyEntity, Constructor, EntityName, AnyString, CheckCallback, GeneratedColumnCallback, FormulaCallback, FilterQuery, Dictionary, AutoPath, EntityClass, IndexCallback, ObjectQuery, Raw, TriggerDef } 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';
|
|
@@ -57,6 +57,8 @@ export type EntityOptions<T, E = T extends EntityClass<infer P> ? P : T> = {
|
|
|
57
57
|
};
|
|
58
58
|
/** Used to make ORM aware of externally defined triggers. This is needed for MS SQL Server multi inserts, ignored in other dialects. */
|
|
59
59
|
hasTriggers?: boolean;
|
|
60
|
+
/** Database triggers to create for this entity's table. (SQL drivers only) */
|
|
61
|
+
triggers?: TriggerDef<E>[];
|
|
60
62
|
/** SQL query that maps to a {@doclink virtual-entities | virtual entity}, or for view entities, the view definition. */
|
|
61
63
|
expression?: string | ((em: any, where: ObjectQuery<E>, options: FindOptions<E, any, any, any>, stream?: boolean) => object);
|
|
62
64
|
/** Set {@doclink repositories#custom-repository | custom repository class}. */
|
|
@@ -340,7 +342,7 @@ export interface PropertyOptions<Owner> {
|
|
|
340
342
|
}
|
|
341
343
|
export interface ReferenceOptions<Owner, Target> extends PropertyOptions<Owner> {
|
|
342
344
|
/** Set target entity type. For polymorphic relations, pass an array of entity types. */
|
|
343
|
-
entity?: () => EntityName<Target> | EntityName
|
|
345
|
+
entity?: () => EntityName<Target> | EntityName[];
|
|
344
346
|
/** Set what actions on owning entity should be cascaded to the relationship. Defaults to [Cascade.PERSIST, Cascade.MERGE] (see {@doclink cascading}). */
|
|
345
347
|
cascade?: Cascade[];
|
|
346
348
|
/** Always load the relationship. Discouraged for use with to-many relations for performance reasons. */
|
|
@@ -4,7 +4,7 @@ import { type ReferenceKind } from '../enums.js';
|
|
|
4
4
|
export declare abstract class AbstractNamingStrategy implements NamingStrategy {
|
|
5
5
|
getClassName(file: string, separator?: string): string;
|
|
6
6
|
classToMigrationName(timestamp: string, customMigrationName?: string): string;
|
|
7
|
-
indexName(tableName: string, columns: string[], type: 'primary' | 'foreign' | 'unique' | 'index' | 'sequence' | 'check' | 'default'): string;
|
|
7
|
+
indexName(tableName: string, columns: string[], type: 'primary' | 'foreign' | 'unique' | 'index' | 'sequence' | 'check' | 'default' | 'trigger'): string;
|
|
8
8
|
/**
|
|
9
9
|
* @inheritDoc
|
|
10
10
|
*/
|
|
@@ -75,7 +75,7 @@ export interface NamingStrategy {
|
|
|
75
75
|
/**
|
|
76
76
|
* Returns key/constraint name for the given type. Some drivers might not support all the types (e.g. mysql and sqlite enforce the PK name).
|
|
77
77
|
*/
|
|
78
|
-
indexName(tableName: string, columns: string[], type: 'primary' | 'foreign' | 'unique' | 'index' | 'sequence' | 'check' | 'default'): string;
|
|
78
|
+
indexName(tableName: string, columns: string[], type: 'primary' | 'foreign' | 'unique' | 'index' | 'sequence' | 'check' | 'default' | 'trigger'): string;
|
|
79
79
|
/**
|
|
80
80
|
* Returns alias name for given entity. The alias needs to be unique across the query, which is by default
|
|
81
81
|
* ensured via appended index parameter. It is optional to use it as long as you ensure it will be unique.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikro-orm/core",
|
|
3
|
-
"version": "7.1.0-dev.
|
|
3
|
+
"version": "7.1.0-dev.8",
|
|
4
4
|
"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.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"data-mapper",
|
package/typings.d.ts
CHANGED
|
@@ -656,6 +656,8 @@ export type IndexCallback<T> = (columns: Record<PropertyName<T>, string>, table:
|
|
|
656
656
|
export type FormulaCallback<T> = (columns: FormulaColumns<T>, table: FormulaTable) => string | Raw;
|
|
657
657
|
/** Callback for CHECK constraint expressions. Receives column mappings and table info. */
|
|
658
658
|
export type CheckCallback<T> = (columns: Record<PropertyName<T>, string>, table: SchemaTable) => string | Raw;
|
|
659
|
+
/** Callback for trigger body expressions. Receives column mappings and table info. */
|
|
660
|
+
export type TriggerCallback<T> = (columns: Record<PropertyName<T>, string>, table: SchemaTable) => string | Raw;
|
|
659
661
|
/** Callback for generated (computed) column expressions. Receives column mappings and table info. */
|
|
660
662
|
export type GeneratedColumnCallback<T> = (columns: Record<PropertyName<T>, string>, table: SchemaTable) => string | Raw;
|
|
661
663
|
/** Definition of a CHECK constraint on a table or property. */
|
|
@@ -664,6 +666,23 @@ export interface CheckConstraint<T = any> {
|
|
|
664
666
|
property?: string;
|
|
665
667
|
expression: string | Raw | CheckCallback<T>;
|
|
666
668
|
}
|
|
669
|
+
/** Definition of a database trigger on a table. */
|
|
670
|
+
export interface TriggerDef<T = any> {
|
|
671
|
+
/** Trigger name. Auto-generated if omitted. */
|
|
672
|
+
name?: string;
|
|
673
|
+
/** When the trigger fires relative to the event. */
|
|
674
|
+
timing: 'before' | 'after' | 'instead of';
|
|
675
|
+
/** Which DML events activate the trigger. */
|
|
676
|
+
events: ('insert' | 'update' | 'delete' | 'truncate')[];
|
|
677
|
+
/** Whether the trigger fires once per row or per statement. Defaults to `'row'`. */
|
|
678
|
+
forEach?: 'row' | 'statement';
|
|
679
|
+
/** SQL body of the trigger. Can be a string, Raw query, or callback receiving column name mappings. */
|
|
680
|
+
body?: string | Raw | TriggerCallback<T>;
|
|
681
|
+
/** Optional SQL WHEN condition for the trigger. */
|
|
682
|
+
when?: string;
|
|
683
|
+
/** Raw DDL escape hatch — full CREATE TRIGGER statement. Mutually exclusive with `body`. */
|
|
684
|
+
expression?: string;
|
|
685
|
+
}
|
|
667
686
|
/** Branded string that accepts any string value while preserving autocompletion for known literals. */
|
|
668
687
|
export type AnyString = string & {};
|
|
669
688
|
/** Describes a single property (column, relation, or embedded) within an entity's metadata. */
|
|
@@ -883,6 +902,7 @@ export interface EntityMetadata<Entity = any, Class extends EntityCtor<Entity> =
|
|
|
883
902
|
disabled?: boolean;
|
|
884
903
|
}[];
|
|
885
904
|
checks: CheckConstraint<Entity>[];
|
|
905
|
+
triggers: TriggerDef<Entity>[];
|
|
886
906
|
repositoryClass?: string;
|
|
887
907
|
repository: () => EntityClass<EntityRepository<any>>;
|
|
888
908
|
hooks: {
|
|
@@ -1110,6 +1130,11 @@ export interface IMigrator {
|
|
|
1110
1130
|
* Executes down migrations to the given point. Without parameter it will migrate one version down.
|
|
1111
1131
|
*/
|
|
1112
1132
|
down(options?: string | string[] | Omit<MigrateOptions, 'from'>): Promise<MigrationInfo[]>;
|
|
1133
|
+
/**
|
|
1134
|
+
* Combines multiple executed migrations into a single migration file.
|
|
1135
|
+
* Concatenates source code without touching the database schema.
|
|
1136
|
+
*/
|
|
1137
|
+
rollup(migrations?: string[]): Promise<MigrationResult>;
|
|
1113
1138
|
/**
|
|
1114
1139
|
* Registers event handler.
|
|
1115
1140
|
*/
|
package/typings.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Constructor, IMigrationGenerator, IMigrationRunner, IMigrator, IMigratorStorage, MaybePromise, Migration, MigrationInfo, MigrationRow, MigratorEvent } from '../typings.js';
|
|
1
|
+
import type { Constructor, IMigrationGenerator, IMigrationRunner, IMigrator, IMigratorStorage, MaybePromise, Migration, MigrationInfo, MigrationResult, MigrationRow, MigratorEvent } from '../typings.js';
|
|
2
2
|
import type { Transaction } from '../connections/Connection.js';
|
|
3
3
|
import type { Configuration, MigrationsOptions } from './Configuration.js';
|
|
4
4
|
import type { EntityManagerType, IDatabaseDriver } from '../drivers/IDatabaseDriver.js';
|
|
@@ -70,6 +70,16 @@ export declare abstract class AbstractMigrator<D extends IDatabaseDriver> implem
|
|
|
70
70
|
* @inheritDoc
|
|
71
71
|
*/
|
|
72
72
|
down(options?: string | string[] | Omit<MigrateOptions, 'from'>): Promise<MigrationInfo[]>;
|
|
73
|
+
/**
|
|
74
|
+
* @inheritDoc
|
|
75
|
+
*/
|
|
76
|
+
rollup(migrations?: string[]): Promise<MigrationResult>;
|
|
77
|
+
/**
|
|
78
|
+
* Extracts the body of a method from migration source code using brace counting.
|
|
79
|
+
* Returns the raw lines between the opening and closing braces, or empty string if not found.
|
|
80
|
+
* @internal
|
|
81
|
+
*/
|
|
82
|
+
private extractMethodBody;
|
|
73
83
|
abstract getStorage(): IMigratorStorage;
|
|
74
84
|
/**
|
|
75
85
|
* @inheritDoc
|
|
@@ -63,6 +63,209 @@ export class AbstractMigrator {
|
|
|
63
63
|
async down(options) {
|
|
64
64
|
return this.runMigrations('down', options);
|
|
65
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* @inheritDoc
|
|
68
|
+
*/
|
|
69
|
+
async rollup(migrations) {
|
|
70
|
+
await this.init();
|
|
71
|
+
const { fs } = await import('@mikro-orm/core/fs-utils');
|
|
72
|
+
const all = await this.discoverMigrations();
|
|
73
|
+
const executedSet = new Set(await this.storage.executed());
|
|
74
|
+
let toRollup;
|
|
75
|
+
if (migrations && migrations.length > 0) {
|
|
76
|
+
const requested = new Set(migrations.map(m => this.getMigrationFilename(m)));
|
|
77
|
+
toRollup = all.filter(m => requested.has(m.name));
|
|
78
|
+
const found = new Set(toRollup.map(m => m.name));
|
|
79
|
+
const notFound = [...requested].filter(name => !found.has(name));
|
|
80
|
+
if (notFound.length > 0) {
|
|
81
|
+
throw new Error(`Migrations not found: ${notFound.join(', ')}`);
|
|
82
|
+
}
|
|
83
|
+
const notExecuted = toRollup.filter(m => !executedSet.has(m.name));
|
|
84
|
+
if (notExecuted.length > 0) {
|
|
85
|
+
throw new Error(`Cannot roll up migrations that have not been executed: ${notExecuted.map(m => m.name).join(', ')}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
toRollup = all.filter(m => executedSet.has(m.name));
|
|
90
|
+
}
|
|
91
|
+
if (toRollup.length < 2) {
|
|
92
|
+
throw new Error('At least 2 executed migrations are required for rollup');
|
|
93
|
+
}
|
|
94
|
+
const withoutPath = toRollup.filter(m => !m.path);
|
|
95
|
+
if (withoutPath.length > 0) {
|
|
96
|
+
throw new Error(`Cannot roll up migrations without file paths (class-based migrations): ${withoutPath.map(m => m.name).join(', ')}`);
|
|
97
|
+
}
|
|
98
|
+
const upBodies = [];
|
|
99
|
+
const downBodies = [];
|
|
100
|
+
const placeholder = `__mikro_orm_rollup_${Date.now()}__`;
|
|
101
|
+
for (const migration of toRollup) {
|
|
102
|
+
const source = await fs.readFile(migration.path);
|
|
103
|
+
const upBody = this.extractMethodBody(source, 'up');
|
|
104
|
+
const downBody = this.extractMethodBody(source, 'down');
|
|
105
|
+
if (upBody) {
|
|
106
|
+
upBodies.push(` // --- merged from ${migration.name} ---\n${upBody}`);
|
|
107
|
+
}
|
|
108
|
+
if (downBody) {
|
|
109
|
+
downBodies.unshift(` // --- merged from ${migration.name} ---\n${downBody}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const diff = {
|
|
113
|
+
up: [placeholder],
|
|
114
|
+
down: downBodies.length > 0 ? [placeholder] : [],
|
|
115
|
+
};
|
|
116
|
+
const [templateCode, fileName] = await this.generator.generate(diff);
|
|
117
|
+
const placeholderRe = new RegExp(`^.*${placeholder}.*$`, 'm');
|
|
118
|
+
let code = templateCode.replace(placeholderRe, upBodies.join('\n'));
|
|
119
|
+
if (downBodies.length > 0) {
|
|
120
|
+
code = code.replace(placeholderRe, downBodies.join('\n'));
|
|
121
|
+
}
|
|
122
|
+
await fs.writeFile(fs.normalizePath(this.absolutePath, fileName), code, { flush: true });
|
|
123
|
+
const updateStorage = async () => {
|
|
124
|
+
for (const migration of toRollup) {
|
|
125
|
+
await this.storage.unlogMigration({ name: migration.name });
|
|
126
|
+
}
|
|
127
|
+
await this.storage.logMigration({ name: fileName.replace(/\.[jt]s$/, '') });
|
|
128
|
+
};
|
|
129
|
+
if (this.options.transactional) {
|
|
130
|
+
await this.driver.getConnection().transactional(async (trx) => {
|
|
131
|
+
this.storage.setMasterMigration(trx);
|
|
132
|
+
try {
|
|
133
|
+
await updateStorage();
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
this.storage.unsetMasterMigration();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
await updateStorage();
|
|
142
|
+
}
|
|
143
|
+
await Promise.all(toRollup.map(migration => fs.unlink(migration.path)));
|
|
144
|
+
return { fileName, code, diff: { up: [], down: [] } };
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Extracts the body of a method from migration source code using brace counting.
|
|
148
|
+
* Returns the raw lines between the opening and closing braces, or empty string if not found.
|
|
149
|
+
* @internal
|
|
150
|
+
*/
|
|
151
|
+
extractMethodBody(source, methodName) {
|
|
152
|
+
const lines = source.split('\n');
|
|
153
|
+
// match method declarations, not occurrences in comments/strings — require preceding whitespace or keyword
|
|
154
|
+
const methodPattern = new RegExp(`^\\s+(?:override\\s+|async\\s+)*${methodName}\\s*\\(`);
|
|
155
|
+
let methodLine = -1;
|
|
156
|
+
for (let i = 0; i < lines.length; i++) {
|
|
157
|
+
if (methodPattern.test(lines[i])) {
|
|
158
|
+
methodLine = i;
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (methodLine === -1) {
|
|
163
|
+
return '';
|
|
164
|
+
}
|
|
165
|
+
let braceCount = 0;
|
|
166
|
+
let bodyStart = -1;
|
|
167
|
+
let bodyEnd = -1;
|
|
168
|
+
let bodyStartCol = -1;
|
|
169
|
+
let bodyEndCol = -1;
|
|
170
|
+
let inBacktick = false;
|
|
171
|
+
let inBlockComment = false;
|
|
172
|
+
// stack tracks brace depth at which each template expression `${...}` was entered
|
|
173
|
+
const templateExprStack = [];
|
|
174
|
+
for (let i = methodLine; i < lines.length; i++) {
|
|
175
|
+
const line = lines[i];
|
|
176
|
+
for (let j = 0; j < line.length; j++) {
|
|
177
|
+
// handle multi-line block comments
|
|
178
|
+
if (inBlockComment) {
|
|
179
|
+
if (line[j] === '*' && j + 1 < line.length && line[j + 1] === '/') {
|
|
180
|
+
inBlockComment = false;
|
|
181
|
+
j++;
|
|
182
|
+
}
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
// handle multi-line template literals
|
|
186
|
+
if (inBacktick) {
|
|
187
|
+
if (line[j] === '\\') {
|
|
188
|
+
j++;
|
|
189
|
+
}
|
|
190
|
+
else if (line[j] === '`') {
|
|
191
|
+
inBacktick = false;
|
|
192
|
+
}
|
|
193
|
+
else if (line[j] === '$' && j + 1 < line.length && line[j + 1] === '{') {
|
|
194
|
+
// entering template expression — resume brace counting
|
|
195
|
+
templateExprStack.push(braceCount);
|
|
196
|
+
inBacktick = false;
|
|
197
|
+
j++; // skip the {
|
|
198
|
+
braceCount++;
|
|
199
|
+
}
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const ch = line[j];
|
|
203
|
+
// single/double quoted strings (single-line only)
|
|
204
|
+
if (ch === "'" || ch === '"') {
|
|
205
|
+
const quote = ch;
|
|
206
|
+
j++;
|
|
207
|
+
while (j < line.length) {
|
|
208
|
+
if (line[j] === '\\') {
|
|
209
|
+
j++;
|
|
210
|
+
}
|
|
211
|
+
else if (line[j] === quote) {
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
j++;
|
|
215
|
+
}
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
// template literal start
|
|
219
|
+
if (ch === '`') {
|
|
220
|
+
inBacktick = true;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
// single-line comment
|
|
224
|
+
if (ch === '/' && j + 1 < line.length && line[j + 1] === '/') {
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
// block comment start
|
|
228
|
+
if (ch === '/' && j + 1 < line.length && line[j + 1] === '*') {
|
|
229
|
+
inBlockComment = true;
|
|
230
|
+
j++;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (ch === '{') {
|
|
234
|
+
if (braceCount === 0) {
|
|
235
|
+
bodyStart = i;
|
|
236
|
+
bodyStartCol = j + 1;
|
|
237
|
+
}
|
|
238
|
+
braceCount++;
|
|
239
|
+
}
|
|
240
|
+
else if (ch === '}') {
|
|
241
|
+
braceCount--;
|
|
242
|
+
// closing a template expression — re-enter backtick mode
|
|
243
|
+
if (templateExprStack.length > 0 && braceCount === templateExprStack[templateExprStack.length - 1]) {
|
|
244
|
+
templateExprStack.pop();
|
|
245
|
+
inBacktick = true;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (braceCount === 0) {
|
|
249
|
+
bodyEnd = i;
|
|
250
|
+
bodyEndCol = j;
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (bodyEnd !== -1) {
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (bodyStart === -1 || bodyEnd === -1) {
|
|
260
|
+
return '';
|
|
261
|
+
}
|
|
262
|
+
// single-line method body: extract content between braces on the same line
|
|
263
|
+
if (bodyStart === bodyEnd) {
|
|
264
|
+
const content = lines[bodyStart].slice(bodyStartCol, bodyEndCol).trim();
|
|
265
|
+
return content ? ` ${content}` : '';
|
|
266
|
+
}
|
|
267
|
+
return lines.slice(bodyStart + 1, bodyEnd).join('\n');
|
|
268
|
+
}
|
|
66
269
|
/**
|
|
67
270
|
* @inheritDoc
|
|
68
271
|
*/
|
package/utils/QueryHelper.d.ts
CHANGED
|
@@ -6,6 +6,22 @@ import type { FilterOptions } from '../drivers/IDatabaseDriver.js';
|
|
|
6
6
|
/** @internal */
|
|
7
7
|
export declare class QueryHelper {
|
|
8
8
|
static readonly SUPPORTED_OPERATORS: string[];
|
|
9
|
+
/**
|
|
10
|
+
* True when the property has multiple polymorph target types. Covers two structurally-equivalent
|
|
11
|
+
* shapes routed through the same loading path:
|
|
12
|
+
* 1. Union-target owner side — `Post.attachments: Collection<Image | Video>` (one owner, many
|
|
13
|
+
* target types, shared pivot with target-side discriminator).
|
|
14
|
+
* 2. Merged inverse of Rails-style polymorphic M:N — `Tag.owners: Collection<Post | Video>`
|
|
15
|
+
* (many owner types pointing at one target, viewed from the target where "owners" looks
|
|
16
|
+
* like a union of multiple types).
|
|
17
|
+
*
|
|
18
|
+
* Both cases are loaded via `loadFromUnionTargetPolymorphicPivotTable`, which buckets pivot rows
|
|
19
|
+
* by discriminator and hydrates each target class separately.
|
|
20
|
+
*/
|
|
21
|
+
static isUnionTargetPolymorphic(prop: {
|
|
22
|
+
polymorphic?: boolean;
|
|
23
|
+
polymorphTargets?: readonly unknown[];
|
|
24
|
+
}): boolean;
|
|
9
25
|
/**
|
|
10
26
|
* Finds the discriminator value (key) for a given entity class in a discriminator map.
|
|
11
27
|
* Walks up the prototype chain so TPT subclasses resolve to their root's key.
|
package/utils/QueryHelper.js
CHANGED
|
@@ -7,6 +7,21 @@ import { isRaw, Raw } from './RawQueryFragment.js';
|
|
|
7
7
|
/** @internal */
|
|
8
8
|
export class QueryHelper {
|
|
9
9
|
static SUPPORTED_OPERATORS = ['>', '<', '<=', '>=', '!', '!='];
|
|
10
|
+
/**
|
|
11
|
+
* True when the property has multiple polymorph target types. Covers two structurally-equivalent
|
|
12
|
+
* shapes routed through the same loading path:
|
|
13
|
+
* 1. Union-target owner side — `Post.attachments: Collection<Image | Video>` (one owner, many
|
|
14
|
+
* target types, shared pivot with target-side discriminator).
|
|
15
|
+
* 2. Merged inverse of Rails-style polymorphic M:N — `Tag.owners: Collection<Post | Video>`
|
|
16
|
+
* (many owner types pointing at one target, viewed from the target where "owners" looks
|
|
17
|
+
* like a union of multiple types).
|
|
18
|
+
*
|
|
19
|
+
* Both cases are loaded via `loadFromUnionTargetPolymorphicPivotTable`, which buckets pivot rows
|
|
20
|
+
* by discriminator and hydrates each target class separately.
|
|
21
|
+
*/
|
|
22
|
+
static isUnionTargetPolymorphic(prop) {
|
|
23
|
+
return !!prop.polymorphic && (prop.polymorphTargets?.length ?? 0) > 1;
|
|
24
|
+
}
|
|
10
25
|
/**
|
|
11
26
|
* Finds the discriminator value (key) for a given entity class in a discriminator map.
|
|
12
27
|
* Walks up the prototype chain so TPT subclasses resolve to their root's key.
|
package/utils/Utils.js
CHANGED
|
@@ -141,7 +141,7 @@ export function parseJsonSafe(value) {
|
|
|
141
141
|
/** Collection of general-purpose utility methods used throughout the ORM. */
|
|
142
142
|
export class Utils {
|
|
143
143
|
static PK_SEPARATOR = '~~~';
|
|
144
|
-
static #ORM_VERSION = '7.1.0-dev.
|
|
144
|
+
static #ORM_VERSION = '7.1.0-dev.8';
|
|
145
145
|
/**
|
|
146
146
|
* Checks if the argument is instance of `Object`. Returns false for arrays.
|
|
147
147
|
*/
|
package/utils/fs-utils.d.ts
CHANGED
|
@@ -13,7 +13,9 @@ export interface FsUtils {
|
|
|
13
13
|
normalizePath(...parts: string[]): string;
|
|
14
14
|
relativePath(path: string, relativeTo: string): string;
|
|
15
15
|
absolutePath(path: string, baseDir?: string): string;
|
|
16
|
+
readFile(path: string): Promise<string>;
|
|
16
17
|
writeFile(path: string, data: string, options?: Record<string, any>): Promise<void>;
|
|
18
|
+
unlink(path: string): Promise<void>;
|
|
17
19
|
dynamicImport<T = any>(id: string): Promise<T>;
|
|
18
20
|
}
|
|
19
21
|
export declare const fs: FsUtils;
|
package/utils/fs-utils.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, globSync as nodeGlobSync, mkdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
2
|
-
import { writeFile as nodeWriteFile } from 'node:fs/promises';
|
|
2
|
+
import { readFile as nodeReadFile, unlink as nodeUnlink, writeFile as nodeWriteFile } from 'node:fs/promises';
|
|
3
3
|
import { isAbsolute, join, normalize, relative } from 'node:path';
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
5
|
import { Utils } from './Utils.js';
|
|
@@ -181,9 +181,15 @@ export const fs = {
|
|
|
181
181
|
}
|
|
182
182
|
return this.normalizePath(path);
|
|
183
183
|
},
|
|
184
|
+
async readFile(path) {
|
|
185
|
+
return nodeReadFile(path, 'utf-8');
|
|
186
|
+
},
|
|
184
187
|
async writeFile(path, data, options) {
|
|
185
188
|
await nodeWriteFile(path, data, options);
|
|
186
189
|
},
|
|
190
|
+
async unlink(path) {
|
|
191
|
+
await nodeUnlink(path);
|
|
192
|
+
},
|
|
187
193
|
async dynamicImport(id) {
|
|
188
194
|
/* v8 ignore next */
|
|
189
195
|
const specifier = id.startsWith('file://') ? id : pathToFileURL(id).href;
|