Source: lib/metamodel/EntityType.js

'use strict';

const binding = require('../binding');

const SingularAttribute = require('./SingularAttribute');
const BasicType = require('./BasicType');
const Type = require('./Type');
const ManagedType = require('./ManagedType');
const Permission = require('../util/Permission');
const Metadata = require('../util/Metadata');

/**
 * @alias metamodel.EntityType
 * @extends metamodel.ManagedType
 */
class EntityType extends ManagedType {
  /**
   * @inheritDoc
   * @return {Type.PersistenceType}
   */
  get persistenceType() {
    return Type.PersistenceType.ENTITY;
  }

  /**
   * @type metamodel.SingularAttribute
   */
  get id() {
    return this.declaredId || this.superType.id;
  }

  /**
   * @type metamodel.SingularAttribute
   */
  get version() {
    return this.declaredVersion || this.superType.version;
  }

  /**
   * @type metamodel.SingularAttribute
   */
  get acl() {
    return this.declaredAcl || this.superType.acl;
  }

  /**
   * @param {string} ref
   * @param {metamodel.EntityType} superType
   * @param {Class<binding.Entity>=} typeConstructor
   */
  constructor(ref, superType, typeConstructor) {
    super(ref, typeConstructor);

    /** @type metamodel.SingularAttribute */
    this.declaredId = null;
    /** @type metamodel.SingularAttribute */
    this.declaredVersion = null;
    /** @type metamodel.SingularAttribute */
    this.declaredAcl = null;
    /** @type metamodel.EntityType */
    this.superType = superType;

    /** @type util.Permission */
    this.loadPermission = new Permission();
    /** @type util.Permission */
    this.updatePermission = new Permission();
    /** @type util.Permission */
    this.deletePermission = new Permission();
    /** @type util.Permission */
    this.queryPermission = new Permission();
    /** @type util.Permission */
    this.schemaSubclassPermission = new Permission();
    /** @type util.Permission */
    this.insertPermission = new Permission();
  }

  /**
   * @inheritDoc
   */
  createProxyClass() {
    let Class = this.superType.typeConstructor;
    if (Class === Object) {
      switch (this.name) {
        case 'User':
          Class = binding.User;
          break;
        case 'Role':
          Class = binding.Role;
          break;
        default:
          Class = binding.Entity;
          break;
      }
    }

    return this.enhancer.createProxy(Class);
  }

  /**
   * Gets all on this class referencing attributes
   *
   * @param {EntityManager} db The instances will be found by this EntityManager
   * @param {Object} [options] Some options to pass
   * @param {Array.<string>} [options.classes] An array of class names to filter for, null for no filter
   * @return {Map.<metamodel.ManagedType, Set.<string>>} A map from every referencing class to a set of its referencing
   * attribute names
   */
  getReferencing(db, options) {
    const opts = Object.assign({}, options);
    const entities = db.metamodel.entities;
    const referencing = new Map();

    const names = Object.keys(entities);
    for (let i = 0, len = names.length; i < len; i += 1) {
      const name = names[i];
      // Skip class if not in class filter
      if (!opts.classes || opts.classes.indexOf(name) !== -1) {
        const entity = entities[name];
        const iter = entity.attributes();
        for (let el = iter.next(); !el.done; el = iter.next()) {
          const attr = el.value;
          // Filter only referencing singular and collection attributes
          if (attr.type === this || attr.elementType === this) {
            const typeReferences = referencing.get(attr.declaringType) || new Set();
            typeReferences.add(attr.name);
            referencing.set(attr.declaringType, typeReferences);
          }
        }
      }
    }

    return referencing;
  }

  /**
   * @inheritDoc
   */
  createObjectFactory(db) {
    switch (this.name) {
      case 'User':
        return binding.UserFactory.create(this, db);
      case 'Device':
        return binding.DeviceFactory.create(this, db);
      case 'Object':
        return undefined;
      default:
        return binding.EntityFactory.create(this, db);
    }
  }

