@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.
@@ -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: (TT | Reference<TT>)[]): number;
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: TT) => boolean), ...entities: (TT | Reference<TT>)[]): number;
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. */
@@ -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
- const entity = this.#em.getEntityFactory().create(prop.targetMeta.class, item, {
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,
@@ -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
- prop.discriminatorValue = prop2.discriminatorValue;
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
- // For polymorphic M:N, use discriminator base name for FK column (e.g., taggable_id instead of post_id)
486
- if (prop.polymorphic && prop.discriminator) {
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
- prop.inverseJoinColumns ??= this.initManyToOneFieldName(prop, meta2.root.className);
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
- // For polymorphic M:N, create discriminator column and polymorphic FK
725
- if (prop.polymorphic && prop.discriminatorColumn) {
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
- if (isToOne) {
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) {
@@ -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<Target>[];
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.6",
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
@@ -48,6 +48,7 @@ export class EntityMetadata {
48
48
  this.indexes = [];
49
49
  this.uniques = [];
50
50
  this.checks = [];
51
+ this.triggers = [];
51
52
  this.referencingProperties = [];
52
53
  this.concurrencyCheckKeys = new Set();
53
54
  Object.assign(this, meta);
@@ -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
  */
@@ -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.
@@ -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.6';
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
  */
@@ -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;