UNPKG

@mikro-orm/core

Version:

TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.

1,083 lines (1,082 loc) 98.2 kB
import { getOnConflictReturningFields, getWhereCondition, resetUntouchedCollections } 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; /** @internal */ signal; /** @internal */ inflightQueryAbortStrategy; #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); } 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); const meta = this.metadata.get(entityName); em.validateIndexUsage(meta, where, options); await em.tryFlush(entityName, options); where = await em.processWhere(entityName, where, options, 'read'); validateParams(where); 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); em.validateIndexUsage(meta, options.where ?? {}, options); 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, parent) => { const children = types.filter(meta2 => meta2.extends && this.metadata.find(meta2.extends) === parent); children.forEach(m => lookUpChildren(ret, m)); ret.push(...children.filter(c => c.discriminatorValue)); return children; }; lookUpChildren(children, meta); /* 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.ONE_TO_ONE && !prop.owner)) && prop.kind !== ReferenceKind.SCALAR; if (!joined && !hint.filter) { continue; } // Polymorphic to-one relations are already filtered via per-target LEFT JOINs created // for the `:ref filter` hint; emitting a populate-filter entry here would auto-join only // the first target meta and either drop rows pointing to other targets or reference // parent-table-only columns on the child sub-table alias for TPT children. if (prop.polymorphic && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) { 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) }; // For polymorphic relations, check all targets for filters (not just the first targetMeta) let hasActiveFilter = false; if (prop.polymorphic && prop.polymorphTargets?.length) { for (const targetMeta of prop.polymorphTargets) { const cond = await this.applyFilters(targetMeta.class, {}, options.filters, 'read', options); if (!Utils.isEmpty(cond)) { hasActiveFilter = true; break; } } } else { const cond = await this.applyFilters(prop.targetMeta.class, {}, options.filters, 'read', options); hasActiveFilter = !Utils.isEmpty(cond); } if (hasActiveFilter) { const populated = options.populate.filter(({ field }) => field.split(':')[0] === prop.name); let found = false; for (const hint of populated) { const strategy = getLoadingStrategy(prop.strategy || hint.strategy || options.strategy || this.config.get('loadStrategy'), prop.kind); // inverse 1:1 always produces a JOIN (forced by `joinedProps()`), regardless of strategy const willJoin = strategy === LoadStrategy.JOINED || (prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner); if (!hint.all || willJoin) { hint.filter = true; } if (hint.field === `${prop.name}:ref` || (hint.filter && willJoin)) { 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 ??= []; const targets = prop.polymorphic && prop.polymorphTargets?.length ? prop.polymorphTargets : [prop.targetMeta]; for (const targetMeta of targets) { const before = hint.children.length; await this.autoJoinRefsForFilters(targetMeta, { ...options, populate: hint.children }, { class: meta.root.class, propName: prop.name }); // For polymorphic relations, `hint.children` is applied to every polymorph target during // population, so drop any auto-added hint whose field does not exist on every target — // otherwise the shared `:ref` would crash when applied to a target lacking it (GH #7722). if (targets.length > 1 && hint.children.length > before) { const added = hint.children.splice(before); for (const h of added) { const f = h.field.split(':')[0]; if (targets.every(t => f in t.properties)) { hint.children.push(h); } } } } } } } /** * @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]; } 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<User> { * 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: false, }); em.config .getHydrator(this.metadata) .hydrate(entity, wrapped.__meta, data, em.#entityFactory, 'full', false, false); Utils.merge(wrapped.__originalEntityData, this.#comparator.prepareEntity(reloaded)); } return entity; } 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); em.validateIndexUsage(meta, where, options); where = await em.processWhere(entityName, where, options, 'read'); validateEmptyWhere(where); em.checkLockRequirements(options.lockMode, meta); const isOptimisticLocking = options.lockMode == null || options.lockMode === LockMode.NONE || 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; } 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(); em.#entityFactory.assignDefaultValues(entity, meta); 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); } } em.#entityFactory.assignDefaultValues(data, meta, true); for (const key of Object.keys(data)) { const prop = meta.properties[key]; if (prop?.persist === false) { delete data[key]; } } } 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: entity ?? data, em, meta }, meta); if (entity) { data = em.#comparator.prepareEntity(entity); } } 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.concat(...(options.onConflictMergeFields ?? [])), 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) resetUntouchedCollections(meta, entity); 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(); const entitiesByAllDataIdx = 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(); em.#entityFactory.assignDefaultValues(entity, meta); entitiesByAllDataIdx.set(allData.length, entity); 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; } } em.#entityFactory.assignDefaultValues(row, meta, true); for (const key of Object.keys(row)) { const prop = meta.properties[key]; if (prop?.persist === false) { delete row[key]; } } } 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); } for (const [idx, entity] of entitiesByAllDataIdx) { allData[idx] = em.#comparator.prepareEntity(entity); } } 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 the reload only when RETURNING brought back a row for every input // (onConflictWhere can suppress writes, leaving some rows out) // 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 === data.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 : [])) .concat(...(options.onConflictMergeFields ?? [])), 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)) {