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.

197 lines (196 loc) 9 kB
import { Utils } from '../utils/Utils.js'; import { QueryHelper } from '../utils/QueryHelper.js'; import { ChangeSet, ChangeSetType } from './ChangeSet.js'; import { helper } from '../entity/wrap.js'; import { validateEntity } from '../entity/validators.js'; import { Reference } from '../entity/Reference.js'; import { PolymorphicRef } from '../entity/PolymorphicRef.js'; import { ReferenceKind } from '../enums.js'; import { isRaw } from '../utils/RawQueryFragment.js'; /** @internal Computes change sets by comparing entity state against original snapshots. */ export class ChangeSetComputer { #comparator; #metadata; #platform; #config; #em; #collectionUpdates; constructor(em, collectionUpdates) { this.#em = em; this.#collectionUpdates = collectionUpdates; this.#config = this.#em.config; this.#metadata = this.#em.getMetadata(); this.#platform = this.#em.getPlatform(); this.#comparator = this.#config.getComparator(this.#metadata); } /** Computes a change set for the given entity by diffing against its original state. */ computeChangeSet(entity) { const meta = this.#metadata.get(entity.constructor); if (meta.readonly) { return null; } const wrapped = helper(entity); const type = wrapped.__originalEntityData ? ChangeSetType.UPDATE : ChangeSetType.CREATE; const map = new Map(); // Execute `onCreate` and `onUpdate` on properties recursively, saves `onUpdate` results // to the `map` as we want to apply those only if something else changed. if (type === ChangeSetType.CREATE) { // run update hooks only after we know there are other changes for (const prop of meta.hydrateProps) { this.processPropertyInitializers(entity, prop, type, map); } } if (type === ChangeSetType.UPDATE && !wrapped.__initialized) { const data = this.#comparator.prepareEntity(entity); if (meta.root.discriminatorColumn) { delete data[meta.root.discriminatorColumn]; } if (Utils.equals(data, wrapped.__originalEntityData)) { return null; } } const changeSet = new ChangeSet(entity, type, this.computePayload(entity), meta); changeSet.originalEntity = wrapped.__originalEntityData; for (const prop of meta.relations.filter(prop => prop.persist !== false || prop.userDefined === false)) { this.processProperty(changeSet, prop); } if (changeSet.type === ChangeSetType.UPDATE && !Utils.hasObjectKeys(changeSet.payload)) { return null; } validateEntity(changeSet.entity, meta); // Execute `onCreate` and `onUpdate` on properties recursively, saves `onUpdate` results // to the `map` as we want to apply those only if something else changed. if (type === ChangeSetType.UPDATE) { for (const prop of meta.hydrateProps) { this.processPropertyInitializers(entity, prop, type, map); } } if (map.size > 0) { for (const [entity, pairs] of map) { for (const [prop, value] of pairs) { entity[prop] = value; } } // Recompute the changeset, we need to merge this as here we ignore relations. const diff = this.computePayload(entity, true); Utils.merge(changeSet.payload, diff); } return changeSet; } /** * Traverses entity graph and executes `onCreate` and `onUpdate` methods, assigning the values to given properties. */ processPropertyInitializers(entity, prop, type, map, nested) { if (prop.onCreate && type === ChangeSetType.CREATE && (entity[prop.name] == null || (Utils.isScalarReference(entity[prop.name]) && entity[prop.name].unwrap() == null))) { entity[prop.name] = prop.onCreate(entity, this.#em); } else if (prop.default != null && !isRaw(prop.default) && ![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && type === ChangeSetType.CREATE && entity[prop.name] === undefined) { entity[prop.name] = prop.default; } if (prop.onUpdate && type === ChangeSetType.UPDATE) { const pairs = map.get(entity) ?? []; pairs.push([prop.name, prop.onUpdate(entity, this.#em)]); map.set(entity, pairs); } if (prop.kind === ReferenceKind.EMBEDDED && entity[prop.name]) { const items = prop.array ? entity[prop.name] : [entity[prop.name]]; for (const item of items) { for (const embeddedProp of prop.targetMeta.hydrateProps) { this.processPropertyInitializers(item, embeddedProp, type, map, nested || prop.object); } } } } computePayload(entity, ignoreUndefined = false) { const data = this.#comparator.prepareEntity(entity); const wrapped = helper(entity); const entityName = wrapped.__meta.class; const originalEntityData = wrapped.__originalEntityData; if (!wrapped.__initialized) { for (const prop of wrapped.__meta.primaryKeys) { delete data[prop]; } // strip discriminator column for STI entities — it's always hardcoded // in the snapshot generator and not present in __originalEntityData, // causing false dirty detection on uninitialized reference proxies if (wrapped.__meta.root.discriminatorColumn) { delete data[wrapped.__meta.root.discriminatorColumn]; } return data; } if (originalEntityData) { const comparator = this.#comparator.getEntityComparator(entityName); const diff = comparator(originalEntityData, data); if (ignoreUndefined) { Utils.keys(diff) .filter(k => diff[k] === undefined) .forEach(k => delete diff[k]); } return diff; } return data; } processProperty(changeSet, prop, target) { if (!target) { const targets = Utils.unwrapProperty(changeSet.entity, changeSet.meta, prop); targets.forEach(([t]) => this.processProperty(changeSet, prop, t)); return; } if (Utils.isCollection(target)) { // m:n or 1:m this.processToMany(prop, changeSet); } if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) { this.processToOne(prop, changeSet); } } processToOne(prop, changeSet) { const isToOneOwner = prop.kind === ReferenceKind.MANY_TO_ONE || (prop.kind === ReferenceKind.ONE_TO_ONE && prop.owner); if (!isToOneOwner || prop.mapToPk) { return; } const targets = Utils.unwrapProperty(changeSet.entity, changeSet.meta, prop); targets.forEach(([rawTarget, idx]) => { const target = Reference.unwrapReference(rawTarget); const needsProcessing = target != null && (prop.targetKey != null || !target.__helper.hasPrimaryKey()); if (needsProcessing) { let value = prop.targetKey ? target[prop.targetKey] : target.__helper.__identifier; /* v8 ignore next */ if (prop.targetKey && prop.targetMeta) { const targetProp = prop.targetMeta.properties[prop.targetKey]; if (targetProp?.customType) { value = targetProp.customType.convertToDatabaseValue(value, this.#platform, { mode: 'serialization' }); } } if (prop.polymorphic) { const discriminator = QueryHelper.findDiscriminatorValue(prop.discriminatorMap, target.constructor); Utils.setPayloadProperty(changeSet.payload, changeSet.meta, prop, new PolymorphicRef(discriminator, value), idx); } else { Utils.setPayloadProperty(changeSet.payload, changeSet.meta, prop, value, idx); } } }); } processToMany(prop, changeSet) { const target = changeSet.entity[prop.name]; if (!target.isDirty() && changeSet.type !== ChangeSetType.CREATE) { return; } if (target.isDirty()) { this.#collectionUpdates.add(target); } if (prop.owner && !this.#platform.usesPivotTable()) { changeSet.payload[prop.name] = target.getItems(false).map((item) => { return item.__helper.__identifier ?? item.__helper.getPrimaryKey(); }); } } }