Source: lib/metamodel/ModelBuilder.js

'use strict';

const BasicType = require('./BasicType');
const EntityType = require('./EntityType');
const EmbeddableType = require('./EmbeddableType');

const ListAttribute = require('./ListAttribute');
const MapAttribute = require('./MapAttribute');
const SetAttribute = require('./SetAttribute');
const SingularAttribute = require('./SingularAttribute');

const PersistentError = require('../error/PersistentError');

/**
 * @alias metamodel.ModelBuilder
 */
class ModelBuilder {
  constructor() {
    /** @type Object<string,metamodel.ManagedType> */
    this.models = {};

    /** @type Object<string,Object> */
    this.modelDescriptors = null;

    Object.keys(BasicType).forEach((typeName) => {
      const basicType = BasicType[typeName];
      if (basicType instanceof BasicType) {
        this.models[basicType.ref] = basicType;
      }
    });
  }

  /**
   * @param {string} ref
   * @return {metamodel.ManagedType}
   */
  getModel(ref) {
    if (ref in this.models) {
      return this.models[ref];
    }

    const model = this.buildModel(ref);
    this.models[ref] = model;
    return model;
  }

  /**
   * @param {Object[]} modelDescriptors
   * @return {Object<string,metamodel.ManagedType>}
   */
  buildModels(modelDescriptors) {
    this.modelDescriptors = {};

    modelDescriptors.forEach((modelDescriptor) => {
      this.modelDescriptors[modelDescriptor.class] = modelDescriptor;
    });

    Object.keys(this.modelDescriptors).forEach((ref) => {
      try {
        const model = this.getModel(ref);
        this.buildAttributes(model);
      } catch (e) {
        throw new PersistentError('Can\'t create model for entity class ' + ref, e);
      }
    });

    // ensure at least an object entity
    this.getModel(EntityType.Object.ref);

    return this.models;
  }

  /**
   * @param {string} ref
   * @return {metamodel.ManagedType}
   */
  buildModel(ref) {
    const modelDescriptor = this.modelDescriptors[ref];
    let type;
    if (ref === EntityType.Object.ref) {
      type = new EntityType.Object();
    } else if (modelDescriptor) {
      if (modelDescriptor.embedded) {
        type = new EmbeddableType(ref);
      } else {
        const superTypeIdentifier = modelDescriptor.superClass || EntityType.Object.ref;
        type = new EntityType(ref, this.getModel(superTypeIdentifier));
      }
    } else {
      throw new TypeError('No model available for ' + ref);
    }

    type.metadata = {};

    if (modelDescriptor) {
      type.metadata = modelDescriptor.metadata || {};
      const permissions = modelDescriptor.acl || {};
      Object.keys(permissions).forEach((permission) => {
        type[permission + 'Permission'].fromJSON(permissions[permission]);
      });
    }

    return type;
  }

  /**
   * @param {metamodel.EntityType} model
   * @return {void}
   */
  buildAttributes(model) {
    const modelDescriptor = this.modelDescriptors[model.ref];
    const fields = modelDescriptor.fields;

    Object.keys(fields).forEach((name) => {
      const field = fields[name];
      if (!model.getAttribute(name)) { // skip predefined attributes
        model.addAttribute(this.buildAttribute(field), field.order);
      }
    });

    if (modelDescriptor.validationCode) {
      model.validationCode = modelDescriptor.validationCode;
    }
  }

  /**
   * @param {Object} field The field metadata
   * @param {string} field.name The name of zhe field
   * @param {string} field.type The type reference of the field
   * @param {number} field.order The order number of the field
   * @param {Object<string,*>} field.metadata Additional metadata of the field
   * @return {metamodel.Attribute}
   */
  buildAttribute(field) {
    // TODO: remove readonly if createdAt and updatedAt becomes real metadata fields in the schema
    const isMetadata = field.flags && (field.flags.indexOf('METADATA') !== -1 || field.flags.indexOf('READONLY') !== -1);
    const name = field.name;
    const ref = field.type;
    if (ref.indexOf('/db/collection.') !== 0) {
      const singularAttribute = new SingularAttribute(name, this.getModel(ref), isMetadata);
      singularAttribute.metadata = field.metadata;
      return singularAttribute;
    }
    const collectionType = ref.substring(0, ref.indexOf('['));
    const elementType = ref.substring(ref.indexOf('[') + 1, ref.indexOf(']')).trim();

    switch (collectionType) {
      case ListAttribute.ref:
        return new ListAttribute(name, this.getModel(elementType));
      case SetAttribute.ref:
        return new SetAttribute(name, this.getModel(elementType));
      case MapAttribute.ref: {
        const keyType = elementType.substring(0, elementType.indexOf(',')).trim();
        const valueType = elementType.substring(elementType.indexOf(',') + 1).trim();

        return new MapAttribute(name, this.getModel(keyType), this.getModel(valueType));
      }
      default:
        throw new TypeError('No collection available for ' + ref);
    }
  }
}

module.exports = ModelBuilder;