@mikro-orm/core 7.1.0-dev.5 → 7.1.0-dev.50
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 +87 -12
- package/EntityManager.js +252 -40
- package/README.md +2 -1
- package/connections/Connection.d.ts +67 -1
- package/connections/Connection.js +64 -0
- package/drivers/IDatabaseDriver.d.ts +45 -7
- package/entity/BaseEntity.d.ts +68 -1
- package/entity/BaseEntity.js +18 -0
- package/entity/Collection.d.ts +6 -3
- package/entity/Collection.js +15 -4
- package/entity/EntityAssigner.js +8 -0
- package/entity/EntityFactory.js +5 -3
- package/entity/EntityLoader.d.ts +8 -1
- package/entity/EntityLoader.js +103 -30
- package/entity/EntityRepository.d.ts +27 -9
- package/entity/EntityRepository.js +12 -0
- package/entity/Reference.d.ts +42 -1
- package/entity/Reference.js +9 -0
- package/entity/defineEntity.d.ts +101 -21
- package/entity/defineEntity.js +17 -6
- package/entity/utils.js +4 -5
- package/enums.d.ts +8 -1
- package/errors.d.ts +2 -0
- package/errors.js +4 -0
- package/index.d.ts +2 -2
- package/index.js +1 -1
- package/metadata/EntitySchema.js +18 -0
- package/metadata/MetadataDiscovery.d.ts +12 -0
- package/metadata/MetadataDiscovery.js +173 -22
- package/metadata/MetadataValidator.d.ts +26 -0
- package/metadata/MetadataValidator.js +237 -1
- package/metadata/Routine.d.ts +74 -0
- package/metadata/Routine.js +140 -0
- package/metadata/index.d.ts +1 -0
- package/metadata/index.js +1 -0
- package/metadata/types.d.ts +78 -9
- package/naming-strategy/AbstractNamingStrategy.d.ts +1 -1
- package/naming-strategy/NamingStrategy.d.ts +1 -1
- package/package.json +1 -1
- package/platforms/Platform.d.ts +18 -3
- package/platforms/Platform.js +58 -6
- package/serialization/EntitySerializer.js +4 -3
- package/typings.d.ts +399 -22
- package/typings.js +43 -12
- package/unit-of-work/UnitOfWork.js +43 -5
- package/utils/AbstractMigrator.d.ts +20 -5
- package/utils/AbstractMigrator.js +263 -28
- package/utils/AbstractSchemaGenerator.d.ts +1 -1
- package/utils/AbstractSchemaGenerator.js +4 -1
- package/utils/Configuration.d.ts +30 -0
- package/utils/Configuration.js +45 -0
- package/utils/DataloaderUtils.d.ts +10 -1
- package/utils/DataloaderUtils.js +78 -0
- package/utils/EntityComparator.js +1 -1
- package/utils/QueryHelper.d.ts +16 -0
- package/utils/QueryHelper.js +15 -0
- package/utils/TransactionManager.js +2 -0
- package/utils/Utils.js +1 -1
- package/utils/fs-utils.d.ts +2 -0
- package/utils/fs-utils.js +7 -1
- package/utils/index.d.ts +1 -0
- package/utils/index.js +1 -0
- package/utils/partition-utils.d.ts +17 -0
- package/utils/partition-utils.js +79 -0
- package/utils/upsert-utils.d.ts +2 -0
- package/utils/upsert-utils.js +26 -1
package/EntityManager.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getOnConflictReturningFields, getWhereCondition } from './utils/upsert-utils.js';
|
|
1
|
+
import { getOnConflictReturningFields, getWhereCondition, resetUntouchedCollections } from './utils/upsert-utils.js';
|
|
2
2
|
import { Utils } from './utils/Utils.js';
|
|
3
3
|
import { Cursor } from './utils/Cursor.js';
|
|
4
4
|
import { QueryHelper } from './utils/QueryHelper.js';
|
|
@@ -49,6 +49,10 @@ export class EntityManager {
|
|
|
49
49
|
#disableTransactions;
|
|
50
50
|
#flushMode;
|
|
51
51
|
#schema;
|
|
52
|
+
/** @internal */
|
|
53
|
+
signal;
|
|
54
|
+
/** @internal */
|
|
55
|
+
inflightQueryAbortStrategy;
|
|
52
56
|
#useContext;
|
|
53
57
|
/**
|
|
54
58
|
* @internal
|
|
@@ -103,9 +107,6 @@ export class EntityManager {
|
|
|
103
107
|
repo(entityName) {
|
|
104
108
|
return this.getRepository(entityName);
|
|
105
109
|
}
|
|
106
|
-
/**
|
|
107
|
-
* Finds all entities matching your `where` query. You can pass additional options via the `options` parameter.
|
|
108
|
-
*/
|
|
109
110
|
async find(entityName, where, options = {}) {
|
|
110
111
|
if (options.disableIdentityMap ?? this.config.get('disableIdentityMap')) {
|
|
111
112
|
const em = this.getContext(false);
|
|
@@ -116,10 +117,11 @@ export class EntityManager {
|
|
|
116
117
|
}
|
|
117
118
|
const em = this.getContext();
|
|
118
119
|
em.prepareOptions(options);
|
|
120
|
+
const meta = this.metadata.get(entityName);
|
|
121
|
+
em.validateIndexUsage(meta, where, options);
|
|
119
122
|
await em.tryFlush(entityName, options);
|
|
120
123
|
where = await em.processWhere(entityName, where, options, 'read');
|
|
121
124
|
validateParams(where);
|
|
122
|
-
const meta = this.metadata.get(entityName);
|
|
123
125
|
if (meta.orderBy) {
|
|
124
126
|
options.orderBy = QueryHelper.mergeOrderBy(options.orderBy, meta.orderBy);
|
|
125
127
|
}
|
|
@@ -203,6 +205,7 @@ export class EntityManager {
|
|
|
203
205
|
options.orderBy = options.orderBy || {};
|
|
204
206
|
options.populate = (await em.preparePopulate(entityName, options));
|
|
205
207
|
const meta = this.metadata.get(entityName);
|
|
208
|
+
em.validateIndexUsage(meta, options.where ?? {}, options);
|
|
206
209
|
options = { ...options };
|
|
207
210
|
// save the original hint value so we know it was infer/all
|
|
208
211
|
options._populateWhere = options.populateWhere ?? this.config.get('populateWhere');
|
|
@@ -319,13 +322,13 @@ export class EntityManager {
|
|
|
319
322
|
}
|
|
320
323
|
const types = Object.values(meta.root.discriminatorMap).map(cls => this.metadata.get(cls));
|
|
321
324
|
const children = [];
|
|
322
|
-
const lookUpChildren = (ret,
|
|
323
|
-
const children = types.filter(meta2 => meta2.extends ===
|
|
324
|
-
children.forEach(m => lookUpChildren(ret, m
|
|
325
|
+
const lookUpChildren = (ret, parent) => {
|
|
326
|
+
const children = types.filter(meta2 => meta2.extends && this.metadata.find(meta2.extends) === parent);
|
|
327
|
+
children.forEach(m => lookUpChildren(ret, m));
|
|
325
328
|
ret.push(...children.filter(c => c.discriminatorValue));
|
|
326
329
|
return children;
|
|
327
330
|
};
|
|
328
|
-
lookUpChildren(children, meta
|
|
331
|
+
lookUpChildren(children, meta);
|
|
329
332
|
/* v8 ignore next */
|
|
330
333
|
where[meta.root.discriminatorColumn] =
|
|
331
334
|
children.length > 0
|
|
@@ -355,10 +358,18 @@ export class EntityManager {
|
|
|
355
358
|
const field = hint.field.split(':')[0];
|
|
356
359
|
const prop = meta.properties[field];
|
|
357
360
|
const strategy = getLoadingStrategy(prop.strategy || hint.strategy || options.strategy || this.config.get('loadStrategy'), prop.kind);
|
|
358
|
-
const joined = strategy === LoadStrategy.JOINED
|
|
361
|
+
const joined = (strategy === LoadStrategy.JOINED || (prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner)) &&
|
|
362
|
+
prop.kind !== ReferenceKind.SCALAR;
|
|
359
363
|
if (!joined && !hint.filter) {
|
|
360
364
|
continue;
|
|
361
365
|
}
|
|
366
|
+
// Polymorphic to-one relations are already filtered via per-target LEFT JOINs created
|
|
367
|
+
// for the `:ref filter` hint; emitting a populate-filter entry here would auto-join only
|
|
368
|
+
// the first target meta and either drop rows pointing to other targets or reference
|
|
369
|
+
// parent-table-only columns on the child sub-table alias for TPT children.
|
|
370
|
+
if (prop.polymorphic && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
362
373
|
const filters = QueryHelper.mergePropertyFilters(prop.filters, options.filters);
|
|
363
374
|
const where = await this.applyFilters(prop.targetMeta.class, {}, filters, 'read', {
|
|
364
375
|
...options,
|
|
@@ -424,11 +435,13 @@ export class EntityManager {
|
|
|
424
435
|
const populated = options.populate.filter(({ field }) => field.split(':')[0] === prop.name);
|
|
425
436
|
let found = false;
|
|
426
437
|
for (const hint of populated) {
|
|
427
|
-
|
|
438
|
+
const strategy = getLoadingStrategy(prop.strategy || hint.strategy || options.strategy || this.config.get('loadStrategy'), prop.kind);
|
|
439
|
+
// inverse 1:1 always produces a JOIN (forced by `joinedProps()`), regardless of strategy
|
|
440
|
+
const willJoin = strategy === LoadStrategy.JOINED || (prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner);
|
|
441
|
+
if (!hint.all || willJoin) {
|
|
428
442
|
hint.filter = true;
|
|
429
443
|
}
|
|
430
|
-
|
|
431
|
-
if (hint.field === `${prop.name}:ref` || (hint.filter && strategy === LoadStrategy.JOINED)) {
|
|
444
|
+
if (hint.field === `${prop.name}:ref` || (hint.filter && willJoin)) {
|
|
432
445
|
found = true;
|
|
433
446
|
}
|
|
434
447
|
}
|
|
@@ -442,7 +455,23 @@ export class EntityManager {
|
|
|
442
455
|
const prop = meta?.properties[field];
|
|
443
456
|
if (prop && !ref) {
|
|
444
457
|
hint.children ??= [];
|
|
445
|
-
|
|
458
|
+
const targets = prop.polymorphic && prop.polymorphTargets?.length ? prop.polymorphTargets : [prop.targetMeta];
|
|
459
|
+
for (const targetMeta of targets) {
|
|
460
|
+
const before = hint.children.length;
|
|
461
|
+
await this.autoJoinRefsForFilters(targetMeta, { ...options, populate: hint.children }, { class: meta.root.class, propName: prop.name });
|
|
462
|
+
// For polymorphic relations, `hint.children` is applied to every polymorph target during
|
|
463
|
+
// population, so drop any auto-added hint whose field does not exist on every target —
|
|
464
|
+
// otherwise the shared `:ref` would crash when applied to a target lacking it (GH #7722).
|
|
465
|
+
if (targets.length > 1 && hint.children.length > before) {
|
|
466
|
+
const added = hint.children.splice(before);
|
|
467
|
+
for (const h of added) {
|
|
468
|
+
const f = h.field.split(':')[0];
|
|
469
|
+
if (targets.every(t => f in t.properties)) {
|
|
470
|
+
hint.children.push(h);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
446
475
|
}
|
|
447
476
|
}
|
|
448
477
|
}
|
|
@@ -494,10 +523,6 @@ export class EntityManager {
|
|
|
494
523
|
const conds = [...ret, where].filter(c => Utils.hasObjectKeys(c) || Raw.hasObjectFragments(c));
|
|
495
524
|
return conds.length > 1 ? { $and: conds } : conds[0];
|
|
496
525
|
}
|
|
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
526
|
async findAndCount(entityName, where, options = {}) {
|
|
502
527
|
const em = this.getContext(false);
|
|
503
528
|
await em.tryFlush(entityName, options);
|
|
@@ -631,9 +656,6 @@ export class EntityManager {
|
|
|
631
656
|
}
|
|
632
657
|
return entity;
|
|
633
658
|
}
|
|
634
|
-
/**
|
|
635
|
-
* Finds first entity matching your `where` query.
|
|
636
|
-
*/
|
|
637
659
|
async findOne(entityName, where, options = {}) {
|
|
638
660
|
if (options.disableIdentityMap ?? this.config.get('disableIdentityMap')) {
|
|
639
661
|
const em = this.getContext(false);
|
|
@@ -653,10 +675,11 @@ export class EntityManager {
|
|
|
653
675
|
}
|
|
654
676
|
await em.tryFlush(entityName, options);
|
|
655
677
|
const meta = em.metadata.get(entityName);
|
|
678
|
+
em.validateIndexUsage(meta, where, options);
|
|
656
679
|
where = await em.processWhere(entityName, where, options, 'read');
|
|
657
680
|
validateEmptyWhere(where);
|
|
658
681
|
em.checkLockRequirements(options.lockMode, meta);
|
|
659
|
-
const isOptimisticLocking = options.lockMode == null || options.lockMode === LockMode.OPTIMISTIC;
|
|
682
|
+
const isOptimisticLocking = options.lockMode == null || options.lockMode === LockMode.NONE || options.lockMode === LockMode.OPTIMISTIC;
|
|
660
683
|
if (entity && !em.shouldRefresh(meta, entity, options) && isOptimisticLocking) {
|
|
661
684
|
return em.lockAndPopulate(meta, entity, where, options);
|
|
662
685
|
}
|
|
@@ -701,12 +724,6 @@ export class EntityManager {
|
|
|
701
724
|
await em.storeCache(options.cache, cached, () => helper(entity).toPOJO());
|
|
702
725
|
return entity;
|
|
703
726
|
}
|
|
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
727
|
async findOneOrFail(entityName, where, options = {}) {
|
|
711
728
|
let entity;
|
|
712
729
|
let isStrictViolation = false;
|
|
@@ -803,7 +820,10 @@ export class EntityManager {
|
|
|
803
820
|
data = QueryHelper.processObjectParams(data);
|
|
804
821
|
validateParams(data, 'insert data');
|
|
805
822
|
if (em.eventManager.hasListeners(EventType.beforeUpsert, meta)) {
|
|
806
|
-
await em.eventManager.dispatchEvent(EventType.beforeUpsert, { entity: data, em, meta }, meta);
|
|
823
|
+
await em.eventManager.dispatchEvent(EventType.beforeUpsert, { entity: entity ?? data, em, meta }, meta);
|
|
824
|
+
if (entity) {
|
|
825
|
+
data = em.#comparator.prepareEntity(entity);
|
|
826
|
+
}
|
|
807
827
|
}
|
|
808
828
|
const ret = await em.driver.nativeUpdate(entityName, where, data, {
|
|
809
829
|
ctx: em.#transactionContext,
|
|
@@ -852,6 +872,7 @@ export class EntityManager {
|
|
|
852
872
|
em.getHydrator().hydrate(entity, meta, data2, em.#entityFactory, 'full', false, true);
|
|
853
873
|
}
|
|
854
874
|
// recompute the data as there might be some values missing (e.g. those with db column defaults)
|
|
875
|
+
resetUntouchedCollections(meta, entity);
|
|
855
876
|
const snapshot = this.#comparator.prepareEntity(entity);
|
|
856
877
|
em.#unitOfWork.register(entity, snapshot, { refresh: true });
|
|
857
878
|
if (em.eventManager.hasListeners(EventType.afterUpsert, meta)) {
|
|
@@ -918,6 +939,7 @@ export class EntityManager {
|
|
|
918
939
|
const allWhere = [];
|
|
919
940
|
const entities = new Map();
|
|
920
941
|
const entitiesByData = new Map();
|
|
942
|
+
const entitiesByAllDataIdx = new Map();
|
|
921
943
|
for (let i = 0; i < data.length; i++) {
|
|
922
944
|
let row = data[i];
|
|
923
945
|
let where;
|
|
@@ -931,6 +953,7 @@ export class EntityManager {
|
|
|
931
953
|
}
|
|
932
954
|
where = helper(entity).getPrimaryKey();
|
|
933
955
|
em.#entityFactory.assignDefaultValues(entity, meta);
|
|
956
|
+
entitiesByAllDataIdx.set(allData.length, entity);
|
|
934
957
|
row = em.#comparator.prepareEntity(entity);
|
|
935
958
|
}
|
|
936
959
|
else {
|
|
@@ -978,6 +1001,9 @@ export class EntityManager {
|
|
|
978
1001
|
const entity = entitiesByData.get(dto) ?? dto;
|
|
979
1002
|
await em.eventManager.dispatchEvent(EventType.beforeUpsert, { entity, em, meta }, meta);
|
|
980
1003
|
}
|
|
1004
|
+
for (const [idx, entity] of entitiesByAllDataIdx) {
|
|
1005
|
+
allData[idx] = em.#comparator.prepareEntity(entity);
|
|
1006
|
+
}
|
|
981
1007
|
}
|
|
982
1008
|
const res = await em.driver.nativeUpdateMany(entityName, allWhere, allData, {
|
|
983
1009
|
ctx: em.#transactionContext,
|
|
@@ -1078,6 +1104,7 @@ export class EntityManager {
|
|
|
1078
1104
|
}
|
|
1079
1105
|
for (const [entity] of entities) {
|
|
1080
1106
|
// recompute the data as there might be some values missing (e.g. those with db column defaults)
|
|
1107
|
+
resetUntouchedCollections(meta, entity);
|
|
1081
1108
|
const snapshot = this.#comparator.prepareEntity(entity);
|
|
1082
1109
|
em.#unitOfWork.register(entity, snapshot, { refresh: true });
|
|
1083
1110
|
}
|
|
@@ -1171,6 +1198,7 @@ export class EntityManager {
|
|
|
1171
1198
|
*/
|
|
1172
1199
|
async lock(entity, lockMode, options = {}) {
|
|
1173
1200
|
options = Utils.isPlainObject(options) ? options : { lockVersion: options };
|
|
1201
|
+
this.getContext(false).prepareOptions(options);
|
|
1174
1202
|
await this.getUnitOfWork().lock(entity, { lockMode, ...options });
|
|
1175
1203
|
}
|
|
1176
1204
|
/**
|
|
@@ -1192,9 +1220,6 @@ export class EntityManager {
|
|
|
1192
1220
|
helper(data).setSchema(options.schema);
|
|
1193
1221
|
}
|
|
1194
1222
|
if (!helper(data).__managed) {
|
|
1195
|
-
// the entity might have been created via `em.create()`, which adds it to the persist stack automatically
|
|
1196
|
-
em.#unitOfWork.getPersistStack().delete(data);
|
|
1197
|
-
// it can be also in the identity map if it had a PK value already
|
|
1198
1223
|
em.#unitOfWork.unsetIdentity(data);
|
|
1199
1224
|
}
|
|
1200
1225
|
const meta = helper(data).__meta;
|
|
@@ -1283,9 +1308,6 @@ export class EntityManager {
|
|
|
1283
1308
|
helper(row).setSchema(options.schema);
|
|
1284
1309
|
}
|
|
1285
1310
|
if (!helper(row).__managed) {
|
|
1286
|
-
// the entity might have been created via `em.create()`, which adds it to the persist stack automatically
|
|
1287
|
-
em.#unitOfWork.getPersistStack().delete(row);
|
|
1288
|
-
// it can be also in the identity map if it had a PK value already
|
|
1289
1311
|
em.#unitOfWork.unsetIdentity(row);
|
|
1290
1312
|
}
|
|
1291
1313
|
const payload = em.#comparator.prepareEntity(row);
|
|
@@ -1323,6 +1345,37 @@ export class EntityManager {
|
|
|
1323
1345
|
});
|
|
1324
1346
|
return res.affectedRows;
|
|
1325
1347
|
}
|
|
1348
|
+
/**
|
|
1349
|
+
* Invokes a stored procedure or function declared via the {@link Routine} class. Arg and return
|
|
1350
|
+
* types are inferred from the literal config; use {@link Routine.create} to refine when needed.
|
|
1351
|
+
*
|
|
1352
|
+
* Procedures emitting result sets (MySQL `SELECT`s, PG/Oracle refcursor OUT params) return
|
|
1353
|
+
* `Dictionary[][]`; MSSQL multi-result procs aren't exposed here, use `em.getConnection().execute()`.
|
|
1354
|
+
*
|
|
1355
|
+
* Mongo throws. SQLite bridges `bodyJs` functions via better-sqlite3 UDFs (procedures and
|
|
1356
|
+
* function-without-bodyJs throw); libSQL throws unconditionally. Oracle calls run on their own
|
|
1357
|
+
* pool connection with `autoCommit: true`, so wrapping in `em.transactional(...)` throws.
|
|
1358
|
+
*
|
|
1359
|
+
* @example
|
|
1360
|
+
* ```ts
|
|
1361
|
+
* const hash = await em.callRoutine(HashUser, { name: 'jon', salt: 'pepper' });
|
|
1362
|
+
*
|
|
1363
|
+
* const hashRef = new ScalarReference<string>();
|
|
1364
|
+
* await em.callRoutine(AddRecord, { p_name: 'jon', p_age: 30, p_hash: hashRef });
|
|
1365
|
+
*
|
|
1366
|
+
* const TwoCursors = Routine.create<Record<string, never>, unknown[][]>({ ... });
|
|
1367
|
+
* const [users, books] = await em.transactional(em => em.callRoutine(TwoCursors, {}));
|
|
1368
|
+
* ```
|
|
1369
|
+
*/
|
|
1370
|
+
async callRoutine(routine, args) {
|
|
1371
|
+
const em = this.getContext(false);
|
|
1372
|
+
// Identity by reference so the user-held import survives bundling and tree-shakes unused routines.
|
|
1373
|
+
if (!em.config.hasRoutine(routine)) {
|
|
1374
|
+
throw new Error(`Routine '${routine.name}' is not registered in the 'routines' config option.`);
|
|
1375
|
+
}
|
|
1376
|
+
const conn = em.driver.getConnection('write');
|
|
1377
|
+
return conn.callRoutine(routine, args, em.#transactionContext);
|
|
1378
|
+
}
|
|
1326
1379
|
/**
|
|
1327
1380
|
* Fires native delete query. Calling this has no side effects on the context (identity map).
|
|
1328
1381
|
*/
|
|
@@ -1471,6 +1524,29 @@ export class EntityManager {
|
|
|
1471
1524
|
await em.storeCache(options.cache, cached, () => +count);
|
|
1472
1525
|
return +count;
|
|
1473
1526
|
}
|
|
1527
|
+
/**
|
|
1528
|
+
* Counts entities grouped by one or more properties. Returns a dictionary keyed by the grouped
|
|
1529
|
+
* field value(s), with counts as values. For composite `groupBy`, keys are joined with `~~~`.
|
|
1530
|
+
*
|
|
1531
|
+
* SQL drivers issue a single `GROUP BY` query; MongoDB uses an aggregation pipeline.
|
|
1532
|
+
*
|
|
1533
|
+
* @example
|
|
1534
|
+
* ```ts
|
|
1535
|
+
* // Count books per author
|
|
1536
|
+
* const counts = await em.countBy(Book, 'author');
|
|
1537
|
+
* // { '1': 2, '2': 1, '3': 3 }
|
|
1538
|
+
*
|
|
1539
|
+
* // Count with a filter
|
|
1540
|
+
* const counts = await em.countBy(Book, 'author', { where: { active: true } });
|
|
1541
|
+
*
|
|
1542
|
+
* // Composite groupBy — keys joined with ~~~
|
|
1543
|
+
* const counts = await em.countBy(Order, ['status', 'country']);
|
|
1544
|
+
* // { 'pending~~~US': 5, 'shipped~~~DE': 3 }
|
|
1545
|
+
* ```
|
|
1546
|
+
*/
|
|
1547
|
+
async countBy(entityName, groupBy, options) {
|
|
1548
|
+
throw new Error(`${this.constructor.name}.countBy() is not supported by the current driver`);
|
|
1549
|
+
}
|
|
1474
1550
|
/**
|
|
1475
1551
|
* Tells the EntityManager to make an instance managed and persistent.
|
|
1476
1552
|
* The entity will be entered into the database at or before transaction commit or as a result of the flush operation.
|
|
@@ -1576,7 +1652,7 @@ export class EntityManager {
|
|
|
1576
1652
|
const em = this.getContext();
|
|
1577
1653
|
em.prepareOptions(options);
|
|
1578
1654
|
const entityName = arr[0].constructor;
|
|
1579
|
-
const preparedPopulate = await em.preparePopulate(entityName, { populate: populate, filters: options.filters }, options.validate);
|
|
1655
|
+
const preparedPopulate = await em.preparePopulate(entityName, { populate: populate, filters: options.filters, populateHints: options.populateHints }, options.validate);
|
|
1580
1656
|
await em.#entityLoader.populate(entityName, arr, preparedPopulate, options);
|
|
1581
1657
|
return entities;
|
|
1582
1658
|
}
|
|
@@ -1609,6 +1685,8 @@ export class EntityManager {
|
|
|
1609
1685
|
fork.#filterParams = Utils.copy(em.#filterParams);
|
|
1610
1686
|
fork.loggerContext = Utils.merge({}, em.loggerContext, options.loggerContext);
|
|
1611
1687
|
fork.#schema = options.schema ?? em.#schema;
|
|
1688
|
+
fork.signal = options.signal ?? em.signal;
|
|
1689
|
+
fork.inflightQueryAbortStrategy = options.inflightQueryAbortStrategy ?? em.inflightQueryAbortStrategy;
|
|
1612
1690
|
if (!options.clear) {
|
|
1613
1691
|
for (const entity of em.#unitOfWork.getIdentityMap()) {
|
|
1614
1692
|
fork.#unitOfWork.register(entity);
|
|
@@ -1684,6 +1762,22 @@ export class EntityManager {
|
|
|
1684
1762
|
getTransactionContext() {
|
|
1685
1763
|
return this.getContext(false).#transactionContext;
|
|
1686
1764
|
}
|
|
1765
|
+
/**
|
|
1766
|
+
* Returns the cancellation defaults configured on this EntityManager (via `em.fork({ signal })`
|
|
1767
|
+
* or inherited from a transactional fork). Returns `undefined` when no signal is set.
|
|
1768
|
+
*
|
|
1769
|
+
* @internal — exposed for subclass drivers and `UnitOfWork`; not part of the public API.
|
|
1770
|
+
*/
|
|
1771
|
+
getAbortOptions() {
|
|
1772
|
+
const em = this.getContext(false);
|
|
1773
|
+
if (em.signal == null && em.inflightQueryAbortStrategy == null) {
|
|
1774
|
+
return undefined;
|
|
1775
|
+
}
|
|
1776
|
+
return {
|
|
1777
|
+
signal: em.signal,
|
|
1778
|
+
inflightQueryAbortStrategy: em.inflightQueryAbortStrategy,
|
|
1779
|
+
};
|
|
1780
|
+
}
|
|
1687
1781
|
/**
|
|
1688
1782
|
* Sets the transaction context.
|
|
1689
1783
|
*/
|
|
@@ -1727,6 +1821,79 @@ export class EntityManager {
|
|
|
1727
1821
|
throw ValidationError.transactionRequired();
|
|
1728
1822
|
}
|
|
1729
1823
|
}
|
|
1824
|
+
validateIndexUsage(meta, where, options) {
|
|
1825
|
+
if (!options.using) {
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
const indexNames = Utils.asArray(options.using);
|
|
1829
|
+
const allIndexes = [...meta.indexes, ...meta.uniques];
|
|
1830
|
+
const indexMap = new Map();
|
|
1831
|
+
for (const idx of allIndexes) {
|
|
1832
|
+
if (idx.name) {
|
|
1833
|
+
indexMap.set(idx.name, Utils.asArray(idx.properties ?? []));
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
for (const prop of meta.props) {
|
|
1837
|
+
if (typeof prop.index === 'string') {
|
|
1838
|
+
indexMap.set(prop.index, [prop.name]);
|
|
1839
|
+
}
|
|
1840
|
+
if (typeof prop.unique === 'string') {
|
|
1841
|
+
indexMap.set(prop.unique, [prop.name]);
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
const allowedProps = new Set();
|
|
1845
|
+
for (const name of indexNames) {
|
|
1846
|
+
const props = indexMap.get(name);
|
|
1847
|
+
if (!props) {
|
|
1848
|
+
const available = [...indexMap.keys()];
|
|
1849
|
+
throw new Error(`Index '${name}' not found on entity '${meta.className}'. ` +
|
|
1850
|
+
(available.length > 0 ? `Available indexes: ${available.join(', ')}` : 'No named indexes defined.'));
|
|
1851
|
+
}
|
|
1852
|
+
for (const prop of props) {
|
|
1853
|
+
allowedProps.add(prop);
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
this.validateWhereKeysForIndex(where, allowedProps, indexNames);
|
|
1857
|
+
if (options.orderBy) {
|
|
1858
|
+
const orderMaps = Array.isArray(options.orderBy) ? options.orderBy : [options.orderBy];
|
|
1859
|
+
for (const orderMap of orderMaps) {
|
|
1860
|
+
for (const key of Object.keys(orderMap)) {
|
|
1861
|
+
if (!allowedProps.has(key)) {
|
|
1862
|
+
throw new Error(`Property '${key}' in orderBy is not covered by index '${indexNames.join("', '")}'. ` +
|
|
1863
|
+
`Allowed properties: ${[...allowedProps].join(', ')}`);
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
validateWhereKeysForIndex(where, allowedProps, indexNames) {
|
|
1870
|
+
if (typeof where !== 'object' || where === null) {
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1873
|
+
if (Array.isArray(where)) {
|
|
1874
|
+
for (const item of where) {
|
|
1875
|
+
this.validateWhereKeysForIndex(item, allowedProps, indexNames);
|
|
1876
|
+
}
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1879
|
+
for (const key of Object.keys(where)) {
|
|
1880
|
+
if (key === '$and' || key === '$or') {
|
|
1881
|
+
for (const item of Utils.asArray(where[key])) {
|
|
1882
|
+
this.validateWhereKeysForIndex(item, allowedProps, indexNames);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
else if (key === '$not') {
|
|
1886
|
+
this.validateWhereKeysForIndex(where[key], allowedProps, indexNames);
|
|
1887
|
+
}
|
|
1888
|
+
else if (key.startsWith('$')) {
|
|
1889
|
+
continue;
|
|
1890
|
+
}
|
|
1891
|
+
else if (!allowedProps.has(key)) {
|
|
1892
|
+
throw new Error(`Property '${key}' in where clause is not covered by index '${indexNames.join("', '")}'. ` +
|
|
1893
|
+
`Allowed properties: ${[...allowedProps].join(', ')}`);
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1730
1897
|
async lockAndPopulate(meta, entity, where, options) {
|
|
1731
1898
|
if (!meta.virtual && options.lockMode === LockMode.OPTIMISTIC) {
|
|
1732
1899
|
await this.lock(entity, options.lockMode, {
|
|
@@ -1761,8 +1928,17 @@ export class EntityManager {
|
|
|
1761
1928
|
return [];
|
|
1762
1929
|
}
|
|
1763
1930
|
const meta = this.metadata.find(entityName);
|
|
1764
|
-
// infer populate
|
|
1765
|
-
|
|
1931
|
+
// infer populate hints from `fields` when present; merge with explicit `populate` if both are set.
|
|
1932
|
+
// `populateArray` is only kept when the entries are user-provided strings — when this method runs a
|
|
1933
|
+
// second time (e.g. from `lockAndPopulate`) the populate is already normalized and merging would be a no-op.
|
|
1934
|
+
const fields = options.fields ? this.buildFields(options.fields) : undefined;
|
|
1935
|
+
const populateArray = Array.isArray(options.populate) && options.populate.every(p => typeof p === 'string')
|
|
1936
|
+
? options.populate
|
|
1937
|
+
: undefined;
|
|
1938
|
+
const hasNestedFields = fields?.some(f => f.includes('.')) ?? false;
|
|
1939
|
+
const shouldInfer = fields !== undefined && options.populate === undefined;
|
|
1940
|
+
const shouldMerge = fields !== undefined && populateArray !== undefined && populateArray.length > 0 && hasNestedFields;
|
|
1941
|
+
if (shouldInfer || shouldMerge) {
|
|
1766
1942
|
// we need to prune the `populate` hint from to-one relations, as partially loading them does not require their population, we want just the FK
|
|
1767
1943
|
const pruneToOneRelations = (meta, fields) => {
|
|
1768
1944
|
const ret = [];
|
|
@@ -1796,7 +1972,19 @@ export class EntityManager {
|
|
|
1796
1972
|
}
|
|
1797
1973
|
return Utils.unique(ret);
|
|
1798
1974
|
};
|
|
1799
|
-
|
|
1975
|
+
const fromFields = pruneToOneRelations(meta, fields);
|
|
1976
|
+
if (shouldInfer) {
|
|
1977
|
+
options.populate = fromFields;
|
|
1978
|
+
}
|
|
1979
|
+
else {
|
|
1980
|
+
// with an explicit populate, only nested paths (e.g. `item.id`) need to be merged in — they
|
|
1981
|
+
// require a JOIN to access sub-fields, whereas bare names are already handled by the driver.
|
|
1982
|
+
// skip paths already in user's `populate` (regardless of `:ref` modifier) to avoid producing
|
|
1983
|
+
// both a `:ref` and a non-`:ref` entry for the same relation
|
|
1984
|
+
const existing = new Set(populateArray.map(p => p.split(':')[0]));
|
|
1985
|
+
const extras = fromFields.filter(f => f.includes('.') && !existing.has(f));
|
|
1986
|
+
options.populate = [...populateArray, ...extras];
|
|
1987
|
+
}
|
|
1800
1988
|
}
|
|
1801
1989
|
if (!options.populate) {
|
|
1802
1990
|
const populate = this.#entityLoader.normalizePopulate(entityName, [], options.strategy, true, options.exclude);
|
|
@@ -1836,9 +2024,21 @@ export class EntityManager {
|
|
|
1836
2024
|
}
|
|
1837
2025
|
if (options.populateHints) {
|
|
1838
2026
|
applyPopulateHints(populate, options.populateHints);
|
|
2027
|
+
this.forceSelectInForLimitedPopulate(populate);
|
|
1839
2028
|
}
|
|
1840
2029
|
return populate;
|
|
1841
2030
|
}
|
|
2031
|
+
/** Force SELECT_IN strategy on populate entries with `limit`, since JOINED cannot do per-parent limiting. */
|
|
2032
|
+
forceSelectInForLimitedPopulate(populate) {
|
|
2033
|
+
for (const entry of populate) {
|
|
2034
|
+
if (entry.limit != null) {
|
|
2035
|
+
entry.strategy = LoadStrategy.SELECT_IN;
|
|
2036
|
+
}
|
|
2037
|
+
if (entry.children?.length) {
|
|
2038
|
+
this.forceSelectInForLimitedPopulate(entry.children);
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
1842
2042
|
/**
|
|
1843
2043
|
* when the entity is found in identity map, we check if it was partially loaded or we are trying to populate
|
|
1844
2044
|
* some additional lazy properties, if so, we reload and merge the data from database
|
|
@@ -1870,6 +2070,8 @@ export class EntityManager {
|
|
|
1870
2070
|
throw new ValidationError(`Cannot combine 'fields' and 'exclude' option.`);
|
|
1871
2071
|
}
|
|
1872
2072
|
options.schema ??= this.#schema;
|
|
2073
|
+
options.signal ??= this.signal;
|
|
2074
|
+
options.inflightQueryAbortStrategy ??= this.inflightQueryAbortStrategy;
|
|
1873
2075
|
options.logging = options.loggerContext = Utils.merge({ id: this.id }, this.loggerContext, options.loggerContext, options.logging);
|
|
1874
2076
|
}
|
|
1875
2077
|
/**
|
|
@@ -1878,7 +2080,15 @@ export class EntityManager {
|
|
|
1878
2080
|
cacheKey(entityName, options, method, where) {
|
|
1879
2081
|
const { ...opts } = options;
|
|
1880
2082
|
// ignore some irrelevant options, e.g. logger context can contain dynamic data for the same query
|
|
1881
|
-
for (const k of [
|
|
2083
|
+
for (const k of [
|
|
2084
|
+
'ctx',
|
|
2085
|
+
'strategy',
|
|
2086
|
+
'flushMode',
|
|
2087
|
+
'logging',
|
|
2088
|
+
'loggerContext',
|
|
2089
|
+
'signal',
|
|
2090
|
+
'inflightQueryAbortStrategy',
|
|
2091
|
+
]) {
|
|
1882
2092
|
delete opts[k];
|
|
1883
2093
|
}
|
|
1884
2094
|
return [Utils.className(entityName), method, opts, where];
|
|
@@ -1971,6 +2181,8 @@ export class EntityManager {
|
|
|
1971
2181
|
return (em.#loaders[type] ??= new DataLoader(DataloaderUtils.getColBatchLoadFn(em)));
|
|
1972
2182
|
case 'm:n':
|
|
1973
2183
|
return (em.#loaders[type] ??= new DataLoader(DataloaderUtils.getManyToManyColBatchLoadFn(em)));
|
|
2184
|
+
case 'count':
|
|
2185
|
+
return (em.#loaders[type] ??= new DataLoader(DataloaderUtils.getCountBatchLoadFn(em)));
|
|
1974
2186
|
}
|
|
1975
2187
|
}
|
|
1976
2188
|
/**
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<a href="https://mikro-orm.io"><img src="https://raw.githubusercontent.com/mikro-orm/mikro-orm/master/docs/static/img/logo-readme.svg?sanitize=true" alt="MikroORM" /></a>
|
|
3
3
|
</h1>
|
|
4
4
|
|
|
5
|
-
TypeScript ORM for Node.js based on Data Mapper, [Unit of Work](https://mikro-orm.io/docs/unit-of-work/) and [Identity Map](https://mikro-orm.io/docs/identity-map/) patterns. Supports MongoDB, MySQL, MariaDB, PostgreSQL, SQLite (including libSQL), MSSQL and Oracle databases.
|
|
5
|
+
TypeScript ORM for Node.js based on Data Mapper, [Unit of Work](https://mikro-orm.io/docs/unit-of-work/) and [Identity Map](https://mikro-orm.io/docs/identity-map/) patterns. Supports MongoDB, MySQL, MariaDB, PostgreSQL (including CockroachDB and PGlite), SQLite (including libSQL), MSSQL and Oracle databases.
|
|
6
6
|
|
|
7
7
|
> Heavily inspired by [Doctrine](https://www.doctrine-project.org/) and [Hibernate](https://hibernate.org/).
|
|
8
8
|
|
|
@@ -19,6 +19,7 @@ Install a driver package for your database:
|
|
|
19
19
|
|
|
20
20
|
```sh
|
|
21
21
|
npm install @mikro-orm/postgresql # PostgreSQL
|
|
22
|
+
npm install @mikro-orm/pglite # PGlite (embedded PostgreSQL in WASM)
|
|
22
23
|
npm install @mikro-orm/mysql # MySQL
|
|
23
24
|
npm install @mikro-orm/mariadb # MariaDB
|
|
24
25
|
npm install @mikro-orm/sqlite # SQLite
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { type Configuration, type ConnectionOptions } from '../utils/Configuration.js';
|
|
2
2
|
import type { LogContext, Logger } from '../logging/Logger.js';
|
|
3
3
|
import type { MetadataStorage } from '../metadata/MetadataStorage.js';
|
|
4
|
-
import type { ConnectionType, Dictionary, MaybePromise, Primary } from '../typings.js';
|
|
4
|
+
import type { ConnectionType, Dictionary, MaybePromise, Primary, RoutineProperty } from '../typings.js';
|
|
5
|
+
import type { Routine } from '../metadata/Routine.js';
|
|
5
6
|
import type { Platform } from '../platforms/Platform.js';
|
|
7
|
+
import type { Type } from '../types/Type.js';
|
|
6
8
|
import type { TransactionEventBroadcaster } from '../events/TransactionEventBroadcaster.js';
|
|
7
9
|
import type { IsolationLevel } from '../enums.js';
|
|
8
10
|
/** Abstract base class for database connections, providing transaction and query execution support. */
|
|
@@ -72,6 +74,41 @@ export declare abstract class Connection {
|
|
|
72
74
|
rollback(ctx: Transaction, eventBroadcaster?: TransactionEventBroadcaster, loggerContext?: LogContext): Promise<void>;
|
|
73
75
|
/** Executes a raw query and returns the result. */
|
|
74
76
|
abstract execute<T>(query: string, params?: any[], method?: 'all' | 'get' | 'run', ctx?: Transaction): Promise<QueryResult<T> | any | any[]>;
|
|
77
|
+
/** @internal — public callers go through {@link EntityManager.callRoutine}. */
|
|
78
|
+
callRoutine<T>(routine: Routine, args: Record<string, unknown>, ctx?: Transaction): Promise<T>;
|
|
79
|
+
/**
|
|
80
|
+
* Unwraps a routine argument (resolving any `ScalarReference` wrapper) and, when the param
|
|
81
|
+
* declares a `customType`, marshals it through `convertToDatabaseValue`. `undefined` is
|
|
82
|
+
* normalised to `null` so every driver sees the same shape.
|
|
83
|
+
*
|
|
84
|
+
* @internal
|
|
85
|
+
*/
|
|
86
|
+
protected convertRoutineInbound(value: unknown, param: RoutineProperty | undefined): unknown;
|
|
87
|
+
/**
|
|
88
|
+
* Converts a raw database value to its JS representation via the supplied `customType`, when
|
|
89
|
+
* one is declared. Used to marshal scalar function returns and OUT/INOUT values back to the
|
|
90
|
+
* caller before they land in a `ScalarReference` or `em.callRoutine`'s return value.
|
|
91
|
+
*
|
|
92
|
+
* @internal
|
|
93
|
+
*/
|
|
94
|
+
protected convertRoutineOutbound<T>(value: unknown, customType: Type<unknown> | undefined): T;
|
|
95
|
+
/**
|
|
96
|
+
* Executes a scalar function routine as `select <qualified>(?, ?, ...) as value`, marshalling
|
|
97
|
+
* IN params on the way in and the return value on the way out. The qualified name is built
|
|
98
|
+
* from `routine.schema`, falling back to the platform's default schema (e.g. `dbo` on MSSQL),
|
|
99
|
+
* which gives MySQL/SQLite a bare name and MSSQL/Oracle the mandatory `schema.name` form.
|
|
100
|
+
*
|
|
101
|
+
* @internal
|
|
102
|
+
*/
|
|
103
|
+
protected callRoutineFunction<T>(routine: Routine, args: Record<string, unknown>, ctx?: Transaction): Promise<T>;
|
|
104
|
+
/**
|
|
105
|
+
* Walks a result row produced by an OUT/INOUT-param SELECT and writes each value into the
|
|
106
|
+
* caller's `ScalarReference` slot. Non-reference args are ignored (the user opted out of
|
|
107
|
+
* receiving the OUT value).
|
|
108
|
+
*
|
|
109
|
+
* @internal
|
|
110
|
+
*/
|
|
111
|
+
protected applyRoutineOutParams(row: Dictionary, outParams: RoutineProperty[], args: Record<string, unknown>): void;
|
|
75
112
|
/** Parses and returns the resolved connection configuration (host, port, user, etc.). */
|
|
76
113
|
getConnectionOptions(): ConnectionConfig;
|
|
77
114
|
/** Sets the metadata storage on this connection. */
|
|
@@ -104,3 +141,32 @@ export interface ConnectionConfig {
|
|
|
104
141
|
}
|
|
105
142
|
/** Opaque transaction context type, wrapping the driver-specific transaction object. */
|
|
106
143
|
export type Transaction<T = any> = T & {};
|
|
144
|
+
/**
|
|
145
|
+
* Strategy applied when an `AbortSignal` fires while a query is in flight.
|
|
146
|
+
*
|
|
147
|
+
* - `'ignore query'` — stop awaiting; the query keeps running on the server until it settles
|
|
148
|
+
* (the connection returns to the pool only when the database replies).
|
|
149
|
+
* - `'cancel query'` — ask the database to cancel the running query (e.g. `pg_cancel_backend`,
|
|
150
|
+
* `KILL QUERY`). Falls back to `'ignore query'` if the dialect cannot cancel.
|
|
151
|
+
* Most engines do not cancel writes; partial commits are possible.
|
|
152
|
+
* - `'kill session'` — terminate the database session/process the query runs in
|
|
153
|
+
* (`pg_terminate_backend` etc.). Falls back to `'cancel query'` if not supported.
|
|
154
|
+
*
|
|
155
|
+
* Default: `'ignore query'`.
|
|
156
|
+
*
|
|
157
|
+
* **Streaming queries (`em.stream()` / `qb.stream()`):** the strategy is silently treated as
|
|
158
|
+
* `'ignore query'` because the underlying driver only accepts a plain `AbortSignal` for
|
|
159
|
+
* streamed reads — there is no server-side cancel for an open cursor. The MongoDB driver also
|
|
160
|
+
* has no notion of strategies; only the signal is honored there.
|
|
161
|
+
*/
|
|
162
|
+
export type InflightQueryAbortStrategy = 'ignore query' | 'cancel query' | 'kill session';
|
|
163
|
+
/** Per-query cancellation controls forwarded to the underlying driver. */
|
|
164
|
+
export interface AbortQueryOptions {
|
|
165
|
+
/** AbortSignal that cancels the query when fired. */
|
|
166
|
+
signal?: AbortSignal;
|
|
167
|
+
/**
|
|
168
|
+
* Strategy used when the signal fires while the query is in flight. See
|
|
169
|
+
* {@apilink InflightQueryAbortStrategy} for caveats around streams and MongoDB.
|
|
170
|
+
*/
|
|
171
|
+
inflightQueryAbortStrategy?: InflightQueryAbortStrategy;
|
|
172
|
+
}
|