@mikro-orm/core 7.1.0-dev.5 → 7.1.0-dev.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/EntityManager.d.ts +18 -8
- package/EntityManager.js +77 -17
- package/drivers/IDatabaseDriver.d.ts +15 -1
- package/entity/Collection.d.ts +2 -2
- package/entity/EntityLoader.js +6 -1
- package/entity/EntityRepository.d.ts +16 -8
- package/entity/defineEntity.d.ts +24 -6
- package/entity/defineEntity.js +0 -6
- package/index.d.ts +2 -2
- package/index.js +1 -1
- package/metadata/MetadataDiscovery.d.ts +10 -0
- package/metadata/MetadataDiscovery.js +72 -8
- package/metadata/MetadataValidator.js +9 -0
- package/metadata/types.d.ts +1 -1
- package/package.json +1 -1
- package/platforms/Platform.d.ts +5 -0
- package/platforms/Platform.js +7 -0
- package/typings.d.ts +56 -1
- package/typings.js +10 -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/EntityManager.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { EntityLoader, type EntityLoaderOptions } from './entity/EntityLoader.js
|
|
|
7
7
|
import { Reference } from './entity/Reference.js';
|
|
8
8
|
import { UnitOfWork } from './unit-of-work/UnitOfWork.js';
|
|
9
9
|
import type { CountOptions, DeleteOptions, FilterOptions, FindAllOptions, FindByCursorOptions, FindOneOptions, FindOneOrFailOptions, FindOptions, GetReferenceOptions, IDatabaseDriver, LockOptions, NativeInsertUpdateOptions, StreamOptions, UpdateOptions, UpsertManyOptions, UpsertOptions } from './drivers/IDatabaseDriver.js';
|
|
10
|
-
import type { AnyString, ArrayElement, AutoPath, ConnectionType, Dictionary, EntityClass, EntityData, EntityDictionary, EntityDTO, EntityMetadata, EntityName, FilterDef, FilterQuery, FromEntityType, GetRepository, IHydrator, IsSubset, Loaded, MergeLoaded, MergeSelected, ObjectQuery, PopulateOptions, Primary, Ref, RequiredEntityData, UnboxArray } from './typings.js';
|
|
10
|
+
import type { AnyString, ArrayElement, AutoPath, ConnectionType, Dictionary, EntityClass, EntityData, EntityDictionary, EntityDTO, EntityMetadata, EntityName, FilterDef, FilterQuery, FromEntityType, GetRepository, IHydrator, IsSubset, Loaded, MergeLoaded, MergeSelected, ObjectQuery, PopulateOptions, Primary, Ref, RequiredEntityData, UnboxArray, IndexFilterQuery, WithUsingOptions } from './typings.js';
|
|
11
11
|
import { FlushMode, LockMode, PopulatePath, type TransactionOptions } from './enums.js';
|
|
12
12
|
import type { MetadataStorage } from './metadata/MetadataStorage.js';
|
|
13
13
|
import type { Transaction } from './connections/Connection.js';
|
|
@@ -61,7 +61,9 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
|
|
|
61
61
|
/**
|
|
62
62
|
* Finds all entities matching your `where` query. You can pass additional options via the `options` parameter.
|
|
63
63
|
*/
|
|
64
|
-
find<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never>(entityName: EntityName<Entity>, where: FilterQuery<NoInfer<Entity
|
|
64
|
+
find<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never, Using extends string = never>(entityName: EntityName<Entity>, where: [Using] extends [never] ? FilterQuery<NoInfer<Entity>> : IndexFilterQuery<NoInfer<Entity>, Using>, options?: FindOptions<Entity, Hint, Fields, Excludes> & {
|
|
65
|
+
using?: Using | Using[];
|
|
66
|
+
}): Promise<Loaded<Entity, Hint, Fields, Excludes>[]>;
|
|
65
67
|
/**
|
|
66
68
|
* Finds all entities and returns an async iterable (async generator) that yields results one by one.
|
|
67
69
|
* The results are merged and mapped to entity instances, without adding them to the identity map.
|
|
@@ -79,11 +81,11 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
|
|
|
79
81
|
* }
|
|
80
82
|
* ```
|
|
81
83
|
*/
|
|
82
|
-
stream<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never>(entityName: EntityName<Entity>, options?: StreamOptions<NoInfer<Entity>, Hint, Fields, Excludes>): AsyncIterableIterator<Loaded<Entity, Hint, Fields, Excludes>>;
|
|
84
|
+
stream<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never, Using extends string = never>(entityName: EntityName<Entity>, options?: WithUsingOptions<StreamOptions<NoInfer<Entity>, Hint, Fields, Excludes>, NoInfer<Entity>, Using>): AsyncIterableIterator<Loaded<Entity, Hint, Fields, Excludes>>;
|
|
83
85
|
/**
|
|
84
86
|
* Finds all entities of given type, optionally matching the `where` condition provided in the `options` parameter.
|
|
85
87
|
*/
|
|
86
|
-
findAll<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never>(entityName: EntityName<Entity>, options?: FindAllOptions<NoInfer<Entity>, Hint, Fields, Excludes>): Promise<Loaded<Entity, Hint, Fields, Excludes>[]>;
|
|
88
|
+
findAll<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never, Using extends string = never>(entityName: EntityName<Entity>, options?: WithUsingOptions<FindAllOptions<NoInfer<Entity>, Hint, Fields, Excludes>, NoInfer<Entity>, Using>): Promise<Loaded<Entity, Hint, Fields, Excludes>[]>;
|
|
87
89
|
private getPopulateWhere;
|
|
88
90
|
/**
|
|
89
91
|
* Registers global filter to this entity manager. Global filters are enabled by default (unless disabled via last parameter).
|
|
@@ -130,7 +132,9 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
|
|
|
130
132
|
* Calls `em.find()` and `em.count()` with the same arguments (where applicable) and returns the results as tuple
|
|
131
133
|
* where the first element is the array of entities, and the second is the count.
|
|
132
134
|
*/
|
|
133
|
-
findAndCount<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never>(entityName: EntityName<Entity>, where: FilterQuery<NoInfer<Entity
|
|
135
|
+
findAndCount<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never, Using extends string = never>(entityName: EntityName<Entity>, where: [Using] extends [never] ? FilterQuery<NoInfer<Entity>> : IndexFilterQuery<NoInfer<Entity>, Using>, options?: FindOptions<Entity, Hint, Fields, Excludes> & {
|
|
136
|
+
using?: Using | Using[];
|
|
137
|
+
}): Promise<[Loaded<Entity, Hint, Fields, Excludes>[], number]>;
|
|
134
138
|
/**
|
|
135
139
|
* Calls `em.find()` and `em.count()` with the same arguments (where applicable) and returns the results as {@apilink Cursor} object.
|
|
136
140
|
* Supports `before`, `after`, `first` and `last` options while disallowing `limit` and `offset`. Explicit `orderBy` option
|
|
@@ -187,7 +191,7 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
|
|
|
187
191
|
* }
|
|
188
192
|
* ```
|
|
189
193
|
*/
|
|
190
|
-
findByCursor<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never, IncludeCount extends boolean = true>(entityName: EntityName<Entity>, options: FindByCursorOptions<Entity, Hint, Fields, Excludes, IncludeCount>): Promise<Cursor<Entity, Hint, Fields, Excludes, IncludeCount>>;
|
|
194
|
+
findByCursor<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never, IncludeCount extends boolean = true, Using extends string = never>(entityName: EntityName<Entity>, options: WithUsingOptions<FindByCursorOptions<Entity, Hint, Fields, Excludes, IncludeCount>, Entity, Using>): Promise<Cursor<Entity, Hint, Fields, Excludes, IncludeCount>>;
|
|
191
195
|
/**
|
|
192
196
|
* Refreshes the persistent state of an entity from the database, overriding any local changes that have not yet been
|
|
193
197
|
* persisted. Returns the same entity instance (same object reference), but re-hydrated. If the entity is no longer
|
|
@@ -203,14 +207,18 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
|
|
|
203
207
|
/**
|
|
204
208
|
* Finds first entity matching your `where` query.
|
|
205
209
|
*/
|
|
206
|
-
findOne<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never>(entityName: EntityName<Entity>, where: FilterQuery<NoInfer<Entity
|
|
210
|
+
findOne<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never, Using extends string = never>(entityName: EntityName<Entity>, where: [Using] extends [never] ? FilterQuery<NoInfer<Entity>> : IndexFilterQuery<NoInfer<Entity>, Using>, options?: FindOneOptions<Entity, Hint, Fields, Excludes> & {
|
|
211
|
+
using?: Using | Using[];
|
|
212
|
+
}): Promise<Loaded<Entity, Hint, Fields, Excludes> | null>;
|
|
207
213
|
/**
|
|
208
214
|
* Finds first entity matching your `where` query. If nothing found, it will throw an error.
|
|
209
215
|
* If the `strict` option is specified and nothing is found or more than one matching entity is found, it will throw an error.
|
|
210
216
|
* You can override the factory for creating this method via `options.failHandler` locally
|
|
211
217
|
* or via `Configuration.findOneOrFailHandler` (`findExactlyOneOrFailHandler` when specifying `strict`) globally.
|
|
212
218
|
*/
|
|
213
|
-
findOneOrFail<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never>(entityName: EntityName<Entity>, where: FilterQuery<NoInfer<Entity
|
|
219
|
+
findOneOrFail<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never, Using extends string = never>(entityName: EntityName<Entity>, where: [Using] extends [never] ? FilterQuery<NoInfer<Entity>> : IndexFilterQuery<NoInfer<Entity>, Using>, options?: FindOneOrFailOptions<Entity, Hint, Fields, Excludes> & {
|
|
220
|
+
using?: Using | Using[];
|
|
221
|
+
}): Promise<Loaded<Entity, Hint, Fields, Excludes>>;
|
|
214
222
|
/**
|
|
215
223
|
* Creates or updates the entity, based on whether it is already present in the database.
|
|
216
224
|
* This method performs an `insert on conflict merge` query ensuring the database is in sync, returning a managed
|
|
@@ -524,6 +532,8 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
|
|
|
524
532
|
*/
|
|
525
533
|
getComparator(): EntityComparator;
|
|
526
534
|
private checkLockRequirements;
|
|
535
|
+
private validateIndexUsage;
|
|
536
|
+
private validateWhereKeysForIndex;
|
|
527
537
|
private lockAndPopulate;
|
|
528
538
|
private buildFields;
|
|
529
539
|
/** @internal */
|
package/EntityManager.js
CHANGED
|
@@ -103,9 +103,6 @@ export class EntityManager {
|
|
|
103
103
|
repo(entityName) {
|
|
104
104
|
return this.getRepository(entityName);
|
|
105
105
|
}
|
|
106
|
-
/**
|
|
107
|
-
* Finds all entities matching your `where` query. You can pass additional options via the `options` parameter.
|
|
108
|
-
*/
|
|
109
106
|
async find(entityName, where, options = {}) {
|
|
110
107
|
if (options.disableIdentityMap ?? this.config.get('disableIdentityMap')) {
|
|
111
108
|
const em = this.getContext(false);
|
|
@@ -116,10 +113,11 @@ export class EntityManager {
|
|
|
116
113
|
}
|
|
117
114
|
const em = this.getContext();
|
|
118
115
|
em.prepareOptions(options);
|
|
116
|
+
const meta = this.metadata.get(entityName);
|
|
117
|
+
em.validateIndexUsage(meta, where, options);
|
|
119
118
|
await em.tryFlush(entityName, options);
|
|
120
119
|
where = await em.processWhere(entityName, where, options, 'read');
|
|
121
120
|
validateParams(where);
|
|
122
|
-
const meta = this.metadata.get(entityName);
|
|
123
121
|
if (meta.orderBy) {
|
|
124
122
|
options.orderBy = QueryHelper.mergeOrderBy(options.orderBy, meta.orderBy);
|
|
125
123
|
}
|
|
@@ -203,6 +201,7 @@ export class EntityManager {
|
|
|
203
201
|
options.orderBy = options.orderBy || {};
|
|
204
202
|
options.populate = (await em.preparePopulate(entityName, options));
|
|
205
203
|
const meta = this.metadata.get(entityName);
|
|
204
|
+
em.validateIndexUsage(meta, options.where ?? {}, options);
|
|
206
205
|
options = { ...options };
|
|
207
206
|
// save the original hint value so we know it was infer/all
|
|
208
207
|
options._populateWhere = options.populateWhere ?? this.config.get('populateWhere');
|
|
@@ -494,10 +493,6 @@ export class EntityManager {
|
|
|
494
493
|
const conds = [...ret, where].filter(c => Utils.hasObjectKeys(c) || Raw.hasObjectFragments(c));
|
|
495
494
|
return conds.length > 1 ? { $and: conds } : conds[0];
|
|
496
495
|
}
|
|
497
|
-
/**
|
|
498
|
-
* Calls `em.find()` and `em.count()` with the same arguments (where applicable) and returns the results as tuple
|
|
499
|
-
* where the first element is the array of entities, and the second is the count.
|
|
500
|
-
*/
|
|
501
496
|
async findAndCount(entityName, where, options = {}) {
|
|
502
497
|
const em = this.getContext(false);
|
|
503
498
|
await em.tryFlush(entityName, options);
|
|
@@ -631,9 +626,6 @@ export class EntityManager {
|
|
|
631
626
|
}
|
|
632
627
|
return entity;
|
|
633
628
|
}
|
|
634
|
-
/**
|
|
635
|
-
* Finds first entity matching your `where` query.
|
|
636
|
-
*/
|
|
637
629
|
async findOne(entityName, where, options = {}) {
|
|
638
630
|
if (options.disableIdentityMap ?? this.config.get('disableIdentityMap')) {
|
|
639
631
|
const em = this.getContext(false);
|
|
@@ -653,6 +645,7 @@ export class EntityManager {
|
|
|
653
645
|
}
|
|
654
646
|
await em.tryFlush(entityName, options);
|
|
655
647
|
const meta = em.metadata.get(entityName);
|
|
648
|
+
em.validateIndexUsage(meta, where, options);
|
|
656
649
|
where = await em.processWhere(entityName, where, options, 'read');
|
|
657
650
|
validateEmptyWhere(where);
|
|
658
651
|
em.checkLockRequirements(options.lockMode, meta);
|
|
@@ -701,12 +694,6 @@ export class EntityManager {
|
|
|
701
694
|
await em.storeCache(options.cache, cached, () => helper(entity).toPOJO());
|
|
702
695
|
return entity;
|
|
703
696
|
}
|
|
704
|
-
/**
|
|
705
|
-
* Finds first entity matching your `where` query. If nothing found, it will throw an error.
|
|
706
|
-
* If the `strict` option is specified and nothing is found or more than one matching entity is found, it will throw an error.
|
|
707
|
-
* You can override the factory for creating this method via `options.failHandler` locally
|
|
708
|
-
* or via `Configuration.findOneOrFailHandler` (`findExactlyOneOrFailHandler` when specifying `strict`) globally.
|
|
709
|
-
*/
|
|
710
697
|
async findOneOrFail(entityName, where, options = {}) {
|
|
711
698
|
let entity;
|
|
712
699
|
let isStrictViolation = false;
|
|
@@ -1727,6 +1714,79 @@ export class EntityManager {
|
|
|
1727
1714
|
throw ValidationError.transactionRequired();
|
|
1728
1715
|
}
|
|
1729
1716
|
}
|
|
1717
|
+
validateIndexUsage(meta, where, options) {
|
|
1718
|
+
if (!options.using) {
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
const indexNames = Utils.asArray(options.using);
|
|
1722
|
+
const allIndexes = [...meta.indexes, ...meta.uniques];
|
|
1723
|
+
const indexMap = new Map();
|
|
1724
|
+
for (const idx of allIndexes) {
|
|
1725
|
+
if (idx.name) {
|
|
1726
|
+
indexMap.set(idx.name, Utils.asArray(idx.properties ?? []));
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
for (const prop of meta.props) {
|
|
1730
|
+
if (typeof prop.index === 'string') {
|
|
1731
|
+
indexMap.set(prop.index, [prop.name]);
|
|
1732
|
+
}
|
|
1733
|
+
if (typeof prop.unique === 'string') {
|
|
1734
|
+
indexMap.set(prop.unique, [prop.name]);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
const allowedProps = new Set();
|
|
1738
|
+
for (const name of indexNames) {
|
|
1739
|
+
const props = indexMap.get(name);
|
|
1740
|
+
if (!props) {
|
|
1741
|
+
const available = [...indexMap.keys()];
|
|
1742
|
+
throw new Error(`Index '${name}' not found on entity '${meta.className}'. ` +
|
|
1743
|
+
(available.length > 0 ? `Available indexes: ${available.join(', ')}` : 'No named indexes defined.'));
|
|
1744
|
+
}
|
|
1745
|
+
for (const prop of props) {
|
|
1746
|
+
allowedProps.add(prop);
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
this.validateWhereKeysForIndex(where, allowedProps, indexNames);
|
|
1750
|
+
if (options.orderBy) {
|
|
1751
|
+
const orderMaps = Array.isArray(options.orderBy) ? options.orderBy : [options.orderBy];
|
|
1752
|
+
for (const orderMap of orderMaps) {
|
|
1753
|
+
for (const key of Object.keys(orderMap)) {
|
|
1754
|
+
if (!allowedProps.has(key)) {
|
|
1755
|
+
throw new Error(`Property '${key}' in orderBy is not covered by index '${indexNames.join("', '")}'. ` +
|
|
1756
|
+
`Allowed properties: ${[...allowedProps].join(', ')}`);
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
validateWhereKeysForIndex(where, allowedProps, indexNames) {
|
|
1763
|
+
if (typeof where !== 'object' || where === null) {
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
if (Array.isArray(where)) {
|
|
1767
|
+
for (const item of where) {
|
|
1768
|
+
this.validateWhereKeysForIndex(item, allowedProps, indexNames);
|
|
1769
|
+
}
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
for (const key of Object.keys(where)) {
|
|
1773
|
+
if (key === '$and' || key === '$or') {
|
|
1774
|
+
for (const item of Utils.asArray(where[key])) {
|
|
1775
|
+
this.validateWhereKeysForIndex(item, allowedProps, indexNames);
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
else if (key === '$not') {
|
|
1779
|
+
this.validateWhereKeysForIndex(where[key], allowedProps, indexNames);
|
|
1780
|
+
}
|
|
1781
|
+
else if (key.startsWith('$')) {
|
|
1782
|
+
continue;
|
|
1783
|
+
}
|
|
1784
|
+
else if (!allowedProps.has(key)) {
|
|
1785
|
+
throw new Error(`Property '${key}' in where clause is not covered by index '${indexNames.join("', '")}'. ` +
|
|
1786
|
+
`Allowed properties: ${[...allowedProps].join(', ')}`);
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1730
1790
|
async lockAndPopulate(meta, entity, where, options) {
|
|
1731
1791
|
if (!meta.virtual && options.lockMode === LockMode.OPTIMISTIC) {
|
|
1732
1792
|
await this.lock(entity, options.lockMode, {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ConnectionType, Constructor, EntityData, EntityMetadata, EntityProperty, FilterQuery, Primary, Dictionary, IPrimaryKey, PopulateOptions, EntityDictionary, AutoPath, ObjectQuery, FilterObject, Populate, EntityName, PopulateHintOptions, Prefixes } from '../typings.js';
|
|
1
|
+
import type { ConnectionType, Constructor, EntityData, EntityMetadata, EntityProperty, FilterQuery, Primary, Dictionary, IPrimaryKey, PopulateOptions, EntityDictionary, AutoPath, ObjectQuery, FilterObject, Populate, EntityName, PopulateHintOptions, Prefixes, IndexName } from '../typings.js';
|
|
2
2
|
import type { Connection, QueryResult, Transaction } from '../connections/Connection.js';
|
|
3
3
|
import type { FlushMode, LockMode, QueryOrderMap, QueryFlag, LoadStrategy, PopulateHint, PopulatePath } from '../enums.js';
|
|
4
4
|
import type { Platform } from '../platforms/Platform.js';
|
|
@@ -216,6 +216,20 @@ export interface FindOptions<Entity, Hint extends string = never, Fields extends
|
|
|
216
216
|
connectionType?: ConnectionType;
|
|
217
217
|
/** SQL: appended to FROM clause (e.g. `'force index(my_index)'`); MongoDB: index name or spec passed as `hint`. */
|
|
218
218
|
indexHint?: string | Dictionary;
|
|
219
|
+
/**
|
|
220
|
+
* Named index(es) for this query. When provided:
|
|
221
|
+
* - Validates that `where` and `orderBy` only reference columns covered by the specified index(es).
|
|
222
|
+
* - Emits SQL index hints where supported (MySQL/MariaDB: `USE INDEX`, MSSQL: `WITH (INDEX(...))`).
|
|
223
|
+
* - If `indexHint` is also set, `indexHint` takes precedence for SQL generation.
|
|
224
|
+
*
|
|
225
|
+
* Accepts a single index name or an array. For `defineEntity` entities with named indexes
|
|
226
|
+
* and decorator entities with `[IndexHints]`, index names are autocompleted.
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* await em.find(Book, { title: 'foo' }, { using: 'idx_book_title' });
|
|
230
|
+
* await em.find(Book, { title: 'foo', author: 1 }, { using: ['idx_book_title', 'idx_book_author'] });
|
|
231
|
+
*/
|
|
232
|
+
using?: IndexName<Entity> | IndexName<Entity>[];
|
|
219
233
|
/** sql only */
|
|
220
234
|
comments?: string | string[];
|
|
221
235
|
/** sql only */
|
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,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { PopulatePath } from '../enums.js';
|
|
2
2
|
import type { CreateOptions, EntityManager, MergeOptions } from '../EntityManager.js';
|
|
3
3
|
import type { AssignOptions } from './EntityAssigner.js';
|
|
4
|
-
import type { EntityData, EntityName, Primary, Loaded, FilterQuery, EntityDictionary, AutoPath, RequiredEntityData, Ref, EntityType, EntityDTO, MergeSelected, FromEntityType, IsSubset, MergeLoaded, ArrayElement } from '../typings.js';
|
|
4
|
+
import type { EntityData, EntityName, Primary, Loaded, FilterQuery, EntityDictionary, AutoPath, RequiredEntityData, Ref, EntityType, EntityDTO, MergeSelected, FromEntityType, IsSubset, MergeLoaded, ArrayElement, IndexFilterQuery, WithUsingOptions } from '../typings.js';
|
|
5
5
|
import type { CountOptions, DeleteOptions, FindAllOptions, FindByCursorOptions, FindOneOptions, FindOneOrFailOptions, FindOptions, GetReferenceOptions, NativeInsertUpdateOptions, StreamOptions, UpdateOptions, UpsertManyOptions, UpsertOptions } from '../drivers/IDatabaseDriver.js';
|
|
6
6
|
import type { EntityLoaderOptions } from './EntityLoader.js';
|
|
7
7
|
import type { Cursor } from '../utils/Cursor.js';
|
|
@@ -13,13 +13,17 @@ export declare class EntityRepository<Entity extends object> {
|
|
|
13
13
|
/**
|
|
14
14
|
* Finds first entity matching your `where` query.
|
|
15
15
|
*/
|
|
16
|
-
findOne<Hint extends string = never, Fields extends string = never, Excludes extends string = never>(where: FilterQuery<Entity
|
|
16
|
+
findOne<Hint extends string = never, Fields extends string = never, Excludes extends string = never, Using extends string = never>(where: [Using] extends [never] ? FilterQuery<Entity> : IndexFilterQuery<Entity, Using>, options?: FindOneOptions<Entity, Hint, Fields, Excludes> & {
|
|
17
|
+
using?: Using | Using[];
|
|
18
|
+
}): Promise<Loaded<Entity, Hint, Fields, Excludes> | null>;
|
|
17
19
|
/**
|
|
18
20
|
* Finds first entity matching your `where` query. If nothing is found, it will throw an error.
|
|
19
21
|
* You can override the factory for creating this method via `options.failHandler` locally
|
|
20
22
|
* or via `Configuration.findOneOrFailHandler` globally.
|
|
21
23
|
*/
|
|
22
|
-
findOneOrFail<Hint extends string = never, Fields extends string = never, Excludes extends string = never>(where: FilterQuery<Entity>, options?: FindOneOrFailOptions<Entity, Hint, Fields, Excludes>
|
|
24
|
+
findOneOrFail<Hint extends string = never, Fields extends string = never, Excludes extends string = never, Using extends string = never>(where: [Using] extends [never] ? FilterQuery<Entity> : IndexFilterQuery<Entity, Using>, options?: FindOneOrFailOptions<Entity, Hint, Fields, Excludes> & {
|
|
25
|
+
using?: Using | Using[];
|
|
26
|
+
}): Promise<Loaded<Entity, Hint, Fields, Excludes>>;
|
|
23
27
|
/**
|
|
24
28
|
* Creates or updates the entity, based on whether it is already present in the database.
|
|
25
29
|
* This method performs an `insert on conflict merge` query ensuring the database is in sync, returning a managed
|
|
@@ -72,24 +76,28 @@ export declare class EntityRepository<Entity extends object> {
|
|
|
72
76
|
/**
|
|
73
77
|
* Finds all entities matching your `where` query. You can pass additional options via the `options` parameter.
|
|
74
78
|
*/
|
|
75
|
-
find<Hint extends string = never, Fields extends string = never, Excludes extends string = never>(where: FilterQuery<Entity>, options?: FindOptions<Entity, Hint, Fields, Excludes>
|
|
79
|
+
find<Hint extends string = never, Fields extends string = never, Excludes extends string = never, Using extends string = never>(where: [Using] extends [never] ? FilterQuery<Entity> : IndexFilterQuery<Entity, Using>, options?: FindOptions<Entity, Hint, Fields, Excludes> & {
|
|
80
|
+
using?: Using | Using[];
|
|
81
|
+
}): Promise<Loaded<Entity, Hint, Fields, Excludes>[]>;
|
|
76
82
|
/**
|
|
77
83
|
* Calls `em.find()` and `em.count()` with the same arguments (where applicable) and returns the results as tuple
|
|
78
84
|
* where first element is the array of entities, and the second is the count.
|
|
79
85
|
*/
|
|
80
|
-
findAndCount<Hint extends string = never, Fields extends string = never, Excludes extends string = never>(where: FilterQuery<Entity
|
|
86
|
+
findAndCount<Hint extends string = never, Fields extends string = never, Excludes extends string = never, Using extends string = never>(where: [Using] extends [never] ? FilterQuery<Entity> : IndexFilterQuery<Entity, Using>, options?: FindOptions<Entity, Hint, Fields, Excludes> & {
|
|
87
|
+
using?: Using | Using[];
|
|
88
|
+
}): Promise<[Loaded<Entity, Hint, Fields, Excludes>[], number]>;
|
|
81
89
|
/**
|
|
82
90
|
* @inheritDoc EntityManager.findByCursor
|
|
83
91
|
*/
|
|
84
|
-
findByCursor<Hint extends string = never, Fields extends string = never, Excludes extends string = never, IncludeCount extends boolean = true>(options: FindByCursorOptions<Entity, Hint, Fields, Excludes, IncludeCount>): Promise<Cursor<Entity, Hint, Fields, Excludes, IncludeCount>>;
|
|
92
|
+
findByCursor<Hint extends string = never, Fields extends string = never, Excludes extends string = never, IncludeCount extends boolean = true, Using extends string = never>(options: WithUsingOptions<FindByCursorOptions<Entity, Hint, Fields, Excludes, IncludeCount>, Entity, Using>): Promise<Cursor<Entity, Hint, Fields, Excludes, IncludeCount>>;
|
|
85
93
|
/**
|
|
86
94
|
* Finds all entities of given type. You can pass additional options via the `options` parameter.
|
|
87
95
|
*/
|
|
88
|
-
findAll<Hint extends string = never, Fields extends string = never, Excludes extends string = never>(options?: FindAllOptions<Entity, Hint, Fields, Excludes>): Promise<Loaded<Entity, Hint, Fields, Excludes>[]>;
|
|
96
|
+
findAll<Hint extends string = never, Fields extends string = never, Excludes extends string = never, Using extends string = never>(options?: WithUsingOptions<FindAllOptions<Entity, Hint, Fields, Excludes>, Entity, Using>): Promise<Loaded<Entity, Hint, Fields, Excludes>[]>;
|
|
89
97
|
/**
|
|
90
98
|
* @inheritDoc EntityManager.stream
|
|
91
99
|
*/
|
|
92
|
-
stream<Hint extends string = never, Fields extends string = never, Excludes extends string = never>(options?: StreamOptions<Entity, Hint, Fields, Excludes>): AsyncIterableIterator<Loaded<Entity, Hint, Fields, Excludes>>;
|
|
100
|
+
stream<Hint extends string = never, Fields extends string = never, Excludes extends string = never, Using extends string = never>(options?: WithUsingOptions<StreamOptions<Entity, Hint, Fields, Excludes>, Entity, Using>): AsyncIterableIterator<Loaded<Entity, Hint, Fields, Excludes>>;
|
|
93
101
|
/**
|
|
94
102
|
* @inheritDoc EntityManager.insert
|
|
95
103
|
*/
|
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 } 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, 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';
|
|
@@ -104,8 +104,16 @@ export interface PropertyChain<Value, Options> {
|
|
|
104
104
|
customOrder(...customOrder: string[] | number[] | boolean[]): PropertyChain<Value, Options>;
|
|
105
105
|
extra(extra: string): PropertyChain<Value, Options>;
|
|
106
106
|
ignoreSchemaChanges(...ignoreSchemaChanges: ('type' | 'extra' | 'default')[]): PropertyChain<Value, Options>;
|
|
107
|
-
index
|
|
108
|
-
|
|
107
|
+
/** Explicitly specify index on a property. When a string name is passed, it enables type-safe `using` in `FindOptions`. */
|
|
108
|
+
index<N extends string>(name: N): PropertyChain<Value, Omit<Options, 'index'> & {
|
|
109
|
+
index: N;
|
|
110
|
+
}>;
|
|
111
|
+
index(index?: boolean): PropertyChain<Value, Options>;
|
|
112
|
+
/** Set column as unique. When a string name is passed, it enables type-safe `using` in `FindOptions`. (SQL only) */
|
|
113
|
+
unique<N extends string>(name: N): PropertyChain<Value, Omit<Options, 'unique'> & {
|
|
114
|
+
unique: N;
|
|
115
|
+
}>;
|
|
116
|
+
unique(unique?: boolean): PropertyChain<Value, Options>;
|
|
109
117
|
comment(comment: string): PropertyChain<Value, Options>;
|
|
110
118
|
accessor(accessor?: string | boolean): PropertyChain<Value, Options>;
|
|
111
119
|
eager(eager?: boolean): HasKind<Options, 'm:1' | '1:m' | '1:1' | 'm:n'> extends true ? PropertyChain<Value, Options> : never;
|
|
@@ -338,12 +346,20 @@ export declare class UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys
|
|
|
338
346
|
concurrencyCheck(concurrencyCheck?: boolean): Pick<UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys>, IncludeKeys>;
|
|
339
347
|
/**
|
|
340
348
|
* Explicitly specify index on a property.
|
|
349
|
+
* When a string name is passed, it is captured as a literal type for use with the `using` option in `FindOptions`.
|
|
341
350
|
*/
|
|
342
|
-
index
|
|
351
|
+
index<N extends string>(name: N): Pick<UniversalPropertyOptionsBuilder<Value, Omit<Options, 'index'> & {
|
|
352
|
+
index: N;
|
|
353
|
+
}, IncludeKeys>, IncludeKeys>;
|
|
354
|
+
index(index?: boolean): Pick<UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys>, IncludeKeys>;
|
|
343
355
|
/**
|
|
344
356
|
* Set column as unique for {@link https://mikro-orm.io/docs/schema-generator Schema Generator}. (SQL only)
|
|
357
|
+
* When a string name is passed, it is captured as a literal type for use with the `using` option in `FindOptions`.
|
|
345
358
|
*/
|
|
346
|
-
unique
|
|
359
|
+
unique<N extends string>(name: N): Pick<UniversalPropertyOptionsBuilder<Value, Omit<Options, 'unique'> & {
|
|
360
|
+
unique: N;
|
|
361
|
+
}, IncludeKeys>, IncludeKeys>;
|
|
362
|
+
unique(unique?: boolean): Pick<UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys>, IncludeKeys>;
|
|
347
363
|
/**
|
|
348
364
|
* Specify column with check constraints. (Postgres driver only)
|
|
349
365
|
*
|
|
@@ -669,7 +685,9 @@ export type InferEntityFromProperties<Properties extends Record<string, any>, PK
|
|
|
669
685
|
[Config]?: DefineConfig<{
|
|
670
686
|
forceObject: true;
|
|
671
687
|
}>;
|
|
672
|
-
} : {})
|
|
688
|
+
} : {}) & {
|
|
689
|
+
[IndexHints]?: [Properties];
|
|
690
|
+
};
|
|
673
691
|
type InferCombinedPrimaryKey<Properties extends Record<string, any>, PK, Base> = PK extends undefined ? CombinePrimaryKeys<InferPrimaryKey<Properties>, ExtractBasePrimaryKey<Base>> : PK;
|
|
674
692
|
type ExtractBasePrimaryKey<Base> = Base extends {
|
|
675
693
|
[PrimaryKeyProp]?: infer BasePK;
|
package/entity/defineEntity.js
CHANGED
|
@@ -191,15 +191,9 @@ export class UniversalPropertyOptionsBuilder {
|
|
|
191
191
|
concurrencyCheck(concurrencyCheck = true) {
|
|
192
192
|
return this.assignOptions({ concurrencyCheck });
|
|
193
193
|
}
|
|
194
|
-
/**
|
|
195
|
-
* Explicitly specify index on a property.
|
|
196
|
-
*/
|
|
197
194
|
index(index = true) {
|
|
198
195
|
return this.assignOptions({ index });
|
|
199
196
|
}
|
|
200
|
-
/**
|
|
201
|
-
* Set column as unique for {@link https://mikro-orm.io/docs/schema-generator Schema Generator}. (SQL only)
|
|
202
|
-
*/
|
|
203
197
|
unique(unique = true) {
|
|
204
198
|
return this.assignOptions({ unique });
|
|
205
199
|
}
|
package/index.d.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* @packageDocumentation
|
|
3
3
|
* @module core
|
|
4
4
|
*/
|
|
5
|
-
export { EntityMetadata, PrimaryKeyProp, EntityRepositoryType, OptionalProps, EagerProps, HiddenProps, Config, EntityName, } 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, 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';
|
|
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';
|
|
7
7
|
export * from './enums.js';
|
|
8
8
|
export * from './errors.js';
|
|
9
9
|
export * from './exceptions.js';
|
package/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @packageDocumentation
|
|
3
3
|
* @module core
|
|
4
4
|
*/
|
|
5
|
-
export { EntityMetadata, PrimaryKeyProp, EntityRepositoryType, OptionalProps, EagerProps, HiddenProps, Config, EntityName, } from './typings.js';
|
|
5
|
+
export { EntityMetadata, PrimaryKeyProp, EntityRepositoryType, OptionalProps, EagerProps, HiddenProps, Config, EntityName, IndexHints, } from './typings.js';
|
|
6
6
|
export * from './enums.js';
|
|
7
7
|
export * from './errors.js';
|
|
8
8
|
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.
|
|
@@ -479,17 +479,28 @@ export class MetadataDiscovery {
|
|
|
479
479
|
prop.polymorphic = prop2.polymorphic;
|
|
480
480
|
prop.discriminator = prop2.discriminator;
|
|
481
481
|
prop.discriminatorColumn = prop2.discriminatorColumn;
|
|
482
|
-
|
|
482
|
+
// For a union-target pivot each inverse side sits on one specific target class, so its
|
|
483
|
+
// discriminator value is that class's tableName. For Rails-style, prop2 has a single fixed value.
|
|
484
|
+
prop.discriminatorValue = QueryHelper.isUnionTargetPolymorphic(prop2) ? meta.tableName : prop2.discriminatorValue;
|
|
483
485
|
}
|
|
484
486
|
prop.referencedColumnNames ??= Utils.flatten(meta.primaryKeys.map(primaryKey => meta.properties[primaryKey].fieldNames));
|
|
485
|
-
//
|
|
486
|
-
|
|
487
|
+
// Union-target polymorphic M:N: owner side is fixed (real FK), target side uses discriminator-derived names.
|
|
488
|
+
const isUnionTargetMN = QueryHelper.isUnionTargetPolymorphic(prop);
|
|
489
|
+
if (prop.polymorphic && prop.discriminator && !isUnionTargetMN) {
|
|
490
|
+
// Rails-style: owner side is polymorphic, uses discriminator base name (e.g. taggable_id instead of post_id)
|
|
487
491
|
prop.joinColumns ??= prop.referencedColumnNames.map(referencedColumnName => this.#namingStrategy.joinKeyColumnName(prop.discriminator, referencedColumnName, prop.referencedColumnNames.length > 1));
|
|
488
492
|
}
|
|
489
493
|
else {
|
|
490
494
|
prop.joinColumns ??= prop.referencedColumnNames.map(referencedColumnName => this.#namingStrategy.joinKeyColumnName(meta.root.className, referencedColumnName, meta.compositePK));
|
|
491
495
|
}
|
|
492
|
-
|
|
496
|
+
if (isUnionTargetMN) {
|
|
497
|
+
// Target side uses discriminator base name (e.g. attachable_id — shared across Image/Video)
|
|
498
|
+
const targetPkCols = Utils.flatten(meta2.primaryKeys.map(pk => meta2.properties[pk].fieldNames));
|
|
499
|
+
prop.inverseJoinColumns ??= targetPkCols.map(fieldName => this.#namingStrategy.joinKeyColumnName(prop.discriminator, fieldName, targetPkCols.length > 1));
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
prop.inverseJoinColumns ??= this.initManyToOneFieldName(prop, meta2.root.className);
|
|
503
|
+
}
|
|
493
504
|
}
|
|
494
505
|
isExplicitTableName(meta) {
|
|
495
506
|
return meta.tableName !== this.#namingStrategy.classToTableName(meta.className);
|
|
@@ -583,6 +594,24 @@ export class MetadataDiscovery {
|
|
|
583
594
|
if (prop.inversedBy) {
|
|
584
595
|
prop.targetMeta.properties[prop.inversedBy].pivotEntity = pivotMeta.class;
|
|
585
596
|
}
|
|
597
|
+
// Propagate pivotEntity to ALL inverse collections using mappedBy pointing at this
|
|
598
|
+
// owner prop. Covers three cases:
|
|
599
|
+
// - regular inverse (Tag.posts mappedBy Post.tags) — handled by inversedBy above
|
|
600
|
+
// - union-target inverse (Image.posts mappedBy Post.attachments) — on each polymorph target
|
|
601
|
+
// - merged inverse (Tag.owners mappedBy [Post,Video].tags) — union collection on the target
|
|
602
|
+
const inverseCandidates = QueryHelper.isUnionTargetPolymorphic(prop)
|
|
603
|
+
? prop.polymorphTargets
|
|
604
|
+
: [prop.targetMeta];
|
|
605
|
+
for (const targetMeta of inverseCandidates) {
|
|
606
|
+
for (const inverseProp of Object.values(targetMeta.properties)) {
|
|
607
|
+
if (inverseProp.kind === ReferenceKind.MANY_TO_MANY &&
|
|
608
|
+
inverseProp.mappedBy === prop.name &&
|
|
609
|
+
!inverseProp.pivotEntity) {
|
|
610
|
+
inverseProp.pivotEntity = pivotMeta.class;
|
|
611
|
+
inverseProp.pivotTable = pivotMeta.tableName;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
586
615
|
return pivotMeta;
|
|
587
616
|
});
|
|
588
617
|
}
|
|
@@ -721,8 +750,12 @@ export class MetadataDiscovery {
|
|
|
721
750
|
}
|
|
722
751
|
}
|
|
723
752
|
}
|
|
724
|
-
//
|
|
725
|
-
if (prop.
|
|
753
|
+
// Union-target polymorphic M:N: discriminator + target FK share the pivot across multiple target types
|
|
754
|
+
if (prop.discriminatorColumn && QueryHelper.isUnionTargetPolymorphic(prop)) {
|
|
755
|
+
this.defineUnionTargetPolymorphicPivotProperties(pivotMeta2, meta, prop);
|
|
756
|
+
}
|
|
757
|
+
else if (prop.polymorphic && prop.discriminatorColumn) {
|
|
758
|
+
// Rails-style polymorphic M:N: multiple owners share the pivot, single target type
|
|
726
759
|
this.definePolymorphicPivotProperties(pivotMeta2, meta, prop, targetMeta);
|
|
727
760
|
}
|
|
728
761
|
else {
|
|
@@ -809,6 +842,33 @@ export class MetadataDiscovery {
|
|
|
809
842
|
pivotMeta.polymorphicDiscriminatorMap ??= {};
|
|
810
843
|
pivotMeta.polymorphicDiscriminatorMap[prop.discriminatorValue] = meta.class;
|
|
811
844
|
}
|
|
845
|
+
/**
|
|
846
|
+
* Mirror of definePolymorphicPivotProperties for union-target M:N
|
|
847
|
+
* (e.g. Post.attachments -> Image | Video via shared pivot with a target-side discriminator).
|
|
848
|
+
*
|
|
849
|
+
* Pivot shape:
|
|
850
|
+
* (owner_fk..., discriminator_column, target_fk...)
|
|
851
|
+
* - owner side is a normal M:1 to the single owner entity
|
|
852
|
+
* - target side is a discriminator column + per-target-type virtual M:1 relations
|
|
853
|
+
*/
|
|
854
|
+
defineUnionTargetPolymorphicPivotProperties(pivotMeta, meta, prop) {
|
|
855
|
+
const discriminatorColumn = prop.discriminatorColumn;
|
|
856
|
+
const targets = prop.polymorphTargets;
|
|
857
|
+
pivotMeta.properties[meta.name + '_owner'] = this.definePivotProperty(prop, meta.name + '_owner', meta.class, prop.discriminator, true, false);
|
|
858
|
+
const discriminatorProp = this.createPivotScalarProperty(discriminatorColumn, [this.#platform.getVarcharTypeDeclarationSQL(prop)], [discriminatorColumn], { type: 'string', primary: true, nullable: false });
|
|
859
|
+
this.initFieldName(discriminatorProp);
|
|
860
|
+
pivotMeta.properties[discriminatorColumn] = discriminatorProp;
|
|
861
|
+
const firstTargetColumnTypes = this.getPrimaryKeyColumnTypes(targets[0]);
|
|
862
|
+
pivotMeta.properties[prop.discriminator] = this.createPivotScalarProperty(prop.discriminator, firstTargetColumnTypes, [...prop.inverseJoinColumns], { type: targets[0].className, primary: true, nullable: false });
|
|
863
|
+
pivotMeta.polymorphicDiscriminatorMap ??= {};
|
|
864
|
+
for (const targetMeta of targets) {
|
|
865
|
+
const relationName = `${prop.discriminator}_${targetMeta.tableName}`;
|
|
866
|
+
const relation = this.definePolymorphicOwnerRelation(prop, relationName, targetMeta);
|
|
867
|
+
relation.joinColumns = relation.fieldNames = relation.ownColumns = [...prop.inverseJoinColumns];
|
|
868
|
+
pivotMeta.properties[relationName] = relation;
|
|
869
|
+
pivotMeta.polymorphicDiscriminatorMap[targetMeta.tableName] = targetMeta.class;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
812
872
|
/**
|
|
813
873
|
* Create a virtual M:1 relation from pivot to a polymorphic owner entity.
|
|
814
874
|
* This enables single-query join loading for inverse-side polymorphic M:N.
|
|
@@ -1045,11 +1105,15 @@ export class MetadataDiscovery {
|
|
|
1045
1105
|
prop.discriminatorColumn ??= this.#namingStrategy.discriminatorColumnName(prop.discriminator);
|
|
1046
1106
|
prop.createForeignKeyConstraint = false;
|
|
1047
1107
|
const isToOne = [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind);
|
|
1048
|
-
|
|
1108
|
+
const isUnionTargetMN = prop.kind === ReferenceKind.MANY_TO_MANY && Array.isArray(prop.target);
|
|
1109
|
+
if (isToOne || isUnionTargetMN) {
|
|
1049
1110
|
const types = prop.type.split(/ ?\| ?/);
|
|
1050
1111
|
prop.polymorphTargets = discovered.filter(m => types.includes(m.className) && !m.embeddable);
|
|
1051
1112
|
prop.targetMeta = prop.polymorphTargets[0];
|
|
1052
1113
|
prop.referencedPKs = prop.targetMeta?.primaryKeys;
|
|
1114
|
+
if (isUnionTargetMN && prop.polymorphTargets.length < 2) {
|
|
1115
|
+
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.`);
|
|
1116
|
+
}
|
|
1053
1117
|
}
|
|
1054
1118
|
if (prop.discriminatorMap) {
|
|
1055
1119
|
const normalizedMap = {};
|
|
@@ -1065,7 +1129,7 @@ export class MetadataDiscovery {
|
|
|
1065
1129
|
}
|
|
1066
1130
|
prop.discriminatorMap = normalizedMap;
|
|
1067
1131
|
}
|
|
1068
|
-
else if (isToOne) {
|
|
1132
|
+
else if (isToOne || isUnionTargetMN) {
|
|
1069
1133
|
prop.discriminatorMap = {};
|
|
1070
1134
|
const tableNameToTarget = new Map();
|
|
1071
1135
|
for (const target of prop.polymorphTargets) {
|
|
@@ -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
|
@@ -340,7 +340,7 @@ export interface PropertyOptions<Owner> {
|
|
|
340
340
|
}
|
|
341
341
|
export interface ReferenceOptions<Owner, Target> extends PropertyOptions<Owner> {
|
|
342
342
|
/** Set target entity type. For polymorphic relations, pass an array of entity types. */
|
|
343
|
-
entity?: () => EntityName<Target> | EntityName
|
|
343
|
+
entity?: () => EntityName<Target> | EntityName[];
|
|
344
344
|
/** Set what actions on owning entity should be cascaded to the relationship. Defaults to [Cascade.PERSIST, Cascade.MERGE] (see {@doclink cascading}). */
|
|
345
345
|
cascade?: Cascade[];
|
|
346
346
|
/** Always load the relationship. Discouraged for use with to-many relations for performance reasons. */
|
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.7",
|
|
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/platforms/Platform.d.ts
CHANGED
|
@@ -211,6 +211,11 @@ export declare abstract class Platform {
|
|
|
211
211
|
getFullTextWhereClause(prop: EntityProperty): string;
|
|
212
212
|
supportsCreatingFullTextIndex(): boolean;
|
|
213
213
|
getFullTextIndexExpression(indexName: string, schemaName: string | undefined, tableName: string, columns: SimpleColumnMeta[]): string;
|
|
214
|
+
/**
|
|
215
|
+
* Generates the SQL index hint clause for the given index names.
|
|
216
|
+
* Returns `undefined` when the platform does not support index hints (e.g. PostgreSQL, SQLite).
|
|
217
|
+
*/
|
|
218
|
+
formatIndexHint(indexNames: string[]): string | undefined;
|
|
214
219
|
/** Whether the driver automatically parses JSON columns into JS objects. */
|
|
215
220
|
convertsJsonAutomatically(): boolean;
|
|
216
221
|
/** Converts a JS value to its JSON database representation (typically JSON.stringify). */
|
package/platforms/Platform.js
CHANGED
|
@@ -415,6 +415,13 @@ export class Platform {
|
|
|
415
415
|
getFullTextIndexExpression(indexName, schemaName, tableName, columns) {
|
|
416
416
|
throw new Error('Full text searching is not supported by this driver.');
|
|
417
417
|
}
|
|
418
|
+
/**
|
|
419
|
+
* Generates the SQL index hint clause for the given index names.
|
|
420
|
+
* Returns `undefined` when the platform does not support index hints (e.g. PostgreSQL, SQLite).
|
|
421
|
+
*/
|
|
422
|
+
formatIndexHint(indexNames) {
|
|
423
|
+
return undefined;
|
|
424
|
+
}
|
|
418
425
|
/** Whether the driver automatically parses JSON columns into JS objects. */
|
|
419
426
|
convertsJsonAutomatically() {
|
|
420
427
|
return true;
|
package/typings.d.ts
CHANGED
|
@@ -47,7 +47,7 @@ export type AsyncFunction<R = any, T = Dictionary> = (args: T) => Promise<T>;
|
|
|
47
47
|
export type Compute<T> = {
|
|
48
48
|
[K in keyof T]: T[K];
|
|
49
49
|
} & {};
|
|
50
|
-
type InternalKeys = 'EntityRepositoryType' | 'PrimaryKeyProp' | 'OptionalProps' | 'EagerProps' | 'HiddenProps' | '__selectedType' | '__loadedType';
|
|
50
|
+
type InternalKeys = 'EntityRepositoryType' | 'PrimaryKeyProp' | 'OptionalProps' | 'EagerProps' | 'HiddenProps' | 'IndexHints' | '__selectedType' | '__loadedType';
|
|
51
51
|
/** Filters out function, symbol, and internal keys from an entity type. When `B = true`, also excludes scalar keys. */
|
|
52
52
|
export type CleanKeys<T, K extends keyof T, B extends boolean = false> = T[K] & {} extends Function ? never : K extends symbol | InternalKeys ? never : B extends true ? T[K] & {} extends Scalar ? never : K : K;
|
|
53
53
|
/** Extracts keys of `T` whose values are functions. */
|
|
@@ -150,6 +150,44 @@ export declare const EntityName: unique symbol;
|
|
|
150
150
|
export type InferEntityName<T> = T extends {
|
|
151
151
|
[EntityName]?: infer Name;
|
|
152
152
|
} ? (Name extends string ? Name : never) : never;
|
|
153
|
+
/**
|
|
154
|
+
* Symbol used to declare index-to-column mappings on an entity type.
|
|
155
|
+
* For decorator entities, declare as a phantom property:
|
|
156
|
+
* ```typescript
|
|
157
|
+
* [IndexHints]?: { idx_email: 'email'; idx_name_age: 'name' | 'age' };
|
|
158
|
+
* ```
|
|
159
|
+
* For `defineEntity` entities, index hints are inferred automatically from
|
|
160
|
+
* named indexes (property-level `.index('name')` and entity-level `indexes`/`uniques`).
|
|
161
|
+
*/
|
|
162
|
+
export declare const IndexHints: unique symbol;
|
|
163
|
+
/**
|
|
164
|
+
* Extracts the index hints map from an entity type. Returns `never` when no hints are declared.
|
|
165
|
+
* For decorator entities, `[IndexHints]` contains a pre-computed `{ idxName: 'prop' }` map.
|
|
166
|
+
* For `defineEntity` entities, `[IndexHints]` contains `[Properties]` (a tuple wrapping the
|
|
167
|
+
* raw property builders), which is lazily converted to the index map when first accessed.
|
|
168
|
+
*/
|
|
169
|
+
export type ExtractIndexHints<T> = T extends {
|
|
170
|
+
[IndexHints]?: infer H;
|
|
171
|
+
} ? H extends [infer P extends Record<string, any>] ? InferPropertyIndexMap<P> : H : never;
|
|
172
|
+
/**
|
|
173
|
+
* Extracts `{ indexName: propertyKey }` from property builder options.
|
|
174
|
+
* Checks `.index('name')` and `.unique('name')` on each property in a single pass.
|
|
175
|
+
*/
|
|
176
|
+
export type InferPropertyIndexMap<Properties extends Record<string, any>> = {
|
|
177
|
+
[K in keyof Properties as MaybeReturnType<Properties[K]> extends {
|
|
178
|
+
'~options': {
|
|
179
|
+
index: infer N extends string;
|
|
180
|
+
};
|
|
181
|
+
} ? N : MaybeReturnType<Properties[K]> extends {
|
|
182
|
+
'~options': {
|
|
183
|
+
unique: infer N extends string;
|
|
184
|
+
};
|
|
185
|
+
} ? N : never]: K & string;
|
|
186
|
+
};
|
|
187
|
+
/** Union of declared index names on an entity. Falls back to `string` when no `[IndexHints]` are declared. */
|
|
188
|
+
export type IndexName<T> = [ExtractIndexHints<T>] extends [never] ? string : (keyof ExtractIndexHints<T> & string) | (string & {});
|
|
189
|
+
/** Properties covered by the named index on entity T. Falls back to all entity keys when the index is unknown. */
|
|
190
|
+
export type IndexColumns<T, Name extends string> = ExtractIndexHints<T> extends Record<Name, infer Cols> ? Cols & string : EntityKey<T>;
|
|
153
191
|
/**
|
|
154
192
|
* Branded type that marks a property as optional in `em.create()`.
|
|
155
193
|
* Use as a property type wrapper: `createdAt: Opt<Date>` instead of listing in `[OptionalProps]`.
|
|
@@ -328,6 +366,18 @@ export type ObjectQuery<T> = OperatorMap<T> & FilterObject<T>;
|
|
|
328
366
|
* Accepts an object query, a primary key value, entity props with operators, or an array of filters.
|
|
329
367
|
*/
|
|
330
368
|
export type FilterQuery<T> = ObjectQuery<T> | NonNullable<ExpandScalar<Primary<T>>> | NonNullable<EntityProps<T> & OperatorMap<T>> | FilterQuery<T>[];
|
|
369
|
+
/**
|
|
370
|
+
* `FilterQuery` restricted to only properties covered by the specified index(es).
|
|
371
|
+
* Used when `using` option is set in `FindOptions` to enforce type-safe index usage.
|
|
372
|
+
*/
|
|
373
|
+
export type IndexFilterQuery<T, Using extends string> = [Using] extends [never] ? FilterQuery<T> : (OperatorMap<T> & {
|
|
374
|
+
-readonly [K in Extract<EntityKey<T>, IndexColumns<T, Using>>]?: ExpandQuery<ExpandProperty<FilterObjectProp<T, K>>> | ExpandQueryMerged<ExpandProperty<FilterObjectProp<T, K>>> | FilterValue<ExpandProperty<FilterObjectProp<T, K>>> | ElemMatchFilter<FilterObjectProp<T, K>> | null;
|
|
375
|
+
}) | NonNullable<ExpandScalar<Primary<T>>> | IndexFilterQuery<T, Using>[];
|
|
376
|
+
/** Replaces `where` and `using` on an options type with index-aware variants when `Using` is specified. */
|
|
377
|
+
export type WithUsingOptions<Opts, Entity, Using extends string> = Omit<Opts, 'where' | 'using'> & {
|
|
378
|
+
using?: Using | Using[];
|
|
379
|
+
where?: [Using] extends [never] ? FilterQuery<Entity> : IndexFilterQuery<Entity, Using>;
|
|
380
|
+
};
|
|
331
381
|
/** Public interface for the entity wrapper, accessible via `wrap(entity)`. Provides helper methods for entity state management. */
|
|
332
382
|
export interface IWrappedEntity<Entity extends object> {
|
|
333
383
|
isInitialized(): boolean;
|
|
@@ -1060,6 +1110,11 @@ export interface IMigrator {
|
|
|
1060
1110
|
* Executes down migrations to the given point. Without parameter it will migrate one version down.
|
|
1061
1111
|
*/
|
|
1062
1112
|
down(options?: string | string[] | Omit<MigrateOptions, 'from'>): Promise<MigrationInfo[]>;
|
|
1113
|
+
/**
|
|
1114
|
+
* Combines multiple executed migrations into a single migration file.
|
|
1115
|
+
* Concatenates source code without touching the database schema.
|
|
1116
|
+
*/
|
|
1117
|
+
rollup(migrations?: string[]): Promise<MigrationResult>;
|
|
1063
1118
|
/**
|
|
1064
1119
|
* Registers event handler.
|
|
1065
1120
|
*/
|
package/typings.js
CHANGED
|
@@ -21,6 +21,16 @@ export const Config = Symbol('Config');
|
|
|
21
21
|
/** Symbol used to declare the entity name as a string literal type (used by `defineEntity`). */
|
|
22
22
|
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
|
23
23
|
export const EntityName = Symbol('EntityName');
|
|
24
|
+
/**
|
|
25
|
+
* Symbol used to declare index-to-column mappings on an entity type.
|
|
26
|
+
* For decorator entities, declare as a phantom property:
|
|
27
|
+
* ```typescript
|
|
28
|
+
* [IndexHints]?: { idx_email: 'email'; idx_name_age: 'name' | 'age' };
|
|
29
|
+
* ```
|
|
30
|
+
* For `defineEntity` entities, index hints are inferred automatically from
|
|
31
|
+
* named indexes (property-level `.index('name')` and entity-level `indexes`/`uniques`).
|
|
32
|
+
*/
|
|
33
|
+
export const IndexHints = Symbol('IndexHints');
|
|
24
34
|
/**
|
|
25
35
|
* Runtime metadata for an entity, holding its properties, relations, indexes, hooks, and more.
|
|
26
36
|
* Created during metadata discovery and used throughout the ORM lifecycle.
|
|
@@ -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.7';
|
|
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;
|