import { getOnConflictReturningFields, getWhereCondition } from './utils/upsert-utils.js'; import { Utils } from './utils/Utils.js'; import { Cursor } from './utils/Cursor.js'; import { QueryHelper } from './utils/QueryHelper.js'; import { TransactionContext } from './utils/TransactionContext.js'; import { isRaw, Raw } from './utils/RawQueryFragment.js'; import { EntityFactory } from './entity/EntityFactory.js'; import { EntityAssigner } from './entity/EntityAssigner.js'; import { validateEmptyWhere, validateParams, validatePrimaryKey, validateProperty } from './entity/validators.js'; import { EntityLoader } from './entity/EntityLoader.js'; import { Reference } from './entity/Reference.js'; import { helper } from './entity/wrap.js'; import { ChangeSet, ChangeSetType } from './unit-of-work/ChangeSet.js'; import { UnitOfWork } from './unit-of-work/UnitOfWork.js'; import { EventType, FlushMode, LoadStrategy, LockMode, PopulateHint, PopulatePath, QueryFlag, ReferenceKind, SCALAR_TYPES, } from './enums.js'; import { EventManager } from './events/EventManager.js'; import { TransactionEventBroadcaster } from './events/TransactionEventBroadcaster.js'; import { OptimisticLockError, ValidationError } from './errors.js'; import { applyPopulateHints, getLoadingStrategy } from './entity/utils.js'; import { TransactionManager } from './utils/TransactionManager.js'; /** * The EntityManager is the central access point to ORM functionality. It is a facade to all different ORM subsystems * such as UnitOfWork, Query Language, and Repository API. * @template {IDatabaseDriver} Driver current driver type */ export class EntityManager { config; driver; metadata; eventManager; static #counter = 1; /** @internal */ _id = EntityManager.#counter++; /** Whether this is the global (root) EntityManager instance. */ global = false; /** The context name of this EntityManager, derived from the ORM configuration. */ name; #loaders = {}; #repositoryMap = new Map(); #entityLoader; #comparator; #entityFactory; #unitOfWork; #resultCache; #filters = {}; #filterParams = {}; loggerContext; #transactionContext; #disableTransactions; #flushMode; #schema; #useContext; /** * @internal */ constructor(config, driver, metadata, useContext = true, eventManager = new EventManager(config.get('subscribers'))) { this.config = config; this.driver = driver; this.metadata = metadata; this.eventManager = eventManager; this.#useContext = useContext; this.#entityLoader = new EntityLoader(this); this.name = this.config.get('contextName'); this.#comparator = this.config.getComparator(this.metadata); this.#resultCache = this.config.getResultCacheAdapter(); this.#disableTransactions = this.config.get('disableTransactions'); this.#entityFactory = new EntityFactory(this); this.#unitOfWork = new UnitOfWork(this); } /** * Gets the Driver instance used by this EntityManager. * Driver is singleton, for one MikroORM instance, only one driver is created. */ getDriver() { return this.driver; } /** * Gets the Connection instance, by default returns write connection */ getConnection(type) { return this.driver.getConnection(type); } /** * Gets the platform instance. Just like the driver, platform is singleton, one for a MikroORM instance. */ getPlatform() { return this.driver.getPlatform(); } /** * Gets repository for given entity. You can pass either string name or entity class reference. */ getRepository(entityName) { const meta = this.metadata.get(entityName); if (!this.#repositoryMap.has(meta)) { const RepositoryClass = this.config.getRepositoryClass(meta.repository); this.#repositoryMap.set(meta, new RepositoryClass(this, entityName)); } return this.#repositoryMap.get(meta); } /** * Shortcut for `em.getRepository()`. */ repo(entityName) { return this.getRepository(entityName); } /** * Finds all entities matching your `where` query. You can pass additional options via the `options` parameter. */ async find(entityName, where, options = {}) { if (options.disableIdentityMap ?? this.config.get('disableIdentityMap')) { const em = this.getContext(false); const fork = em.fork({ keepTransactionContext: true }); const ret = await fork.find(entityName, where, { ...options, disableIdentityMap: false }); fork.clear(); return ret; } const em = this.getContext(); em.prepareOptions(options); await em.tryFlush(entityName, options); where = await em.processWhere(entityName, where, options, 'read'); validateParams(where); const meta = this.metadata.get(entityName); if (meta.orderBy) { options.orderBy = QueryHelper.mergeOrderBy(options.orderBy, meta.orderBy); } else { options.orderBy ??= {}; } options.populate = await em.preparePopulate(entityName, options); const populate = options.populate; const cacheKey = em.cacheKey(entityName, options, 'em.find', where); const cached = await em.tryCache(entityName, options.cache, cacheKey, options.refresh, true); if (cached?.data) { await em.#entityLoader.populate(entityName, cached.data, populate, { ...options, ...em.getPopulateWhere(where, options), ignoreLazyScalarProperties: true, lookup: false, }); return cached.data; } options = { ...options }; // save the original hint value so we know it was infer/all options._populateWhere = options.populateWhere ?? this.config.get('populateWhere'); options.populateWhere = this.createPopulateWhere({ ...where }, options); options.populateFilter = await this.getJoinedFilters(meta, options); await em.processUnionWhere(entityName, options, 'read'); const results = await em.driver.find(entityName, where, { ctx: em.#transactionContext, em, ...options }); if (results.length === 0) { await em.storeCache(options.cache, cached, []); return []; } const ret = []; for (const data of results) { const entity = em.#entityFactory.create(entityName, data, { merge: true, refresh: options.refresh, schema: options.schema, convertCustomTypes: true, }); ret.push(entity); } const unique = Utils.unique(ret); await em.#entityLoader.populate(entityName, unique, populate, { ...options, ...em.getPopulateWhere(where, options), ignoreLazyScalarProperties: true, lookup: false, }); await em.#unitOfWork.dispatchOnLoadEvent(); if (meta.virtual) { await em.storeCache(options.cache, cached, () => ret); } else { await em.storeCache(options.cache, cached, () => unique.map(e => helper(e).toPOJO())); } return unique; } /** * Finds all entities and returns an async iterable (async generator) that yields results one by one. * The results are merged and mapped to entity instances, without adding them to the identity map. * You can disable merging by passing the options `{ mergeResults: false }`. * With `mergeResults` disabled, to-many collections will contain at most one item, and you will get duplicate * root entities when there are multiple items in the populated collection. * This is useful for processing large datasets without loading everything into memory at once. * * ```ts * const stream = em.stream(Book, { populate: ['author'] }); * * for await (const book of stream) { * // book is an instance of Book entity * console.log(book.title, book.author.name); * } * ``` */ async *stream(entityName, options = {}) { const em = this.getContext(); em.prepareOptions(options); options.strategy = 'joined'; await em.tryFlush(entityName, options); const where = await em.processWhere(entityName, options.where ?? {}, options, 'read'); validateParams(where); options.orderBy = options.orderBy || {}; options.populate = await em.preparePopulate(entityName, options); const meta = this.metadata.get(entityName); options = { ...options }; // save the original hint value so we know it was infer/all options._populateWhere = options.populateWhere ?? this.config.get('populateWhere'); options.populateWhere = this.createPopulateWhere({ ...where }, options); options.populateFilter = await this.getJoinedFilters(meta, options); const stream = em.driver.stream(entityName, where, { ctx: em.#transactionContext, mapResults: false, ...options, }); for await (const data of stream) { const fork = em.fork(); const entity = fork.#entityFactory.create(entityName, data, { refresh: options.refresh, schema: options.schema, convertCustomTypes: true, }); helper(entity).setSerializationContext({ populate: options.populate, fields: options.fields, exclude: options.exclude, }); await fork.#unitOfWork.dispatchOnLoadEvent(); fork.clear(); yield entity; } } /** * Finds all entities of given type, optionally matching the `where` condition provided in the `options` parameter. */ async findAll(entityName, options) { return this.find(entityName, options?.where ?? {}, options); } getPopulateWhere(where, options) { if (options.populateWhere === undefined) { options.populateWhere = this.config.get('populateWhere'); } if (options.populateWhere === PopulateHint.ALL) { return { where: {}, populateWhere: options.populateWhere }; } /* v8 ignore next */ if (options.populateWhere === PopulateHint.INFER) { return { where, populateWhere: options.populateWhere }; } return { where: options.populateWhere }; } /** * Registers global filter to this entity manager. Global filters are enabled by default (unless disabled via last parameter). */ addFilter(options) { if (options.entity) { options.entity = Utils.asArray(options.entity).map(n => Utils.className(n)); } options.default ??= true; this.getContext(false).#filters[options.name] = options; } /** * Sets filter parameter values globally inside context defined by this entity manager. * If you want to set shared value for all contexts, be sure to use the root entity manager. */ setFilterParams(name, args) { this.getContext().#filterParams[name] = args; } /** * Returns filter parameters for given filter set in this context. */ getFilterParams(name) { return this.getContext().#filterParams[name]; } /** * Sets logger context for this entity manager. */ setLoggerContext(context) { this.getContext().loggerContext = context; } /** * Gets logger context for this entity manager. */ getLoggerContext(options) { const em = options?.disableContextResolution ? this : this.getContext(); em.loggerContext ??= {}; return em.loggerContext; } /** Sets the flush mode for this EntityManager. Pass `undefined` to reset to the global default. */ setFlushMode(flushMode) { this.getContext(false).#flushMode = flushMode; } async processWhere(entityName, where, options, type) { where = QueryHelper.processWhere({ where, entityName, metadata: this.metadata, platform: this.driver.getPlatform(), convertCustomTypes: options.convertCustomTypes, aliased: type === 'read', }); where = await this.applyFilters(entityName, where, options.filters ?? {}, type, options); where = this.applyDiscriminatorCondition(entityName, where); return where; } async processUnionWhere(entityName, options, type) { if (options.unionWhere?.length) { if (!this.driver.getPlatform().supportsUnionWhere()) { throw new Error(`unionWhere is only supported on SQL drivers`); } options.unionWhere = await Promise.all( options.unionWhere.map(branch => this.processWhere(entityName, branch, options, type)), ); } } // this method only handles the problem for mongo driver, SQL drivers have their implementation inside QueryBuilder applyDiscriminatorCondition(entityName, where) { const meta = this.metadata.find(entityName); if (meta?.root.inheritanceType !== 'sti' || !meta?.discriminatorValue) { return where; } const types = Object.values(meta.root.discriminatorMap).map(cls => this.metadata.get(cls)); const children = []; const lookUpChildren = (ret, type) => { const children = types.filter(meta2 => meta2.extends === type); children.forEach(m => lookUpChildren(ret, m.class)); ret.push(...children.filter(c => c.discriminatorValue)); return children; }; lookUpChildren(children, meta.class); /* v8 ignore next */ where[meta.root.discriminatorColumn] = children.length > 0 ? { $in: [meta.discriminatorValue, ...children.map(c => c.discriminatorValue)] } : meta.discriminatorValue; return where; } createPopulateWhere(cond, options) { const ret = {}; const populateWhere = options.populateWhere ?? this.config.get('populateWhere'); if (populateWhere === PopulateHint.INFER) { Utils.merge(ret, cond); } else if (typeof populateWhere === 'object') { Utils.merge(ret, populateWhere); } return ret; } async getJoinedFilters(meta, options) { // If user provided populateFilter, merge it with computed filters const userFilter = options.populateFilter; if (!this.config.get('filtersOnRelations') || !options.populate) { return userFilter; } const ret = {}; for (const hint of options.populate) { const field = hint.field.split(':')[0]; const prop = meta.properties[field]; const strategy = getLoadingStrategy( prop.strategy || hint.strategy || options.strategy || this.config.get('loadStrategy'), prop.kind, ); const joined = strategy === LoadStrategy.JOINED && prop.kind !== ReferenceKind.SCALAR; if (!joined && !hint.filter) { continue; } const filters = QueryHelper.mergePropertyFilters(prop.filters, options.filters); const where = await this.applyFilters(prop.targetMeta.class, {}, filters, 'read', { ...options, populate: hint.children, }); const where2 = await this.getJoinedFilters(prop.targetMeta, { ...options, filters, populate: hint.children, populateWhere: PopulateHint.ALL, }); if (Utils.hasObjectKeys(where)) { ret[field] = ret[field] ? { $and: [where, ret[field]] } : where; } if (where2 && Utils.hasObjectKeys(where2)) { if (ret[field]) { Utils.merge(ret[field], where2); } else { ret[field] = where2; } } } // Merge user-provided populateFilter with computed filters if (userFilter) { Utils.merge(ret, userFilter); } return Utils.hasObjectKeys(ret) ? ret : undefined; } /** * When filters are active on M:1 or 1:1 relations, we need to ref join them eagerly as they might affect the FK value. */ async autoJoinRefsForFilters(meta, options, parent) { if (!meta || !this.config.get('autoJoinRefsForFilters') || options.filters === false) { return; } const ret = options.populate; for (const prop of meta.relations) { if ( prop.object || ![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) || !( (options.fields?.length ?? 0) === 0 || options.fields?.some(f => prop.name === f || prop.name.startsWith(`${String(f)}.`)) ) || (parent?.class === prop.targetMeta.root.class && parent.propName === prop.inversedBy) ) { continue; } options = { ...options, filters: QueryHelper.mergePropertyFilters(prop.filters, options.filters) }; const cond = await this.applyFilters(prop.targetMeta.class, {}, options.filters, 'read', options); if (!Utils.isEmpty(cond)) { const populated = options.populate.filter(({ field }) => field.split(':')[0] === prop.name); let found = false; for (const hint of populated) { if (!hint.all) { hint.filter = true; } const strategy = getLoadingStrategy( prop.strategy || hint.strategy || options.strategy || this.config.get('loadStrategy'), prop.kind, ); if (hint.field === `${prop.name}:ref` || (hint.filter && strategy === LoadStrategy.JOINED)) { found = true; } } if (!found) { ret.push({ field: `${prop.name}:ref`, strategy: LoadStrategy.JOINED, filter: true }); } } } for (const hint of ret) { const [field, ref] = hint.field.split(':'); const prop = meta?.properties[field]; if (prop && !ref) { hint.children ??= []; await this.autoJoinRefsForFilters( prop.targetMeta, { ...options, populate: hint.children }, { class: meta.root.class, propName: prop.name }, ); } } } /** * @internal */ async applyFilters(entityName, where, options, type, findOptions) { const meta = this.metadata.get(entityName); const filters = []; const ret = []; const active = new Set(); const push = source => { const activeFilters = QueryHelper.getActiveFilters(meta, options, source).filter(f => !active.has(f.name)); filters.push(...activeFilters); activeFilters.forEach(f => active.add(f.name)); }; push(this.config.get('filters')); push(this.#filters); push(meta.filters); if (filters.length === 0) { return where; } for (const filter of filters) { let cond; if (filter.cond instanceof Function) { // @ts-ignore // oxfmt-ignore const args = Utils.isPlainObject(options?.[filter.name]) ? options[filter.name] : this.getContext().#filterParams[filter.name]; if (!args && filter.cond.length > 0 && filter.args !== false) { throw new Error(`No arguments provided for filter '${filter.name}'`); } cond = await filter.cond(args, type, this, findOptions, Utils.className(entityName)); } else { cond = filter.cond; } cond = QueryHelper.processWhere({ where: cond, entityName, metadata: this.metadata, platform: this.driver.getPlatform(), aliased: type === 'read', }); if (filter.strict) { Object.defineProperty(cond, '__strict', { value: filter.strict, enumerable: false }); } ret.push(cond); } const conds = [...ret, where].filter(c => Utils.hasObjectKeys(c) || Raw.hasObjectFragments(c)); return conds.length > 1 ? { $and: conds } : conds[0]; } /** * Calls `em.find()` and `em.count()` with the same arguments (where applicable) and returns the results as tuple * where the first element is the array of entities, and the second is the count. */ async findAndCount(entityName, where, options = {}) { const em = this.getContext(false); await em.tryFlush(entityName, options); options.flushMode = 'commit'; // do not try to auto flush again return Promise.all([em.find(entityName, where, options), em.count(entityName, where, options)]); } /** * Calls `em.find()` and `em.count()` with the same arguments (where applicable) and returns the results as {@apilink Cursor} object. * Supports `before`, `after`, `first` and `last` options while disallowing `limit` and `offset`. Explicit `orderBy` option * is required. * * Use `first` and `after` for forward pagination, or `last` and `before` for backward pagination. * * - `first` and `last` are numbers and serve as an alternative to `offset`, those options are mutually exclusive, use only one at a time * - `before` and `after` specify the previous cursor value, it can be one of the: * - `Cursor` instance * - opaque string provided by `startCursor/endCursor` properties * - POJO/entity instance * * ```ts * const currentCursor = await em.findByCursor(User, { * first: 10, * after: previousCursor, // cursor instance * orderBy: { id: 'desc' }, * }); * * // to fetch next page * const nextCursor = await em.findByCursor(User, { * first: 10, * after: currentCursor.endCursor, // opaque string * orderBy: { id: 'desc' }, * }); * * // to fetch next page * const nextCursor2 = await em.findByCursor(User, { * first: 10, * after: { id: lastSeenId }, // entity-like POJO * orderBy: { id: 'desc' }, * }); * ``` * * The options also support an `includeCount` (true by default) option. If set to false, the `totalCount` is not * returned as part of the cursor. This is useful for performance reason, when you don't care about the total number * of pages. * * The `Cursor` object provides the following interface: * * ```ts * Cursor { * items: [ * User { ... }, * User { ... }, * User { ... }, * ], * totalCount: 50, // not included if `includeCount: false` * startCursor: 'WzRd', * endCursor: 'WzZd', * hasPrevPage: true, * hasNextPage: true, * } * ``` */ async findByCursor(entityName, options) { const em = this.getContext(false); options.overfetch ??= true; options.where ??= {}; if (Utils.isEmpty(options.orderBy) && !Raw.hasObjectFragments(options.orderBy)) { throw new Error('Explicit `orderBy` option required'); } const [entities, count] = options.includeCount !== false ? await em.findAndCount(entityName, options.where, options) : [await em.find(entityName, options.where, options)]; return new Cursor(entities, count, options, this.metadata.get(entityName)); } /** * Refreshes the persistent state of an entity from the database, overriding any local changes that have not yet been * persisted. Returns the same entity instance (same object reference), but re-hydrated. If the entity is no longer * in database, the method throws an error just like `em.findOneOrFail()` (and respects the same config options). */ async refreshOrFail(entity, options = {}) { const ret = await this.refresh(entity, options); if (!ret) { options.failHandler ??= this.config.get('findOneOrFailHandler'); const wrapped = helper(entity); const where = wrapped.getPrimaryKey(); throw options.failHandler(wrapped.__meta.className, where); } return ret; } /** * Refreshes the persistent state of an entity from the database, overriding any local changes that have not yet been * persisted. Returns the same entity instance (same object reference), but re-hydrated. If the entity is no longer * in database, the method returns `null`. */ async refresh(entity, options = {}) { const fork = this.fork({ keepTransactionContext: true }); const wrapped = helper(entity); const reloaded = await fork.findOne(wrapped.__meta.class, entity, { schema: wrapped.__schema, ...options, flushMode: FlushMode.COMMIT, }); const em = this.getContext(); if (!reloaded) { em.#unitOfWork.unsetIdentity(entity); return null; } let found = false; for (const e of fork.#unitOfWork.getIdentityMap()) { const ref = em.getReference(e.constructor, helper(e).getPrimaryKey()); const data = helper(e).serialize({ ignoreSerializers: true, includeHidden: true, convertCustomTypes: false }); em.config .getHydrator(this.metadata) .hydrate(ref, helper(ref).__meta, data, em.#entityFactory, 'full', false, false); Utils.merge(helper(ref).__originalEntityData, this.#comparator.prepareEntity(e)); found ||= ref === entity; } if (!found) { const data = helper(reloaded).serialize({ ignoreSerializers: true, includeHidden: true, convertCustomTypes: true, }); em.config .getHydrator(this.metadata) .hydrate(entity, wrapped.__meta, data, em.#entityFactory, 'full', false, true); Utils.merge(wrapped.__originalEntityData, this.#comparator.prepareEntity(reloaded)); } return entity; } /** * Finds first entity matching your `where` query. */ async findOne(entityName, where, options = {}) { if (options.disableIdentityMap ?? this.config.get('disableIdentityMap')) { const em = this.getContext(false); const fork = em.fork({ keepTransactionContext: true }); const ret = await fork.findOne(entityName, where, { ...options, disableIdentityMap: false }); fork.clear(); return ret; } const em = this.getContext(); em.prepareOptions(options); let entity = em.#unitOfWork.tryGetById(entityName, where, options.schema); // query for a not managed entity which is already in the identity map as it // was provided with a PK this entity does not exist in the db, there can't // be any relations to it, so no need to deal with the populate hint if (entity && !helper(entity).__managed) { return entity; } await em.tryFlush(entityName, options); const meta = em.metadata.get(entityName); where = await em.processWhere(entityName, where, options, 'read'); validateEmptyWhere(where); em.checkLockRequirements(options.lockMode, meta); const isOptimisticLocking = options.lockMode == null || options.lockMode === LockMode.OPTIMISTIC; if (entity && !em.shouldRefresh(meta, entity, options) && isOptimisticLocking) { return em.lockAndPopulate(meta, entity, where, options); } validateParams(where); options.populate = await em.preparePopulate(entityName, options); const cacheKey = em.cacheKey(entityName, options, 'em.findOne', where); const cached = await em.tryCache(entityName, options.cache, cacheKey, options.refresh, true); if (cached?.data !== undefined) { if (cached.data) { await em.#entityLoader.populate(entityName, [cached.data], options.populate, { ...options, ...em.getPopulateWhere(where, options), ignoreLazyScalarProperties: true, lookup: false, }); } return cached.data; } options = { ...options }; // save the original hint value so we know it was infer/all options._populateWhere = options.populateWhere ?? this.config.get('populateWhere'); options.populateWhere = this.createPopulateWhere({ ...where }, options); options.populateFilter = await this.getJoinedFilters(meta, options); await em.processUnionWhere(entityName, options, 'read'); const data = await em.driver.findOne(entityName, where, { ctx: em.#transactionContext, em, ...options, }); if (!data) { await em.storeCache(options.cache, cached, null); return null; } entity = em.#entityFactory.create(entityName, data, { merge: true, refresh: options.refresh, schema: options.schema, convertCustomTypes: true, }); await em.lockAndPopulate(meta, entity, where, options); await em.#unitOfWork.dispatchOnLoadEvent(); await em.storeCache(options.cache, cached, () => helper(entity).toPOJO()); return entity; } /** * Finds first entity matching your `where` query. If nothing found, it will throw an error. * If the `strict` option is specified and nothing is found or more than one matching entity is found, it will throw an error. * You can override the factory for creating this method via `options.failHandler` locally * or via `Configuration.findOneOrFailHandler` (`findExactlyOneOrFailHandler` when specifying `strict`) globally. */ async findOneOrFail(entityName, where, options = {}) { let entity; let isStrictViolation = false; if (options.strict) { const ret = await this.find(entityName, where, { ...options, limit: 2 }); isStrictViolation = ret.length !== 1; entity = ret[0]; } else { entity = await this.findOne(entityName, where, options); } if (!entity || isStrictViolation) { const key = options.strict ? 'findExactlyOneOrFailHandler' : 'findOneOrFailHandler'; options.failHandler ??= this.config.get(key); const name = Utils.className(entityName); /* v8 ignore next */ where = Utils.isEntity(where) ? helper(where).getPrimaryKey() : where; throw options.failHandler(name, where); } return entity; } /** * Creates or updates the entity, based on whether it is already present in the database. * This method performs an `insert on conflict merge` query ensuring the database is in sync, returning a managed * entity instance. The method accepts either `entityName` together with the entity `data`, or just entity instance. * * ```ts * // insert into "author" ("age", "email") values (33, 'foo@bar.com') on conflict ("email") do update set "age" = 41 * const author = await em.upsert(Author, { email: 'foo@bar.com', age: 33 }); * ``` * * The entity data needs to contain either the primary key, or any other unique property. Let's consider the following example, where `Author.email` is a unique property: * * ```ts * // insert into "author" ("age", "email") values (33, 'foo@bar.com') on conflict ("email") do update set "age" = 41 * // select "id" from "author" where "email" = 'foo@bar.com' * const author = await em.upsert(Author, { email: 'foo@bar.com', age: 33 }); * ``` * * Depending on the driver support, this will either use a returning query, or a separate select query, to fetch the primary key if it's missing from the `data`. * * If the entity is already present in current context, there won't be any queries - instead, the entity data will be assigned and an explicit `flush` will be required for those changes to be persisted. */ async upsert(entityNameOrEntity, data, options = {}) { if (options.disableIdentityMap ?? this.config.get('disableIdentityMap')) { const em = this.getContext(false); const fork = em.fork({ keepTransactionContext: true }); const ret = await fork.upsert(entityNameOrEntity, data, { ...options, disableIdentityMap: false }); fork.clear(); return ret; } const em = this.getContext(false); em.prepareOptions(options); let entityName; let where; let entity = null; if (data === undefined) { entityName = entityNameOrEntity.constructor; data = entityNameOrEntity; } else { entityName = entityNameOrEntity; } const meta = this.metadata.get(entityName); const convertCustomTypes = !Utils.isEntity(data); if (Utils.isEntity(data)) { entity = data; if (helper(entity).__managed && helper(entity).__em === em && !this.config.get('upsertManaged')) { em.#entityFactory.mergeData(meta, entity, data, { initialized: true }); return entity; } where = helper(entity).getPrimaryKey(); data = em.#comparator.prepareEntity(entity); } else { data = Utils.copy(QueryHelper.processParams(data)); where = Utils.extractPK(data, meta); if (where && !this.config.get('upsertManaged')) { const exists = em.#unitOfWork.getById(entityName, where, options.schema); if (exists) { return em.assign(exists, data); } } } where = getWhereCondition(meta, options.onConflictFields, data, where).where; data = QueryHelper.processObjectParams(data); validateParams(data, 'insert data'); if (em.eventManager.hasListeners(EventType.beforeUpsert, meta)) { await em.eventManager.dispatchEvent(EventType.beforeUpsert, { entity: data, em, meta }, meta); } const ret = await em.driver.nativeUpdate(entityName, where, data, { ctx: em.#transactionContext, upsert: true, convertCustomTypes, ...options, }); em.#unitOfWork.getChangeSetPersister().mapReturnedValues(entity, data, ret.row, meta, true); entity ??= em.#entityFactory.create(entityName, data, { refresh: true, initialized: true, schema: options.schema, }); const uniqueFields = options.onConflictFields ?? (Utils.isPlainObject(where) ? Object.keys(where) : meta.primaryKeys); const returning = getOnConflictReturningFields(meta, data, uniqueFields, options); if ( options.onConflictAction === 'ignore' || !helper(entity).hasPrimaryKey() || (returning.length > 0 && !(this.getPlatform().usesReturningStatement() && ret.row)) ) { const where = {}; if (Array.isArray(uniqueFields)) { for (const prop of uniqueFields) { if (data[prop] != null) { where[prop] = data[prop]; } else if (meta.primaryKeys.includes(prop) && ret.insertId != null) { where[prop] = ret.insertId; } } } else { Object.keys(data).forEach(prop => { where[prop] = data[prop]; }); if (meta.simplePK && ret.insertId != null) { where[meta.primaryKeys[0]] = ret.insertId; } } const data2 = await this.driver.findOne(meta.class, where, { fields: returning, ctx: em.#transactionContext, convertCustomTypes: true, connectionType: 'write', schema: options.schema, }); em.getHydrator().hydrate(entity, meta, data2, em.#entityFactory, 'full', false, true); } // recompute the data as there might be some values missing (e.g. those with db column defaults) const snapshot = this.#comparator.prepareEntity(entity); em.#unitOfWork.register(entity, snapshot, { refresh: true }); if (em.eventManager.hasListeners(EventType.afterUpsert, meta)) { await em.eventManager.dispatchEvent(EventType.afterUpsert, { entity, em, meta }, meta); } return entity; } /** * Creates or updates the entity, based on whether it is already present in the database. * This method performs an `insert on conflict merge` query ensuring the database is in sync, returning a managed * entity instance. The method accepts either `entityName` together with the entity `data`, or just entity instance. * * ```ts * // insert into "author" ("age", "email") values (33, 'foo@bar.com') on conflict ("email") do update set "age" = 41 * const authors = await em.upsertMany(Author, [{ email: 'foo@bar.com', age: 33 }, ...]); * ``` * * The entity data needs to contain either the primary key, or any other unique property. Let's consider the following example, where `Author.email` is a unique property: * * ```ts * // insert into "author" ("age", "email") values (33, 'foo@bar.com'), (666, 'lol@lol.lol') on conflict ("email") do update set "age" = excluded."age" * // select "id" from "author" where "email" = 'foo@bar.com' * const author = await em.upsertMany(Author, [ * { email: 'foo@bar.com', age: 33 }, * { email: 'lol@lol.lol', age: 666 }, * ]); * ``` * * Depending on the driver support, this will either use a returning query, or a separate select query, to fetch the primary key if it's missing from the `data`. * * If the entity is already present in current context, there won't be any queries - instead, the entity data will be assigned and an explicit `flush` will be required for those changes to be persisted. */ async upsertMany(entityNameOrEntity, data, options = {}) { if (options.disableIdentityMap ?? this.config.get('disableIdentityMap')) { const em = this.getContext(false); const fork = em.fork({ keepTransactionContext: true }); const ret = await fork.upsertMany(entityNameOrEntity, data, { ...options, disableIdentityMap: false }); fork.clear(); return ret; } const em = this.getContext(false); em.prepareOptions(options); let entityName; let propIndex; if (data === undefined) { entityName = entityNameOrEntity[0].constructor; data = entityNameOrEntity; } else { entityName = entityNameOrEntity; } const batchSize = options.batchSize ?? this.config.get('batchSize'); if (data.length > batchSize) { const ret = []; for (let i = 0; i < data.length; i += batchSize) { const chunk = data.slice(i, i + batchSize); ret.push(...(await this.upsertMany(entityName, chunk, options))); } return ret; } const meta = this.metadata.get(entityName); const convertCustomTypes = !Utils.isEntity(data[0]); const allData = []; const allWhere = []; const entities = new Map(); const entitiesByData = new Map(); for (let i = 0; i < data.length; i++) { let row = data[i]; let where; if (Utils.isEntity(row)) { const entity = row; if (helper(entity).__managed && helper(entity).__em === em && !this.config.get('upsertManaged')) { em.#entityFactory.mergeData(meta, entity, row, { initialized: true }); entities.set(entity, row); entitiesByData.set(row, entity); continue; } where = helper(entity).getPrimaryKey(); row = em.#comparator.prepareEntity(entity); } else { row = data[i] = Utils.copy(QueryHelper.processParams(row)); where = Utils.extractPK(row, meta); if (where && !this.config.get('upsertManaged')) { const exists = em.#unitOfWork.getById(entityName, where, options.schema); if (exists) { em.assign(exists, row); entities.set(exists, row); entitiesByData.set(row, exists); continue; } } } const unique = options.onConflictFields ?? meta.props.filter(p => p.unique).map(p => p.name); propIndex = !isRaw(unique) && unique.findIndex(p => data[p] ?? data[p.substring(0, p.indexOf('.'))] != null); const tmp = getWhereCondition(meta, options.onConflictFields, row, where); propIndex = tmp.propIndex; where = QueryHelper.processWhere({ where: tmp.where, entityName, metadata: this.metadata, platform: this.getPlatform(), }); row = QueryHelper.processObjectParams(row); validateParams(row, 'insert data'); allData.push(row); allWhere.push(where); } if (entities.size === data.length) { return [...entities.keys()]; } if (em.eventManager.hasListeners(EventType.beforeUpsert, meta)) { for (const dto of data) { const entity = entitiesByData.get(dto) ?? dto; await em.eventManager.dispatchEvent(EventType.beforeUpsert, { entity, em, meta }, meta); } } const res = await em.driver.nativeUpdateMany(entityName, allWhere, allData, { ctx: em.#transactionContext, upsert: true, convertCustomTypes, ...options, }); entities.clear(); entitiesByData.clear(); const loadPK = new Map(); allData.forEach((row, i) => { em.#unitOfWork .getChangeSetPersister() .mapReturnedValues( Utils.isEntity(data[i]) ? data[i] : null, Utils.isEntity(data[i]) ? {} : data[i], res.rows?.[i], meta, true, ); const entity = Utils.isEntity(data[i]) ? data[i] : em.#entityFactory.create(entityName, row, { refresh: true, initialized: true, schema: options.schema, }); if (!helper(entity).hasPrimaryKey()) { loadPK.set(entity, allWhere[i]); } entities.set(entity, row); entitiesByData.set(row, entity); }); // skip if we got the PKs via returning statement (`rows`) // oxfmt-ignore const uniqueFields = options.onConflictFields ?? (Utils.isPlainObject(allWhere[0]) ? Object.keys(allWhere[0]).flatMap(key => Utils.splitPrimaryKeys(key)) : meta.primaryKeys); const returning = getOnConflictReturningFields(meta, data[0], uniqueFields, options); const reloadFields = returning.length > 0 && !(this.getPlatform().usesReturningStatement() && res.rows?.length); if (options.onConflictAction === 'ignore' || (!res.rows?.length && loadPK.size > 0) || reloadFields) { const unique = meta.hydrateProps.filter(p => !p.lazy).map(p => p.name); const add = new Set(propIndex !== false && propIndex >= 0 ? [unique[propIndex]] : []); for (const cond of loadPK.values()) { Utils.keys(cond).forEach(key => add.add(key)); } const where = { $or: [] }; data.forEach((item, idx) => { where.$or[idx] = {}; const props = Array.isArray(uniqueFields) ? uniqueFields : Object.keys(item); props.forEach(prop => { where.$or[idx][prop] = item[prop]; }); }); const data2 = await this.driver.find(meta.class, where, { fields: returning.concat(...add).concat(...(Array.isArray(uniqueFields) ? uniqueFields : [])), ctx: em.#transactionContext, convertCustomTypes: true, connectionType: 'write', schema: options.schema, }); for (const [entity, cond] of loadPK.entries()) { const row = data2.find(row => { const tmp = {}; add.forEach(k => { if (!meta.properties[k]?.primary) { tmp[k] = row[k]; } }); return this.#comparator.matching(entityName, cond, tmp); }); /* v8 ignore next */ if (!row) { throw new Error(`Cannot find matching entity for condition ${JSON.stringify(cond)}`); } em.getHydrator().hydrate(entity, meta, row, em.#entityFactory, 'full', false, true); } if (loadPK.size !== data2.length && Array.isArray(uniqueFields)) { for (let i = 0; i < allData.length; i++) { const data = allData[i]; const cond = uniqueFields.reduce((a, b) => { // @ts-ignore a[b] = data[b]; return a; }, {}); const entity = entitiesByData.get(data); const row = data2.find(item => { const pk = uniqueFields.reduce((a, b) => { // @ts-ignore a[b] = item[b]; return a; }, {}); return this.#comparator.matching(entityName, cond, pk); }); /* v8 ignore next */ if (!row) { throw new Error(`Cannot find matching entity for condition ${JSON.stringify(cond)}`); } em.getHydrator().hydrate(entity, meta, row, em.#entityFactory, 'full'); } } } for (const [entity] of entities) { // recompute the data as there might be some values missing (e.g. those with db column defaults) const snapshot = this.#comparator.prepareEntity(entity); em.#unitOfWork.register(entity, snapshot, { refresh: true }); } if (em.eventManager.hasListeners(EventType.afterUpsert, meta)) { for (const [entity] of entities) { await em.eventManager.dispatchEvent(EventType.afterUpsert, { entity, em, meta }, meta); } } return [...entities.keys()]; } /** * Runs your callback wrapped inside a database transaction. * * If a transaction is already active, a new savepoint (nested transaction) will be created by default. This behavior * can be controlled via the `propagation` option. Use the provided EntityManager instance for all operations that * should be part of the transaction. You can safely use a global EntityManager instance from a DI container, as this * method automatically creates an async context for the transaction. * * **Concurrency note:** When running multiple transactions concurrently (e.g. in parallel requests or jobs), use the * `clear: true` option. This ensures the callback runs in a clear fork of the EntityManager, providing full isolation * between concurrent transactional handlers. Using `clear: true` is an alternative to forking explicitly and calling * the method on the new fork – it already provides the necessary isolation for safe concurrent usage. * * **Propagation note:** Changes made within a transaction (whether top-level or nested) are always propagated to the * parent context, unless the parent context is a global one. If you want to avoid that, fork the EntityManager first * and then call this method on the fork. * * **Example:** * ```ts * await em.transactional(async (em) => { * const author = new Author('Jon'); * em.persist(author); * // flush is called automatically at the end of the callback * }); * ``` */ async transactional(cb, options = {}) { const em = this.getContext(false); if (this.#disableTransactions || em.#disableTransactions) { return cb(em); } const manager = new TransactionManager(this); return manager.handle(cb, options); } /** * Starts new transaction bound to this EntityManager. Use `ctx` parameter to provide the parent when nesting transactions. */ async begin(options = {}) { if (this.#disableTransactions) { return; } const em = this.getContext(false); em.#transactionContext = await em.getConnection('write').begin({ ...options, eventBroadcaster: new TransactionEventBroadcaster(em, { topLevelTransaction: !options.ctx }), }); } /** * Commits the transaction bound to this EntityManager. Flushes before doing the actual commit query. */ async commit() { const em = this.getContext(false); if (this.#disableTransactions) { await em.flush(); return; } if (!em.#transactionContext) { throw ValidationError.transactionRequired(); } await em.flush(); await em.getConnection('write').commit(em.#transactionContext, new TransactionEventBroadcaster(em)); em.#transactionContext = undefined; } /** * Rollbacks the transaction bound to this EntityManager. */ async rollback() { if (this.#disableTransactions) { return; } const em = this.getContext(false); if (!em.#transactionContext) { throw ValidationError.transactionRequired(); } await em.getConnection('write').rollback(em.#transactionContext, new TransactionEventBroadcaster(em)); em.#transactionContext = undefined; em.#unitOfWork.clearActionsQueue(); } /** * Runs your callback wrapped inside a database transaction. */ async lock(entity, lockMode, options = {}) { options = Utils.isPlainObject(options) ? options : { lockVersion: options }; await this.getUnitOfWork().lock(entity, { lockMode, ...options }); } /** * Fires native insert query. Calling this has no side effects on the context (identity map). */ async insert(entityNameOrEntity, data, options = {}) { const em = this.getContext(false); em.prepareOptions(options); let entityName; if (data === undefined) { entityName = entityNameOrEntity.constructor; data = entityNameOrEntity; } else { entityName = entityNameOrEntity; } if (Utils.isEntity(data)) { if (options.schema && helper(data).getSchema() == null) { helper(data).setSchema(options.schema); } if (!helper(data).__managed) { // the entity might have been created via `em.create()`, which adds it to the persist stack automatically em.#unitOfWork.getPersistStack().delete(data); // it can be also in the identity map if it had a PK value already em.#unitOfWork.unsetIdentity(data); } const meta = helper(data).__meta; const payload = em.#comparator.prepareEntity(data); const cs = new ChangeSet(data, ChangeSetType.CREATE, payload, meta); await em.#unitOfWork.getChangeSetPersister().executeInserts([cs], { ctx: em.#transactionContext, ...options }); return cs.getPrimaryKey(); } data = QueryHelper.processObjectParams(data); validateParams(data, 'insert data'); const res = await em.driver.nativeInsert(entityName, data, { ctx: em.#transactionContext, ...options, }); return res.insertId; } /** * Fires native multi-insert query. Calling this has no side effects on the context (identity map). */ async insertMany(entityNameOrEntities, data, options = {}) { const em = this.getContext(false); em.prepareOptions(options); let entityName; if (data === undefined) { entityName = entityNameOrEntities[0].constructor; data = entityNameOrEntities; } else { entityName = entityNameOrEntities; } if (data.length === 0) { return []; } if (Utils.isEntity(data[0])) { const meta = helper(data[0]).__meta; const css = data.map(row => { if (options.schema && helper(row).getSchema() == null) { helper(row).setSchema(options.schema); } if (!helper(row).__managed) { // the entity might have been created via `em.create()`, which adds it to the persist stack automatically em.#unitOfWork.getPersistStack().delete(row); // it can be also in the identity map if it had a PK value already em.#unitOfWork.unsetIdentity(row); } const payload = em.#comparator.prepareEntity(row); return new ChangeSet(row, ChangeSetType.CREATE, payload, meta); }); await em.#unitOfWork.getChangeSetPersister().executeInserts(css, { ctx: em.#transactionContext, ...options }); return css.map(cs => cs.getPrimaryKey()); } data = data.map(row => QueryHelper.processObjectParams(row)); data.forEach(row => validateParams(row, 'insert data')); const res = await em.driver.nativeInsertMany(entityName, data, { ctx: em.#transactionContext, ...options, }); if (res.insertedIds) { return res.insertedIds; } return [res.insertId]; } /** * Fires native update query. Calling this has no side effects on the context (identity map). */ async nativeUpdate(entityName, where, data, options = {}) { const em = this.getContext(false); em.prepareOptions(options); await em.processUnionWhere(entityName, options, 'update'); data = QueryHelper.processObjectParams(data); where = await em.processWhere(entityName, where, { ...options, convertCustomTypes: false }, 'update'); validateParams(data, 'update data'); validateParams(where, 'update condition'); const res = await em.driver.nativeUpdate(entityName, where, data, { ctx: em.#transactionContext, em, ...options, }); return res.affectedRows; } /** * Fires native delete query. Calling this has no side effects on the context (identity map). */ async nativeDelete(entityName, where, options = {}) { const em = this.getContext(false); em.prepareOptions(options); await em.processUnionWhere(entityName, options, 'delete'); where = await em.processWhere(entityName, where, options, 'delete'); validateParams(where, 'delete condition'); const res = await em.driver.nativeDelete(entityName, where, { ctx: em.#transactionContext, em, ...options, }); return res.affectedRows; } /** * Maps raw database result to an entity and merges it to this EntityManager. */ map(entityName, result, options = {}) { const meta = this.metadata.get(entityName); const data = this.driver.mapResult(result, meta); for (const k of Object.keys(data)) { const prop = meta.properties[k]; if ( prop?.kind === ReferenceKind.SCALAR && SCALAR_TYPES.has(prop.runtimeType) && !prop.customType && (prop.setter || !prop.getter) ) { validateProperty(prop, data[k], data); } } return this.merge(entityName, data, { convertCustomTypes: true, refresh: true, validate: false, ...options, }); } /** * Merges given entity to this EntityManager so it becomes managed. You can force refreshing of existing entities * via second parameter. By default, it will return already loaded entities without modifying them. */ merge(entityName, data, options = {}) { if (Utils.isEntity(entityName)) { return this.merge(entityName.constructor, entityName, data); } const em = options.disableContextResolution ? this : this.getContext(); options.schema ??= em.#schema; options.validate ??= true; options.cascade ??= true; validatePrimaryKey(data, em.metadata.get(entityName)); let entity = em.#unitOfWork.tryGetById(entityName, data, options.schema, false); if (entity && helper(entity).__managed && helper(entity).__initialized && !options.refresh) { return entity; } const dataIsEntity = Utils.isEntity(data); entity = dataIsEntity ? data : em.#entityFactory.create(entityName, data, { merge: true, ...options }); const visited = options.cascade ? undefined : new Set([entity]); em.#unitOfWork.merge(entity, visited); return entity; } /** * Creates new instance of given entity and populates it with given data. * The entity constructor will be used unless you provide `{ managed: true }` in the `options` parameter. * The constructor will be given parameters based on the defined constructor of the entity. If the constructor * parameter matches a property name, its value will be extracted from `data`. If no matching property exists, * the whole `data` parameter will be passed. This means we can also define `constructor(data: Partial)` and * `em.create()` will pass the data into it (unless we have a property named `data` too). * * The parameters are strictly checked, you need to provide all required properties. You can use `OptionalProps` * symbol to omit some properties from this check without making them optional. Alternatively, use `partial: true` * in the options to disable the strict checks for required properties. This option has no effect on runtime. * * The newly created entity will be automatically marked for persistence via `em.persist` unless you disable this * behavior, either locally via `persist: false` option, or globally via `persistOnCreate` ORM config option. */ create(entityName, data, options = {}) { const em = this.getContext(); options.schema ??= em.#schema; const entity = em.#entityFactory.create(entityName, data, { ...options, newEntity: !options.managed, merge: options.managed, normalizeAccessors: true, }); options.persist ??= em.config.get('persistOnCreate'); if (options.persist && !this.getMetadata(entityName).embeddable) { em.persist(entity); } return entity; } /** * Shortcut for `wrap(entity).assign(data, { em })` */ assign(entity, data, options = {}) { return EntityAssigner.assign(entity, data, { em: this.getContext(), ...options }); } /** * Gets a reference to the entity identified by the given type and identifier without actually loading it, if the entity is not yet loaded */ getReference(entityName, id, options = {}) { options.schema ??= this.schema; options.convertCustomTypes ??= false; const meta = this.metadata.get(entityName); if (Utils.isPrimaryKey(id)) { if (meta.compositePK) { throw ValidationError.invalidCompositeIdentifier(meta); } id = [id]; } const entity = this.getEntityFactory().createReference(entityName, id, { merge: true, ...options }); if (options.wrapped) { return Reference.create(entity); } return entity; } /** * Returns total number of entities matching your `where` query. */ async count(entityName, where = {}, options = {}) { const em = this.getContext(false); // Shallow copy options since the object will be modified when deleting orderBy options = { ...options }; em.prepareOptions(options); await em.tryFlush(entityName, options); where = await em.processWhere(entityName, where, options, 'read'); options.populate = await em.preparePopulate(entityName, options); options = { ...options }; // save the original hint value so we know it was infer/all const meta = em.metadata.find(entityName); options._populateWhere = options.populateWhere ?? this.config.get('populateWhere'); options.populateWhere = this.createPopulateWhere({ ...where }, options); options.populateFilter = await this.getJoinedFilters(meta, options); validateParams(where); delete options.orderBy; await em.processUnionWhere(entityName, options, 'read'); const cacheKey = em.cacheKey(entityName, options, 'em.count', where); const cached = await em.tryCache(entityName, options.cache, cacheKey); if (cached?.data !== undefined) { return cached.data; } const count = await em.driver.count(entityName, where, { ctx: em.#transactionContext, em, ...options }); await em.storeCache(options.cache, cached, () => +count); return +count; } /** * Tells the EntityManager to make an instance managed and persistent. * The entity will be entered into the database at or before transaction commit or as a result of the flush operation. */ persist(entity) { const em = this.getContext(); if (Utils.isEntity(entity)) { // do not cascade just yet, cascading of entities in persist stack is done when flushing em.#unitOfWork.persist(entity, undefined, { cascade: false }); return em; } const entities = Utils.asArray(entity); for (const ent of entities) { if (!Utils.isEntity(ent, true)) { /* v8 ignore next */ const meta = typeof ent === 'object' ? em.metadata.find(ent.constructor) : undefined; throw ValidationError.notDiscoveredEntity(ent, meta); } // do not cascade just yet, cascading of entities in persist stack is done when flushing em.#unitOfWork.persist(Reference.unwrapReference(ent), undefined, { cascade: false }); } return this; } /** * Marks entity for removal. * A removed entity will be removed from the database at or before transaction commit or as a result of the flush operation. * * To remove entities by condition, use `em.nativeDelete()`. */ remove(entity) { const em = this.getContext(); if (Utils.isEntity(entity)) { // do not cascade just yet, cascading of entities in persist stack is done when flushing em.#unitOfWork.remove(entity, undefined, { cascade: false }); return em; } const entities = Utils.asArray(entity, true); for (const ent of entities) { if (!Utils.isEntity(ent, true)) { throw new Error( `You need to pass entity instance or reference to 'em.remove()'. To remove entities by condition, use 'em.nativeDelete()'.`, ); } // do not cascade just yet, cascading of entities in remove stack is done when flushing em.#unitOfWork.remove(Reference.unwrapReference(ent), undefined, { cascade: false }); } return em; } /** * Flushes all changes to objects that have been queued up to now to the database. * This effectively synchronizes the in-memory state of managed objects with the database. */ async flush() { await this.getUnitOfWork().commit(); } /** * @internal */ async tryFlush(entityName, options) { const em = this.getContext(); const flushMode = options.flushMode ?? em.#flushMode ?? em.config.get('flushMode'); const meta = em.metadata.get(entityName); if (flushMode === FlushMode.COMMIT) { return; } if (flushMode === FlushMode.ALWAYS || em.getUnitOfWork().shouldAutoFlush(meta)) { await em.flush(); } } /** * Clears the EntityManager. All entities that are currently managed by this EntityManager become detached. */ clear() { this.getContext().#unitOfWork.clear(); } /** * Checks whether given property can be populated on the entity. */ canPopulate(entityName, property) { // eslint-disable-next-line prefer-const let [p, ...parts] = property.split('.'); const meta = this.metadata.find(entityName); if (!meta) { return true; } if (p.includes(':')) { p = p.split(':', 2)[0]; } // For TPT inheritance, check the entity's own properties, not just the root's // For STI, meta.properties includes all properties anyway const ret = p in meta.properties; if (parts.length > 0) { return this.canPopulate(meta.properties[p].targetMeta.class, parts.join('.')); } return ret; } /** * Loads specified relations in batch. This will execute one query for each relation, that will populate it on all the specified entities. */ async populate(entities, populate, options = {}) { const arr = Utils.asArray(entities); if (arr.length === 0) { return entities; } const em = this.getContext(); em.prepareOptions(options); const entityName = arr[0].constructor; const preparedPopulate = await em.preparePopulate( entityName, { populate: populate, filters: options.filters }, options.validate, ); await em.#entityLoader.populate(entityName, arr, preparedPopulate, options); return entities; } /** * Returns new EntityManager instance with its own identity map */ fork(options = {}) { const em = options.disableContextResolution ? this : this.getContext(false); options.clear ??= true; options.useContext ??= false; options.freshEventManager ??= false; options.cloneEventManager ??= false; const eventManager = options.freshEventManager ? new EventManager(em.config.get('subscribers')) : options.cloneEventManager ? em.eventManager.clone() : em.eventManager; // we need to allow global context here as forking from global EM is fine const allowGlobalContext = em.config.get('allowGlobalContext'); em.config.set('allowGlobalContext', true); const fork = new em.constructor(em.config, em.driver, em.metadata, options.useContext, eventManager); fork.setFlushMode(options.flushMode ?? em.#flushMode); fork.#disableTransactions = options.disableTransactions ?? this.#disableTransactions ?? this.config.get('disableTransactions'); em.config.set('allowGlobalContext', allowGlobalContext); if (options.keepTransactionContext) { fork.#transactionContext = em.#transactionContext; } fork.#filters = { ...em.#filters }; fork.#filterParams = Utils.copy(em.#filterParams); fork.loggerContext = Utils.merge({}, em.loggerContext, options.loggerContext); fork.#schema = options.schema ?? em.#schema; if (!options.clear) { for (const entity of em.#unitOfWork.getIdentityMap()) { fork.#unitOfWork.register(entity); } for (const entity of em.#unitOfWork.getPersistStack()) { fork.#unitOfWork.persist(entity); } for (const entity of em.#unitOfWork.getOrphanRemoveStack()) { fork.#unitOfWork.getOrphanRemoveStack().add(entity); } } return fork; } /** * Gets the UnitOfWork used by the EntityManager to coordinate operations. */ getUnitOfWork(useContext = true) { if (!useContext) { return this.#unitOfWork; } return this.getContext().#unitOfWork; } /** * Gets the EntityFactory used by the EntityManager. */ getEntityFactory() { return this.getContext().#entityFactory; } /** * @internal use `em.populate()` as the user facing API, this is exposed only for internal usage */ getEntityLoader() { return this.getContext().#entityLoader; } /** * Gets the Hydrator used by the EntityManager. */ getHydrator() { return this.config.getHydrator(this.getMetadata()); } /** * Gets the EntityManager based on current transaction/request context. * @internal */ getContext(validate = true) { if (!this.#useContext) { return this; } let em = TransactionContext.getEntityManager(this.name); // prefer the tx context if (em) { return em; } // no explicit tx started em = this.config.get('context')(this.name) ?? this; if (validate && !this.config.get('allowGlobalContext') && em.global) { throw ValidationError.cannotUseGlobalContext(); } return em; } /** Gets the EventManager instance used by this EntityManager. */ getEventManager() { return this.eventManager; } /** * Checks whether this EntityManager is currently operating inside a database transaction. */ isInTransaction() { return !!this.getContext(false).#transactionContext; } /** * Gets the transaction context (driver dependent object used to make sure queries are executed on same connection). */ getTransactionContext() { return this.getContext(false).#transactionContext; } /** * Sets the transaction context. */ setTransactionContext(ctx) { if (!ctx) { this.resetTransactionContext(); } else { this.getContext(false).#transactionContext = ctx; } } /** * Resets the transaction context. */ resetTransactionContext() { this.getContext(false).#transactionContext = undefined; } /** * Gets the `MetadataStorage` (without parameters) or `EntityMetadata` instance when provided with the `entityName` parameter. */ getMetadata(entityName) { if (entityName) { return this.metadata.get(entityName); } return this.metadata; } /** * Gets the EntityComparator. */ getComparator() { return this.#comparator; } checkLockRequirements(mode, meta) { if (!mode) { return; } if (mode === LockMode.OPTIMISTIC && !meta.versionProperty) { throw OptimisticLockError.notVersioned(meta); } if ([LockMode.PESSIMISTIC_READ, LockMode.PESSIMISTIC_WRITE].includes(mode) && !this.isInTransaction()) { throw ValidationError.transactionRequired(); } } async lockAndPopulate(meta, entity, where, options) { if (!meta.virtual && options.lockMode === LockMode.OPTIMISTIC) { await this.lock(entity, options.lockMode, { lockVersion: options.lockVersion, lockTableAliases: options.lockTableAliases, }); } const preparedPopulate = await this.preparePopulate(meta.class, options); await this.#entityLoader.populate(meta.class, [entity], preparedPopulate, { ...options, ...this.getPopulateWhere(where, options), orderBy: options.populateOrderBy ?? options.orderBy, ignoreLazyScalarProperties: true, lookup: false, }); return entity; } buildFields(fields) { return fields.reduce((ret, f) => { if (Utils.isPlainObject(f)) { Utils.keys(f).forEach(ff => ret.push(...this.buildFields(f[ff]).map(field => `${ff}.${field}`))); } else { ret.push(f); } return ret; }, []); } /** @internal */ async preparePopulate(entityName, options, validate = true) { if (options.populate === false) { return []; } const meta = this.metadata.find(entityName); // infer populate hint if only `fields` are available if (!options.populate && options.fields) { // 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 const pruneToOneRelations = (meta, fields) => { const ret = []; for (let field of fields) { if (field === PopulatePath.ALL || field.startsWith(`${PopulatePath.ALL}.`)) { ret.push( ...meta.props .filter(prop => prop.lazy || [ReferenceKind.SCALAR, ReferenceKind.EMBEDDED].includes(prop.kind)) .map(prop => prop.name), ); continue; } field = field.split(':')[0]; if ( !field.includes('.') && ![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(meta.properties[field].kind) ) { ret.push(field); continue; } const parts = field.split('.'); const key = parts.shift(); if (parts.length === 0) { continue; } const prop = meta.properties[key]; if (!prop.targetMeta) { ret.push(key); continue; } const inner = pruneToOneRelations(prop.targetMeta, [parts.join('.')]); if (inner.length > 0) { ret.push(...inner.map(c => `${key}.${c}`)); } } return Utils.unique(ret); }; options.populate = pruneToOneRelations(meta, this.buildFields(options.fields)); } if (!options.populate) { const populate = this.#entityLoader.normalizePopulate(entityName, [], options.strategy, true, options.exclude); await this.autoJoinRefsForFilters(meta, { ...options, populate }); return populate; } if (typeof options.populate !== 'boolean') { options.populate = Utils.asArray(options.populate) .map(field => { /* v8 ignore next */ if (typeof field === 'boolean' || field === PopulatePath.ALL) { return [{ field: meta.primaryKeys[0], strategy: options.strategy, all: !!field }]; // } // will be handled in QueryBuilder when processing the where condition via CriteriaNode if (field === PopulatePath.INFER) { options.flags ??= []; options.flags.push(QueryFlag.INFER_POPULATE); return []; } if (typeof field === 'string') { return [{ field, strategy: options.strategy }]; } return [field]; }) .flat(); } const populate = this.#entityLoader.normalizePopulate( entityName, options.populate, options.strategy, true, options.exclude, ); const invalid = populate.find(({ field }) => !this.canPopulate(entityName, field)); if (validate && invalid) { throw ValidationError.invalidPropertyName(entityName, invalid.field); } await this.autoJoinRefsForFilters(meta, { ...options, populate }); for (const field of populate) { // force select-in strategy when populating all relations as otherwise we could cause infinite loops when self-referencing const all = field.all ?? (Array.isArray(options.populate) && options.populate.includes('*')); field.strategy = all ? LoadStrategy.SELECT_IN : (options.strategy ?? field.strategy); } if (options.populateHints) { applyPopulateHints(populate, options.populateHints); } return populate; } /** * when the entity is found in identity map, we check if it was partially loaded or we are trying to populate * some additional lazy properties, if so, we reload and merge the data from database */ shouldRefresh(meta, entity, options) { if (!helper(entity).__initialized || options.refresh) { return true; } let autoRefresh; if (options.fields) { autoRefresh = options.fields.some(field => !helper(entity).__loadedProperties.has(field)); } else { autoRefresh = meta.comparableProps.some(prop => { const inlineEmbedded = prop.kind === ReferenceKind.EMBEDDED && !prop.object; return !inlineEmbedded && !prop.lazy && !helper(entity).__loadedProperties.has(prop.name); }); } if (autoRefresh || options.filters) { return true; } if (Array.isArray(options.populate)) { return options.populate.some(field => !helper(entity).__loadedProperties.has(field)); } return !!options.populate; } prepareOptions(options) { if (!Utils.isEmpty(options.fields) && !Utils.isEmpty(options.exclude)) { throw new ValidationError(`Cannot combine 'fields' and 'exclude' option.`); } options.schema ??= this.#schema; options.logging = options.loggerContext = Utils.merge( { id: this.id }, this.loggerContext, options.loggerContext, options.logging, ); } /** * @internal */ cacheKey(entityName, options, method, where) { const { ...opts } = options; // ignore some irrelevant options, e.g. logger context can contain dynamic data for the same query for (const k of ['ctx', 'strategy', 'flushMode', 'logging', 'loggerContext']) { delete opts[k]; } return [Utils.className(entityName), method, opts, where]; } /** * @internal */ async tryCache(entityName, config, key, refresh, merge) { config ??= this.config.get('resultCache').global; if (!config) { return undefined; } const em = this.getContext(); const cacheKey = Array.isArray(config) ? config[0] : JSON.stringify(key); const cached = await em.#resultCache.get(cacheKey); if (!cached) { return { key: cacheKey, data: cached }; } let data; const createOptions = { merge: true, convertCustomTypes: false, refresh, recomputeSnapshot: true, }; if (Array.isArray(cached) && merge) { data = cached.map(item => em.#entityFactory.create(entityName, item, createOptions)); } else if (Utils.isObject(cached) && merge) { data = em.#entityFactory.create(entityName, cached, createOptions); } else { data = cached; } await em.#unitOfWork.dispatchOnLoadEvent(); return { key: cacheKey, data }; } /** * @internal */ async storeCache(config, key, data) { config ??= this.config.get('resultCache').global; if (config) { const em = this.getContext(); const expiration = Array.isArray(config) ? config[1] : typeof config === 'number' ? config : undefined; await em.#resultCache.set(key.key, data instanceof Function ? data() : data, '', expiration); } } /** * Clears result cache for given cache key. If we want to be able to call this method, * we need to set the cache key explicitly when storing the cache. * * ```ts * // set the cache key to 'book-cache-key', with expiration of 60s * const res = await em.find(Book, { ... }, { cache: ['book-cache-key', 60_000] }); * * // clear the cache key by name * await em.clearCache('book-cache-key'); * ``` */ async clearCache(cacheKey) { await this.getContext().#resultCache.remove(cacheKey); } /** * Returns the default schema of this EntityManager. Respects the context, so global EM will give you the contextual schema * if executed inside request context handler. */ get schema() { return this.getContext(false).#schema; } /** * Sets the default schema of this EntityManager. Respects the context, so global EM will set the contextual schema * if executed inside request context handler. */ set schema(schema) { this.getContext(false).#schema = schema ?? undefined; } /** @internal */ async getDataLoader(type) { const em = this.getContext(); if (em.#loaders[type]) { return em.#loaders[type]; } const { DataloaderUtils } = await import('@mikro-orm/core/dataloader'); const DataLoader = await DataloaderUtils.getDataLoader(); switch (type) { case 'ref': return (em.#loaders[type] ??= new DataLoader(DataloaderUtils.getRefBatchLoadFn(em))); case '1:m': return (em.#loaders[type] ??= new DataLoader(DataloaderUtils.getColBatchLoadFn(em))); case 'm:n': return (em.#loaders[type] ??= new DataLoader(DataloaderUtils.getManyToManyColBatchLoadFn(em))); } } /** * Returns the ID of this EntityManager. Respects the context, so global EM will give you the contextual ID * if executed inside request context handler. */ get id() { return this.getContext(false)._id; } /** @ignore */ [Symbol.for('nodejs.util.inspect.custom')]() { return `[EntityManager<${this.id}>]`; } }