@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.
277 lines (276 loc) • 12 kB
JavaScript
import { helper } from '../entity/wrap.js';
import { Utils } from '../utils/Utils.js';
import { ReferenceKind } from '../enums.js';
import { Reference } from '../entity/Reference.js';
import { SerializationContext } from './SerializationContext.js';
import { isRaw } from '../utils/RawQueryFragment.js';
/** Returns true when any entry in `items` is `propName`, the wildcard `*`, or a dot-path under `propName`. */
function matchesPath(items, propName) {
return items.some(item => item === propName || item === '*' || item.startsWith(propName + '.'));
}
function isVisible(meta, propName, options) {
const prop = meta.properties[propName];
if (options.groups && prop?.groups) {
return prop.groups.some(g => options.groups.includes(g));
}
if (Array.isArray(options.fields) && options.fields.length > 0 && !matchesPath(options.fields, propName)) {
return false;
}
if (Array.isArray(options.populate) &&
options.populate.find(item => item === propName || item.startsWith(propName + '.'))) {
return true;
}
if (options.exclude?.find(item => item === propName)) {
return false;
}
const visible = prop && !(prop.hidden && !options.includeHidden);
const prefixed = prop && !prop.primary && !prop.accessor && propName.startsWith('_'); // ignore prefixed properties, if it's not a PK
return visible && !prefixed;
}
function isPopulated(propName, options) {
if (typeof options.populate !== 'boolean' &&
Array.isArray(options.populate) &&
matchesPath(options.populate, propName)) {
return true;
}
if (typeof options.populate === 'boolean') {
return options.populate;
}
return false;
}
/** Converts entity instances to plain DTOs via `serialize()`, with fine-grained control over populate, exclude, fields, and serialization groups. */
export class EntitySerializer {
/** Serializes an entity to a plain DTO, with fine-grained control over population, exclusion, fields, groups, and custom types. */
static serialize(entity, options = {}) {
const wrapped = helper(entity);
const meta = wrapped.__meta;
let contextCreated = false;
if (!wrapped.__serializationContext.root) {
const root = new SerializationContext();
SerializationContext.propagate(root, entity, (meta, prop) => meta.properties[prop]?.kind !== ReferenceKind.SCALAR);
options.populate = (options.populate ? Utils.asArray(options.populate) : options.populate);
contextCreated = true;
}
const root = wrapped.__serializationContext.root;
const ret = {};
const props = new Set();
if (meta.serializedPrimaryKey && !meta.compositePK) {
props.add(meta.serializedPrimaryKey);
}
else {
meta.primaryKeys.forEach(pk => props.add(pk));
}
if (wrapped.isInitialized() || !wrapped.hasPrimaryKey()) {
const entityKeys = new Set(Object.keys(entity));
for (const prop of meta.props) {
if (entityKeys.has(prop.name) || (prop.getter && prop.accessor === prop.name)) {
props.add(prop.name);
}
}
for (const key of entityKeys) {
if (!meta.properties[key]) {
props.add(key);
}
}
}
const visited = root.visited.has(entity);
if (!visited) {
root.visited.add(entity);
}
for (const prop of props) {
if (!isVisible(meta, prop, options)) {
continue;
}
const cycle = root.visit(meta.class, prop);
if (cycle && visited) {
continue;
}
const val = this.processProperty(prop, entity, options);
if (!cycle) {
root.leave(meta.class, prop);
}
if (options.skipNull && Utils.isPlainObject(val)) {
Utils.dropUndefinedProperties(val, null);
}
if (isRaw(val)) {
throw new Error(`Trying to serialize raw SQL fragment: '${val.sql}'`);
}
const visible = typeof val !== 'undefined' && !(val === null && options.skipNull);
if (visible) {
ret[this.propertyName(meta, prop)] = val;
}
}
if (contextCreated) {
root.close();
}
if (!wrapped.isInitialized()) {
return ret;
}
for (const prop of meta.getterProps) {
// decorated get methods
if (prop.getterName != null) {
const visible = entity[prop.getterName] instanceof Function && isVisible(meta, prop.name, options);
if (visible) {
ret[this.propertyName(meta, prop.name)] = this.processProperty(prop.getterName, entity, options);
}
}
else {
// decorated getters
const visible = typeof entity[prop.name] !== 'undefined' && isVisible(meta, prop.name, options);
if (visible) {
ret[this.propertyName(meta, prop.name)] = this.processProperty(prop.name, entity, options);
}
}
}
return ret;
}
static propertyName(meta, prop) {
/* v8 ignore next */
if (meta.properties[prop]?.serializedName) {
return meta.properties[prop].serializedName;
}
if (meta.properties[prop]?.primary && meta.serializedPrimaryKey) {
return meta.serializedPrimaryKey;
}
return prop;
}
static processProperty(prop, entity, options) {
const parts = prop.split('.');
prop = parts[0];
const wrapped = helper(entity);
const property = wrapped.__meta.properties[prop] ?? { name: prop };
const serializer = property?.serializer;
const value = entity[prop];
// getter method
if (entity[prop] instanceof Function) {
const returnValue = entity[prop]();
if (!options.ignoreSerializers && serializer) {
return serializer(returnValue, this.extractChildOptions(options, prop));
}
return returnValue;
}
/* v8 ignore next */
if (!options.ignoreSerializers && serializer) {
return serializer(value);
}
if (Utils.isCollection(value)) {
return this.processCollection(property, entity, options);
}
if (Utils.isEntity(value, true)) {
return this.processEntity(property, entity, wrapped.__platform, options);
}
if (Utils.isScalarReference(value)) {
return value.unwrap();
}
/* v8 ignore next */
if (property?.kind === ReferenceKind.EMBEDDED) {
if (Array.isArray(value)) {
return value.map(item => helper(item).toJSON());
}
if (Utils.isObject(value)) {
return helper(value).toJSON();
}
}
if (property.customType) {
return this.processCustomType(value, property, wrapped.__platform, options.convertCustomTypes);
}
return wrapped.__platform.normalizePrimaryKey(value);
}
static processCustomType(value, prop, platform, convertCustomTypes) {
if (!prop.customType) {
return value;
}
if (convertCustomTypes) {
return prop.customType.convertToDatabaseValue(value, platform, { mode: 'serialization' });
}
return prop.customType.toJSON(value, platform);
}
static extractChildOptions(options, prop) {
return {
...options,
populate: Array.isArray(options.populate)
? Utils.extractChildElements(options.populate, prop, '*')
: options.populate,
exclude: Array.isArray(options.exclude) ? Utils.extractChildElements(options.exclude, prop) : options.exclude,
fields: Array.isArray(options.fields) ? this.extractChildFields(options.fields, prop) : options.fields,
};
}
/**
* Extracts the nested `fields` whitelist for a child property. A bare parent name (`fields: ['books']`) or a
* wildcard removes the whitelist on the sub-tree (everything is included), so consumers don't have to repeat
* every field of the child. Otherwise dot-paths are stripped of the parent prefix and passed down.
*/
static extractChildFields(fields, prop) {
const out = [];
const dotPrefix = prop + '.';
for (const field of fields) {
if (field === prop || field === '*') {
return undefined;
}
if (field.startsWith(dotPrefix)) {
out.push(field.substring(dotPrefix.length));
}
}
return out;
}
static processEntity(prop, entity, platform, options) {
const child = Reference.unwrapReference(entity[prop.name]);
const wrapped = helper(child);
const populated = isPopulated(prop.name, options) && wrapped.isInitialized();
const expand = populated || !wrapped.__managed;
const meta = wrapped.__meta;
const childOptions = this.extractChildOptions(options, prop.name);
const visible = meta.primaryKeys.filter(prop => isVisible(meta, prop, childOptions));
if (expand) {
return this.serialize(child, childOptions);
}
const pk = this.processCustomType(wrapped.getPrimaryKey(), prop, wrapped.__platform, options.convertCustomTypes);
if (options.forceObject ?? wrapped.__config.get('serialization').forceObject) {
return Utils.primaryKeyToObject(meta, pk, visible);
}
if (Utils.isPlainObject(pk)) {
const pruned = Utils.primaryKeyToObject(meta, pk, visible);
if (visible.length === 1) {
return platform.normalizePrimaryKey(pruned[visible[0]]);
}
return pruned;
}
return platform.normalizePrimaryKey(pk);
}
static processCollection(prop, entity, options) {
const col = entity[prop.name];
if (!col.isInitialized()) {
return undefined;
}
return col.getItems(false).map(item => {
const populated = isPopulated(prop.name, options);
const wrapped = helper(item);
if (populated || !wrapped.__managed) {
return this.serialize(item, this.extractChildOptions(options, prop.name));
}
const pk = this.processCustomType(wrapped.getPrimaryKey(), prop, wrapped.__platform, options.convertCustomTypes);
if (options.forceObject ?? wrapped.__config.get('serialization').forceObject) {
return Utils.primaryKeyToObject(wrapped.__meta, pk);
}
return pk;
});
}
}
/**
* Converts entity instance to POJO, converting the `Collection`s to arrays and unwrapping the `Reference` wrapper, while respecting the serialization options.
* This method accepts either a single entity or an array of entities, and returns the corresponding POJO or an array of POJO.
* To serialize a single entity, you can also use `wrap(entity).serialize()` which handles a single entity only.
*
* ```ts
* const dtos = serialize([user1, user, ...], { exclude: ['id', 'email'], forceObject: true });
* const [dto2, dto3] = serialize([user2, user3], { exclude: ['id', 'email'], forceObject: true });
* const dto1 = serialize(user, { exclude: ['id', 'email'], forceObject: true });
* const dto2 = wrap(user).serialize({ exclude: ['id', 'email'], forceObject: true });
* ```
*/
export function serialize(entities, options) {
if (Array.isArray(entities)) {
return entities.map(e => EntitySerializer.serialize(e, options));
}
return EntitySerializer.serialize(entities, options);
}