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.

316 lines (315 loc) • 16 kB
import { ReferenceKind, } from './enums.js'; import { Reference } from './entity/Reference.js'; import { EntityHelper } from './entity/EntityHelper.js'; import { helper } from './entity/wrap.js'; import { Utils } from './utils/Utils.js'; import { EntityComparator } from './utils/EntityComparator.js'; /** Symbol used to declare a custom repository type on an entity class (e.g., `[EntityRepositoryType]?: BookRepository`). */ export const EntityRepositoryType = Symbol('EntityRepositoryType'); /** Symbol used to declare the primary key property name(s) on an entity (e.g., `[PrimaryKeyProp]?: 'id'`). */ export const PrimaryKeyProp = Symbol('PrimaryKeyProp'); /** Symbol used as a brand on `CollectionShape` to prevent false structural matches with entities that have properties like `owner`. */ export const CollectionBrand = Symbol('CollectionBrand'); /** Symbol used to declare which properties are optional in `em.create()` (e.g., `[OptionalProps]?: 'createdAt'`). */ export const OptionalProps = Symbol('OptionalProps'); /** Symbol used to declare which relation properties should be eagerly loaded (e.g., `[EagerProps]?: 'author'`). */ export const EagerProps = Symbol('EagerProps'); /** Symbol used to declare which properties are hidden from serialization (e.g., `[HiddenProps]?: 'password'`). */ export const HiddenProps = Symbol('HiddenProps'); /** Symbol used to declare type-level configuration on an entity (e.g., `[Config]?: DefineConfig<{ forceObject: true }>`). */ export const Config = Symbol('Config'); /** Symbol used to declare the entity name as a string literal type (used by `defineEntity`). */ // eslint-disable-next-line @typescript-eslint/no-redeclare export const EntityName = Symbol('EntityName'); /** * Symbol used to declare index-to-column mappings on an entity type. * For decorator entities, declare as a phantom property: * ```typescript * [IndexHints]?: { idx_email: 'email'; idx_name_age: 'name' | 'age' }; * ``` * For `defineEntity` entities, index hints are inferred automatically from * named indexes (property-level `.index('name')` and entity-level `indexes`/`uniques`). */ export const IndexHints = Symbol('IndexHints'); /** * Runtime metadata for an entity, holding its properties, relations, indexes, hooks, and more. * Created during metadata discovery and used throughout the ORM lifecycle. */ export class EntityMetadata { static counter = 0; _id = 1000 * EntityMetadata.counter++; // keep the id >= 1000 to allow computing cache keys by simple addition propertyOrder = new Map(); constructor(meta = {}) { this.properties = {}; this.props = []; this.primaryKeys = []; this.filters = {}; this.hooks = {}; this.indexes = []; this.uniques = []; this.checks = []; this.triggers = []; this.referencingProperties = []; this.concurrencyCheckKeys = new Set(); Object.assign(this, meta); const name = meta.className ?? meta.name; if (!this.class && name) { let Parent; if (typeof this.extends === 'function') { Parent = this.extends; } else if (this.extends != null && typeof this.extends.class === 'function') { Parent = this.extends.class; } const Class = Parent ? { [name]: class extends Parent { } }[name] : { [name]: class { } }[name]; this.class = Class; } } addProperty(prop) { this.properties[prop.name] = prop; this.propertyOrder.set(prop.name, this.props.length); this.sync(); } removeProperty(name, sync = true) { delete this.properties[name]; this.propertyOrder.delete(name); if (sync) { this.sync(); } } getPrimaryProps(flatten = false) { const pks = this.primaryKeys.map(pk => this.properties[pk]); if (flatten) { return pks.flatMap(pk => { if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(pk.kind)) { return pk.targetMeta.getPrimaryProps(true); } return [pk]; }); } return pks; } getPrimaryProp() { return this.properties[this.primaryKeys[0]]; } /** * Creates a mapping from property names to field names. * @param alias - Optional alias to prefix field names. Can be a string (same for all) or a function (per-property). * When provided, also adds toString() returning the alias for backwards compatibility with formulas. * @param toStringAlias - Optional alias to return from toString(). Defaults to `alias` when it's a string. */ createColumnMappingObject(alias, toStringAlias) { const resolveAlias = typeof alias === 'function' ? alias : () => alias; const defaultAlias = toStringAlias ?? (typeof alias === 'string' ? alias : undefined); const result = Object.values(this.properties).reduce((o, prop) => { if (prop.fieldNames) { const propAlias = resolveAlias(prop); o[prop.name] = propAlias ? `${propAlias}.${prop.fieldNames[0]}` : prop.fieldNames[0]; } return o; }, {}); // Add toString() for backwards compatibility when alias is provided Object.defineProperty(result, 'toString', { value: () => defaultAlias ?? '', enumerable: false, }); // Wrap in Proxy to detect old formula signature usage where the first param was FormulaTable. // If user accesses `.alias` or `.qualifiedName` (FormulaTable-only properties), warn them. const warnedProps = new Set(['alias', 'qualifiedName']); return new Proxy(result, { get(target, prop, receiver) { if (typeof prop === 'string' && warnedProps.has(prop) && !(prop in target)) { // eslint-disable-next-line no-console console.warn(`[MikroORM] Detected old formula callback signature. The first parameter is now 'columns', not 'table'. ` + `Accessing '.${prop}' on the columns object will return undefined. ` + `Update your formula: formula(cols => quote\`\${cols.propName} ...\`). See the v7 upgrade guide.`); } return Reflect.get(target, prop, receiver); }, }); } /** * Creates a column mapping for schema callbacks (indexes, checks, generated columns). * For TPT entities, only includes properties that belong to the current table (ownProps). * Embedded properties expose their sub-columns via nested access (e.g. `cols.address.city`), * while still coercing to the embedded column prefix when used as a string (GH #7712). */ createSchemaColumnMappingObject() { // For TPT entities, only include properties that belong to this entity's table const props = this.inheritanceType === 'tpt' && this.ownProps ? this.ownProps : Object.values(this.properties); const result = {}; for (const prop of props) { if (!prop.fieldNames) { continue; } if (prop.kind === ReferenceKind.EMBEDDED && prop.embeddedProps) { result[prop.name] = EntityMetadata.buildEmbeddedColumnMapping(prop); continue; } result[prop.name] = prop.fieldNames[0]; } return result; } /** @internal Recursively builds a column mapping for an embedded property's sub-columns. */ static buildEmbeddedColumnMapping(embeddedProp) { const result = {}; for (const [name, sub] of Object.entries(embeddedProp.embeddedProps)) { result[name] = sub.kind === ReferenceKind.EMBEDDED && sub.embeddedProps ? EntityMetadata.buildEmbeddedColumnMapping(sub) : sub.fieldNames[0]; } const prefix = embeddedProp.fieldNames[0]; Object.defineProperty(result, 'toString', { value: () => prefix, enumerable: false }); return result; } get tableName() { return this.collection; } set tableName(name) { this.collection = name; } get uniqueName() { return this.tableName + '_' + this._id; } sync(initIndexes = false, config) { this.root ??= this; const props = Object.values(this.properties).sort((a, b) => this.propertyOrder.get(a.name) - this.propertyOrder.get(b.name)); this.props = [...props.filter(p => p.primary), ...props.filter(p => !p.primary)]; this.relations = this.props.filter(prop => typeof prop.kind !== 'undefined' && prop.kind !== ReferenceKind.SCALAR && prop.kind !== ReferenceKind.EMBEDDED); this.bidirectionalRelations = this.relations.filter(prop => prop.mappedBy || prop.inversedBy); this.uniqueProps = this.props.filter(prop => prop.unique); this.getterProps = this.props.filter(prop => prop.getter); this.comparableProps = this.props.filter(prop => EntityComparator.isComparable(prop, this)); this.validateProps = this.props.filter(prop => { if (prop.inherited || (prop.persist === false && prop.userDefined !== false)) { return false; } return (prop.kind === ReferenceKind.SCALAR && !prop.array && ['string', 'number', 'boolean', 'Date'].includes(prop.type)); }); this.hydrateProps = this.props.filter(prop => { // `prop.userDefined` is either `undefined` or `false` const discriminator = this.root.discriminatorColumn === prop.name && prop.userDefined === false; // even if we don't have a setter, do not ignore value from database! const onlyGetter = prop.getter && !prop.setter && prop.persist === false; return !prop.inherited && prop.hydrate !== false && !discriminator && !prop.embedded && !onlyGetter; }); this.trackingProps = this.hydrateProps.filter(prop => { return !prop.getter && !prop.setter && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind); }); this.selfReferencing = this.relations.some(prop => { return this.root.uniqueName === prop.targetMeta?.root.uniqueName; }); this.hasUniqueProps = this.uniques.length + this.uniqueProps.length > 0; // Normalize object-form `view` option: `view: { materialized: true, withData: false }` // into flat metadata fields (`view: true`, `materialized: true`, `withData: false`). if (typeof this.view === 'object') { this.materialized = this.view.materialized; this.withData = this.view.withData; this.view = true; } // If `view` is set, this is a database view entity (not a virtual entity). // Virtual entities evaluate expressions at query time, view entities create actual database views. this.virtual = !!this.expression && !this.view; if (config) { const platform = config.getPlatform(); for (const prop of this.props) { if (prop.enum && !prop.nativeEnumName && prop.items?.every(item => typeof item === 'string')) { const name = platform.getIndexName(this.tableName, prop.fieldNames, 'check'); const exists = this.checks.findIndex(check => check.name === name); if (exists !== -1) { this.checks.splice(exists, 1); } this.checks.push({ name, property: prop.name, expression: platform.getEnumCheckConstraintExpression(prop.fieldNames[0], prop.items), }); } } } this.checks = Utils.removeDuplicates(this.checks); this.indexes = Utils.removeDuplicates(this.indexes); this.uniques = Utils.removeDuplicates(this.uniques); for (const hook of Utils.keys(this.hooks)) { this.hooks[hook] = Utils.removeDuplicates(this.hooks[hook]); } if (this.virtual || this.view) { this.readonly = true; } if (initIndexes && this.name) { this.props.forEach(prop => this.initIndexes(prop)); } this.definedProperties = this.trackingProps.reduce((o, prop) => { const hasInverse = (prop.inversedBy || prop.mappedBy) && !prop.mapToPk; if (hasInverse) { // eslint-disable-next-line @typescript-eslint/no-this-alias const meta = this; o[prop.name] = { get() { return this.__helper.__data[prop.name]; }, set(val) { const wrapped = this.__helper; const hydrator = wrapped.hydrator; const entity = Reference.unwrapReference(val ?? wrapped.__data[prop.name]); const old = Reference.unwrapReference(wrapped.__data[prop.name]); if (old && old !== entity && prop.kind === ReferenceKind.MANY_TO_ONE && prop.inversedBy && old[prop.inversedBy]) { old[prop.inversedBy].removeWithoutPropagation(this); } wrapped.__data[prop.name] = Reference.wrapReference(val, prop); // when propagation from inside hydration, we set the FK to the entity data immediately if (val && hydrator.isRunning() && wrapped.__originalEntityData && prop.owner) { const targetMeta = prop.targetMeta ?? helper(entity)?.__meta; if (targetMeta) { wrapped.__originalEntityData[prop.name] = Utils.getPrimaryKeyValues(val, targetMeta, true); } } EntityHelper.propagate(meta, entity, this, prop, Reference.unwrapReference(val), old); }, enumerable: true, configurable: true, }; } return o; }, { __gettersDefined: { value: true, enumerable: false } }); } initIndexes(prop) { const simpleIndex = this.indexes.find(index => index.properties === prop.name && !index.options && !index.type && !index.expression && index.where == null); const simpleUnique = this.uniques.find(index => index.properties === prop.name && !index.options && !index.expression && index.where == null); const owner = prop.kind === ReferenceKind.MANY_TO_ONE; if (!prop.index && simpleIndex) { Utils.defaultValue(simpleIndex, 'name', true); prop.index = simpleIndex.name; this.indexes.splice(this.indexes.indexOf(simpleIndex), 1); } if (!prop.unique && simpleUnique) { Utils.defaultValue(simpleUnique, 'name', true); prop.unique = simpleUnique.name; this.uniques.splice(this.uniques.indexOf(simpleUnique), 1); } if (prop.index && owner && prop.fieldNames.length > 1) { this.indexes.push({ properties: prop.name }); prop.index = false; } /* v8 ignore next */ if (owner && prop.fieldNames.length > 1 && prop.unique) { this.uniques.push({ properties: prop.name }); prop.unique = false; } } /** @internal */ clone() { return this; } /** @ignore */ [Symbol.for('nodejs.util.inspect.custom')]() { return `[${this.constructor.name}<${this.className}>]`; } }