mikro-orm/mikro-orm

View on GitHub
packages/core/src/serialization/EntityTransformer.ts

Summary

Maintainability
B
5 hrs
Test Coverage
A
100%
import type { Collection } from '../entity/Collection';
import type { AnyEntity, Dictionary, EntityDTO, EntityKey, EntityMetadata, EntityValue, IPrimaryKey } from '../typings';
import { helper, wrap } from '../entity/wrap';
import type { Platform } from '../platforms';
import { Utils } from '../utils/Utils';
import { ReferenceKind } from '../enums';
import type { Reference } from '../entity/Reference';
import { SerializationContext } from './SerializationContext';
import { RawQueryFragment } from '../utils/RawQueryFragment';

function isVisible<Entity extends object>(meta: EntityMetadata<Entity>, propName: EntityKey<Entity>, ignoreFields: string[] = []): boolean {
  const prop = meta.properties[propName];
  const visible = prop && !prop.hidden;
  const prefixed = prop && !prop.primary && propName.startsWith('_'); // ignore prefixed properties, if it's not a PK

  return visible && !prefixed && !ignoreFields.includes(propName);
}

export class EntityTransformer {

  static toObject<Entity extends object, Ignored extends EntityKey<Entity> = never>(entity: Entity, ignoreFields: Ignored[] = [], raw = false): Omit<EntityDTO<Entity>, Ignored> {
    if (!Array.isArray(ignoreFields)) {
      ignoreFields = [];
    }

    const wrapped = helper(entity);
    let contextCreated = false;

    if (!wrapped.__serializationContext.root) {
      const root = new SerializationContext<Entity>(
        wrapped.__config,
        wrapped.__serializationContext.populate,
        wrapped.__serializationContext.fields,
        wrapped.__serializationContext.exclude,
      );
      SerializationContext.propagate(root, entity, isVisible);
      contextCreated = true;
    }

    const root = wrapped.__serializationContext.root!;
    const meta = wrapped.__meta;
    const ret = {} as Dictionary;
    const keys = new Set<EntityKey<Entity>>();

    if (meta.serializedPrimaryKey && !meta.compositePK) {
      keys.add(meta.serializedPrimaryKey);
    } else {
      meta.primaryKeys.forEach(pk => keys.add(pk));
    }

    if (wrapped.isInitialized() || !wrapped.hasPrimaryKey()) {
      Utils.keys(entity as object).forEach(prop => keys.add(prop));
    }

    const visited = root.visited.has(entity);

    if (!visited) {
      root.visited.add(entity);
    }

    [...keys]
      .filter(prop => raw ? meta.properties[prop] : isVisible<Entity>(meta, prop, ignoreFields))
      .map(prop => {
        const populated = root.isMarkedAsPopulated(meta.className, prop);
        const partiallyLoaded = root.isPartiallyLoaded(meta.className, prop);
        const isPrimary = wrapped.__config.get('serialization').includePrimaryKeys && meta.properties[prop].primary;

        if (!partiallyLoaded && !populated && !isPrimary) {
          return [prop, undefined];
        }

        const cycle = root.visit(meta.className, prop);

        if (cycle && visited) {
          return [prop, undefined];
        }

        const val = EntityTransformer.processProperty<Entity>(prop, entity, raw, populated);

        if (!cycle) {
          root.leave(meta.className, prop);
        }

        if (val instanceof RawQueryFragment) {
          throw new Error(`Trying to serialize raw SQL fragment: '${val.sql}'`);
        }

        return [prop, val] as const;
      })
      .filter(([, value]) => typeof value !== 'undefined')
      .forEach(([prop, value]) => ret[this.propertyName(meta, prop!, wrapped.__platform, raw) as any] = value as any);

    if (!visited) {
      root.visited.delete(entity);
    }

    if (!wrapped.isInitialized() && wrapped.hasPrimaryKey()) {
      return ret as EntityDTO<Entity>;
    }

    // decorated getters
    meta.props
      .filter(prop => prop.getter && prop.getterName === undefined && !prop.hidden && typeof entity[prop.name] !== 'undefined')
      // @ts-ignore
      .forEach(prop => ret[this.propertyName(meta, prop.name, wrapped.__platform, raw) as any] = this.processProperty(prop.name, entity, raw));

    // decorated get methods
    meta.props
      .filter(prop => prop.getterName && !prop.hidden && entity[prop.getterName] instanceof Function)
      // @ts-ignore
      .forEach(prop => ret[this.propertyName(meta, prop.name, wrapped.__platform, raw)] = this.processProperty(prop.getterName as keyof Entity & string, entity, raw));

    if (contextCreated) {
      root.close();
    }

    return ret as EntityDTO<Entity>;
  }