  /**
   * @param {util.Metadata} state The root object state, can be <code>null</code> if a currentObject is provided
   * @param {json} jsonObject The json data to merge
   * @param {*} currentObject The object where the jsonObject will be merged into, if the current object is null,
   * a new instance will be created
   * @param {Object=} options The options used to apply the json
   * @param {boolean} [options.persisting=false] indicates if the current state will be persisted.
   * Used to update the internal change tracking state of collections and mark the object persistent or dirty afterwards
   * @param {boolean} [options.onlyMetadata=false] Indicates if only the metadata should be updated
   * @return {*} The merged entity instance
   */
  fromJsonValue(state, jsonObject, currentObject, options) {
    // handle references
    if (typeof jsonObject === 'string') {
      return state.db.getReference(jsonObject);
    }

    if (!jsonObject || typeof jsonObject !== 'object') {
      return null;
    }

    const opt = Object.assign({
      persisting: false,
      onlyMetadata: false,
    }, options);

    let obj;
    let objectState;
    if (currentObject) {
      const currentObjectState = Metadata.get(currentObject);
      // merge state into the current object if:
      // 1. The provided json does not contains an id and we have an already created object for it
      // 2. The object was created without an id and was later fetched from the server (e.g. User/Role)
      // 3. The provided json has the same id as the current object, they can differ on embedded json for a reference
      if (!jsonObject.id || !currentObjectState.id || jsonObject.id === currentObjectState.id) {
        obj = currentObject;
        objectState = currentObjectState;
      }
    }

    if (!obj) {
      obj = state.db.getReference(this.typeConstructor, jsonObject.id);
      objectState = Metadata.get(obj);
    }

    // deserialize our properties
    objectState.enable(false);
    super.fromJsonValue(objectState, jsonObject, obj, opt);
    objectState.enable(true);

    if (opt.persisting) {
      objectState.setPersistent();
    } else if (!opt.onlyMetadata) {
      objectState.setDirty();
    }

    return obj;
  }

  /**
   * Converts the given object to json
   * @param {util.Metadata} state The root object state
   * @param {*} object The object to convert
   * @param {Object} [options=false] to json options by default excludes the metadata
   * @param {boolean} [options.excludeMetadata=false] Excludes the metadata form the serialized json
   * @param {number|boolean} [options.depth=0] Includes up to depth referenced objects into the serialized json
   * @param {boolean} [options.persisting=false] indicates if the current state will be persisted.
   *  Used to update the internal change tracking state of collections and mark the object persistent if its true
   * @return {json} JSON-Object
   */
  toJsonValue(state, object, options) {
    const opt = Object.assign({
      excludeMetadata: false,
      depth: 0,
      persisting: false,
    }, options);

    const isInDepth = opt.depth === true || opt.depth > -1;

    // check if object is already loaded in state
    const objectState = object && Metadata.get(object);
    if (isInDepth && objectState && objectState.isAvailable) {
      // serialize our properties
      objectState.enable(false);
      const json = super.toJsonValue(objectState, object, Object.assign({}, opt, {
        depth: opt.depth === true ? true : opt.depth - 1,
      }));
      objectState.enable(true);

      return json;
    }

    if (object instanceof this.typeConstructor) {
      object.attach(state.db);
      return object.id;
    }

    return null;
  }

  toString() {
    return 'EntityType(' + this.ref + ')';
  }

  toJSON() {
    const json = super.toJSON();

    json.acl.schemaSubclass = this.schemaSubclassPermission;
    json.acl.insert = this.insertPermission;
    json.acl.update = this.updatePermission;
    json.acl.delete = this.deletePermission;
    json.acl.query = this.queryPermission;

    return json;
  }
}

/**
 * @alias metamodel.EntityType.Object
 * @extends metamodel.EntityType
 */
class ObjectType extends EntityType {
  static get ref() {
    return '/db/Object';
  }

  constructor() {
    super(EntityType.Object.ref, null, Object);

    this.declaredId = new class extends SingularAttribute {
      constructor() {
        super('id', BasicType.String, true);
      }

      getJsonValue(state) {
        return state.id || undefined;
      }

      setJsonValue(state, object, jsonValue) {
        if (!this.id) {
          state.id = jsonValue;
        }
      }
    }();
    this.declaredId.init(this, 0);
    this.declaredId.isId = true;

    this.declaredVersion = new class extends SingularAttribute {
      constructor() {
        super('version', BasicType.Integer, true);
      }

      getJsonValue(state) {
        return state.version || undefined;
      }

      setJsonValue(state, object, jsonValue) {
        if (jsonValue) {
          state.version = jsonValue;
        }
      }
    }();
    this.declaredVersion.init(this, 1);
    this.declaredVersion.isVersion = true;

    this.declaredAcl = new class extends SingularAttribute {
      constructor() {
        super('acl', BasicType.JsonObject, true);
      }

      getJsonValue(state) {
        return state.acl.toJSON();
      }

      setJsonValue(state, object, jsonValue) {
        state.acl.fromJSON(jsonValue || {});
      }
    }();

    this.declaredAcl.init(this, 2);
    this.declaredAcl.isAcl = true;

    this.declaredAttributes = [this.declaredId, this.declaredVersion, this.declaredAcl];
  }
}

EntityType.Object = ObjectType;

module.exports = EntityType;