@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.
259 lines (258 loc) • 12.6 kB
JavaScript
import { Collection } from './Collection.js';
import { Utils } from '../utils/Utils.js';
import { Reference } from './Reference.js';
import { ReferenceKind, SCALAR_TYPES } from '../enums.js';
import { validateProperty } from './validators.js';
import { helper, wrap } from './wrap.js';
import { EntityHelper } from './EntityHelper.js';
import { ValidationError } from '../errors.js';
/** Handles assigning data to entities, resolving relations, and propagating changes. */
export class EntityAssigner {
/** Assigns the given data to the entity, resolving relations and handling custom types. */
static assign(entity, data, options = {}) {
let opts = options;
if (opts.visited?.has(entity)) {
return entity;
}
EntityHelper.ensurePropagation(entity);
opts.visited ??= new Set();
opts.visited.add(entity);
const wrapped = helper(entity);
opts = {
...wrapped.__config.get('assign'),
schema: wrapped.__schema,
...opts, // allow overriding the defaults
};
const meta = wrapped.__meta;
const props = meta.properties;
Object.keys(data).forEach(prop => {
return EntityAssigner.assignProperty(entity, prop, props, data, {
...opts,
em: opts.em || wrapped.__em,
platform: wrapped.__platform,
});
});
return entity;
}
static assignProperty(entity, propName, props, data, options) {
let value = data[propName];
const onlyProperties = options.onlyProperties && !(propName in props);
const ignoreUndefined = options.ignoreUndefined === true && value === undefined;
if (onlyProperties || ignoreUndefined) {
return;
}
const prop = { ...props[propName], name: propName };
if (prop && options.onlyOwnProperties) {
if ([ReferenceKind.ONE_TO_MANY].includes(prop.kind)) {
return;
}
if ([ReferenceKind.MANY_TO_MANY].includes(prop.kind)) {
if (!prop.owner) {
return;
}
else if (value?.map) {
value = value.map((v) => Utils.extractPK(v, prop.targetMeta));
}
}
if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
value = Utils.extractPK(value, prop.targetMeta);
}
}
if (propName in props && !prop.nullable && value == null) {
throw new Error(`You must pass a non-${value} value to the property ${propName} of entity ${entity.constructor.name}.`);
}
// create collection instance if its missing so old items can be deleted with orphan removal
if ([ReferenceKind.MANY_TO_MANY, ReferenceKind.ONE_TO_MANY].includes(prop?.kind) && entity[prop.name] == null) {
entity[prop.name] = Collection.create(entity, prop.name, undefined, helper(entity).isInitialized());
}
if (prop && Utils.isCollection(entity[prop.name])) {
return EntityAssigner.assignCollection(entity, entity[prop.name], value, prop, options.em, options);
}
const customType = prop?.customType;
if (options.convertCustomTypes && customType && prop.kind === ReferenceKind.SCALAR && !Utils.isEntity(data)) {
value = customType.convertToJSValue(value, options.platform);
}
if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop?.kind) && value != null) {
if (options.updateNestedEntities &&
Object.hasOwn(entity, propName) &&
Utils.isEntity(entity[propName], true) &&
Utils.isPlainObject(value)) {
const unwrappedEntity = Reference.unwrapReference(entity[propName]);
const wrapped = helper(unwrappedEntity);
if (options.updateByPrimaryKey) {
const pk = Utils.extractPK(value, prop.targetMeta);
if (pk) {
const ref = options.em.getReference(prop.targetMeta.class, pk, options);
// if the PK differs, we want to change the target entity, not update it
const wrappedChild = helper(ref);
const sameTarget = wrappedChild.getSerializedPrimaryKey() === wrapped.getSerializedPrimaryKey();
if (wrappedChild.__managed && wrappedChild.isInitialized() && sameTarget) {
return EntityAssigner.assign(ref, value, options);
}
}
return EntityAssigner.assignReference(entity, value, prop, options.em, options);
}
if (wrapped.__managed && wrap(unwrappedEntity).isInitialized()) {
return EntityAssigner.assign(unwrappedEntity, value, options);
}
}
return EntityAssigner.assignReference(entity, value, prop, options.em, options);
}
if (prop.kind === ReferenceKind.SCALAR && SCALAR_TYPES.has(prop.runtimeType) && (prop.setter || !prop.getter)) {
// mirror the hydrator (used by `em.create`) and coerce string/number inputs to `Date` instances,
// since `EntityData` already permits `string | Date` for `Date`-typed properties at the type level
if (prop.runtimeType === 'Date' &&
value != null &&
!(value instanceof Date) &&
(typeof value === 'string' || typeof value === 'number')) {
value = new Date(value);
}
validateProperty(prop, value, entity);
return (entity[prop.name] = value);
}
if (prop.kind === ReferenceKind.EMBEDDED && EntityAssigner.validateEM(options.em)) {
return EntityAssigner.assignEmbeddable(entity, value, prop, options.em, options);
}
if (options.mergeObjectProperties &&
Utils.isPlainObject(entity[propName]) &&
Utils.isPlainObject(value)) {
entity[propName] ??= {};
entity[propName] = Utils.merge({}, entity[propName], value);
}
else if (!prop || prop.setter || !prop.getter) {
entity[propName] = value;
}
}
/**
* auto-wire 1:1 inverse side with owner as in no-sql drivers it can't be joined
* also makes sure the link is bidirectional when creating new entities from nested structures
* @internal
*/
static autoWireOneToOne(prop, entity) {
const ref = entity[prop.name];
if (prop.kind !== ReferenceKind.ONE_TO_ONE || !Utils.isEntity(ref)) {
return;
}
const meta2 = helper(ref).__meta;
const prop2 = meta2.properties[prop.inversedBy || prop.mappedBy];
/* v8 ignore next */
if (prop2 && !ref[prop2.name]) {
if (Reference.isReference(ref)) {
ref.unwrap()[prop2.name] = Reference.wrapReference(entity, prop2);
}
else {
ref[prop2.name] = Reference.wrapReference(entity, prop2);
}
}
}
static validateEM(em) {
if (!em) {
throw new Error(`To use assign() on not managed entities, explicitly provide EM instance: wrap(entity).assign(data, { em: orm.em })`);
}
return true;
}
static assignReference(entity, value, prop, em, options) {
if (Utils.isEntity(value, true)) {
entity[prop.name] = Reference.wrapReference(value, prop);
}
else if (Utils.isPrimaryKey(value, true) && EntityAssigner.validateEM(em)) {
entity[prop.name] = prop.mapToPk
? value
: Reference.wrapReference(em.getReference(prop.targetMeta.class, value, options), prop);
}
else if (Utils.isPlainObject(value) && options.merge && EntityAssigner.validateEM(em)) {
entity[prop.name] = Reference.wrapReference(em.merge(prop.targetMeta.class, value, options), prop);
}
else if (Utils.isPlainObject(value) && EntityAssigner.validateEM(em)) {
entity[prop.name] = Reference.wrapReference(em.create(prop.targetMeta.class, value, options), prop);
}
else {
const name = entity.constructor.name;
throw new Error(`Invalid reference value provided for '${name}.${prop.name}' in ${name}.assign(): ${JSON.stringify(value)}`);
}
EntityAssigner.autoWireOneToOne(prop, entity);
}
static assignCollection(entity, collection, value, prop, em, options) {
const invalid = [];
const items = Utils.asArray(value).map((item, idx) => {
// try to propagate missing owning side reference to the payload first
const prop2 = prop.targetMeta?.properties[prop.mappedBy];
if (Utils.isPlainObject(item) && prop2 && item[prop2.name] == null) {
item = { ...item, [prop2.name]: Reference.wrapReference(entity, prop2) };
}
if (options.updateNestedEntities && options.updateByPrimaryKey && Utils.isPlainObject(item)) {
const pk = Utils.extractPK(item, prop.targetMeta);
if (pk && EntityAssigner.validateEM(em)) {
const ref = em.getUnitOfWork().getById(prop.targetMeta.class, pk, options.schema);
if (ref) {
return EntityAssigner.assign(ref, item, options);
}
}
return this.createCollectionItem(item, em, prop, invalid, options);
}
/* v8 ignore next */
if (options.updateNestedEntities &&
!options.updateByPrimaryKey &&
collection[idx] &&
helper(collection[idx])?.isInitialized()) {
return EntityAssigner.assign(collection[idx], item, options);
}
return this.createCollectionItem(item, em, prop, invalid, options);
});
if (invalid.length > 0) {
const name = entity.constructor.name;
throw ValidationError.invalidCollectionValues(name, prop.name, invalid);
}
if (Array.isArray(value)) {
collection.set(items);
}
else {
// append to the collection in case of assigning a single value instead of array
collection.add(items);
}
}
static assignEmbeddable(entity, value, prop, em, options) {
const propName = prop.embedded ? prop.embedded[1] : prop.name;
if (value == null) {
entity[propName] = value;
return;
}
// if the value is not an array, we just push, otherwise we replace the array
if (prop.array && (Array.isArray(value) || entity[propName] == null)) {
entity[propName] = [];
}
if (prop.array) {
return Utils.asArray(value).forEach(item => {
const tmp = {};
this.assignEmbeddable(tmp, item, { ...prop, array: false }, em, options);
entity[propName].push(...Object.values(tmp));
});
}
const create = () => EntityAssigner.validateEM(em) &&
em.getEntityFactory().createEmbeddable(prop.targetMeta.class, value, {
convertCustomTypes: options.convertCustomTypes,
newEntity: options.mergeEmbeddedProperties ? !('propName' in entity) : true,
});
entity[propName] = (options.mergeEmbeddedProperties ? entity[propName] || create() : create());
Object.keys(value).forEach(key => {
EntityAssigner.assignProperty(entity[propName], key, prop.embeddedProps, value, options);
});
}
static createCollectionItem(item, em, prop, invalid, options) {
if (Utils.isEntity(item)) {
return item;
}
if (Utils.isPrimaryKey(item) && EntityAssigner.validateEM(em)) {
return em.getReference(prop.targetMeta.class, item, options);
}
if (Utils.isPlainObject(item) && options.merge && EntityAssigner.validateEM(em)) {
return em.merge(prop.targetMeta.class, item, options);
}
if (Utils.isPlainObject(item) && EntityAssigner.validateEM(em)) {
return em.create(prop.targetMeta.class, item, options);
}
invalid.push(item);
return item;
}
}
export const assign = EntityAssigner.assign;