  private static propertyName<Entity>(meta: EntityMetadata<Entity>, prop: EntityKey<Entity>, platform?: Platform, raw?: boolean): EntityKey<Entity> {
    if (raw) {
      return prop;
    }

    if (meta.properties[prop].serializedName) {
      return meta.properties[prop].serializedName as EntityKey<Entity>;
    }

    if (meta.properties[prop].primary && platform) {
      return platform.getSerializedPrimaryKeyField(prop) as EntityKey<Entity>;
    }

    return prop;
  }

  private static processProperty<Entity extends object>(prop: EntityKey<Entity>, entity: Entity, raw: boolean, populated: boolean): EntityValue<Entity> | undefined {
    const wrapped = helper(entity);
    const property = wrapped.__meta.properties[prop];
    const serializer = property?.serializer;
    const value = entity[prop];

    // getter method
    if (entity[prop] as unknown instanceof Function) {
      const returnValue = (entity[prop] as unknown as () => Entity[keyof Entity & string])();
      if (serializer && !raw) {
        return serializer(returnValue);
      }

      return returnValue as EntityValue<Entity>;
    }

    if (serializer && !raw) {
      return serializer(value);
    }

    if (Utils.isCollection(value)) {
      return EntityTransformer.processCollection(prop, entity, raw, populated);
    }

    if (Utils.isEntity(value, true)) {
      return EntityTransformer.processEntity(prop, entity, wrapped.__platform, raw, populated);
    }

    if (Utils.isScalarReference(value)) {
      return value.unwrap();
    }

    if (property.kind === ReferenceKind.EMBEDDED) {
      if (Array.isArray(value)) {
        return (value as object[]).map(item => {
          const wrapped = item && helper(item);
          return wrapped ? wrapped.toJSON() : item;
        }) as EntityValue<Entity>;
      }

      const wrapped = value && helper(value!);
      return wrapped ? wrapped.toJSON() as EntityValue<Entity> : value;
    }

    const customType = property?.customType;

    if (customType) {
      return customType.toJSON(value, wrapped.__platform);
    }

    if (property?.primary) {
      return wrapped.__platform.normalizePrimaryKey(value as unknown as IPrimaryKey) as unknown as EntityValue<Entity>;
    }

    return value;
  }

  private static processEntity<Entity extends object>(prop: keyof Entity, entity: Entity, platform: Platform, raw: boolean, populated: boolean): EntityValue<Entity> | undefined {
    const child = entity[prop] as unknown as Entity | Reference<Entity>;
    const wrapped = helper(child as Entity);
    const meta = wrapped.__meta;
    const visible = meta.primaryKeys.filter(prop => isVisible(meta, prop));

    if (raw && wrapped.isInitialized() && child !== entity) {
      return wrapped.toPOJO() as unknown as EntityValue<Entity>;
    }

    function isPopulated() {
      if (wrapped.__populated != null) {
        return wrapped.__populated;
      }

      if (populated) {
        return true;
      }

      return !wrapped.__managed;
    }

    if (wrapped.isInitialized() && isPopulated() && child !== entity) {
      return wrap(child).toJSON() as EntityValue<Entity>;
    }

    const pk = wrapped.getPrimaryKey(true)!;

    if (wrapped.__config.get('serialization').forceObject) {
      return Utils.primaryKeyToObject(meta, pk, visible) as EntityValue<Entity>;
    }

    if (Utils.isPlainObject(pk)) {
      const pruned = Utils.primaryKeyToObject(meta, pk, visible) as EntityValue<Entity>;

      if (visible.length === 1) {
        return platform.normalizePrimaryKey(pruned[visible[0]] as IPrimaryKey) as EntityValue<Entity>;
      }

      return pruned;
    }

    return platform.normalizePrimaryKey(pk as IPrimaryKey) as EntityValue<Entity>;
  }

  private static processCollection<Entity extends object>(prop: keyof Entity, entity: Entity, raw: boolean, populated: boolean): EntityValue<Entity> | undefined {
    const col = entity[prop] as Collection<AnyEntity>;

    if (raw && col.isInitialized(true)) {
      return col.map(item => helper(item).toPOJO()) as EntityValue<Entity>;
    }

    if (col.shouldPopulate(populated)) {
      return col.toArray() as EntityValue<Entity>;
    }

    if (col.isInitialized()) {
      const wrapped = helper(entity);

      if (wrapped.__config.get('serialization').forceObject) {
        return col.map(item => {
          const wrapped = helper(item);
          return Utils.primaryKeyToObject(wrapped.__meta, wrapped.getPrimaryKey(true)!) as EntityValue<Entity>;
        }) as EntityValue<Entity>;
      }

      return col.map(i => helper(i).getPrimaryKey(true)) as EntityValue<Entity>;
    }

    return undefined;
  }

}