1937 lines
74 KiB
JavaScript
1937 lines
74 KiB
JavaScript
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<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: 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<T>)` 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}>]`;
|
||
}
|
||
}
|