ForestAdmin/forest-express-mongoose

View on GitHub
src/utils/field-analyser.js

Summary

Maintainability
D
2 days
Test Coverage
A
95%
const _ = require('lodash');
const Interface = require('forest-express');
const utils = require('../utils/schema');

class FieldAnalyser {
  constructor(model, lianaOptions) {
    this.model = model;
    this.lianaOptions = lianaOptions;
  }

  _formatRef(ref) {
    const referenceModel = utils.getReferenceModel(this.lianaOptions, ref);
    if (referenceModel) {
      return utils.getModelName(referenceModel);
    }
    Interface.logger.warn(`Cannot find the reference "${ref}" on the model "${this.model.modelName}".`);
    return null;
  }

  _detectReference(fieldInfo) {
    if (fieldInfo.options) {
      if (fieldInfo.options.ref && fieldInfo.options.type) {
        const ref = this._formatRef(fieldInfo.options.ref);
        return ref ? `${ref}._id` : null;
      }
      if (_.isArray(fieldInfo.options.type) && fieldInfo.options.type.length
        && fieldInfo.options.type[0].ref && fieldInfo.options.type[0].type) {
        const ref = this._formatRef(fieldInfo.options.type[0].ref);
        return ref ? `${ref}._id` : null;
      }
    }
    return null;
  }

  _objectType(fieldsInfo, getType) {
    const type = { fields: [] };

    Object.keys(fieldsInfo).forEach((fieldName) => {
      const fieldInfo = fieldsInfo[fieldName];
      const field = {
        field: fieldName,
        type: getType(fieldName),
      };

      if (fieldName === '_id') {
        field.isPrimaryKey = true;
      }

      if (!field.type) { return; }

      const ref = this._detectReference(fieldInfo);
      if (ref) { field.reference = ref; }

      if (fieldInfo.enumValues && fieldInfo.enumValues.length) {
        field.enums = fieldInfo.enumValues;
      }

      if (fieldInfo.enum
        && Array.isArray(fieldInfo.enum)
        && fieldInfo.enum.length) {
        field.enums = fieldInfo.enum;
      }

      type.fields.push(field);
    });

    return type;
  }

  _getTypeFromNative(type) {
    if (type instanceof Array) {
      if (_.isEmpty(type)) {
        return [null];
      }
      return [this._getTypeFromNative(type[0].type || type[0])];
    }
    if (_.isPlainObject(type)) {
      if (_.isEmpty(type)) { return 'Json'; }

      if (type.type) {
        if (type.enum) {
          // NOTICE: Detect enum values for Enums in subdocument arrays.
          return 'Enum';
        }
        return this._getTypeFromNative(type.type);
      }
      return this._objectType(type, (key) => this._getTypeFromNative(type[key]));
    }
    if (_.isFunction(type) && type.name === 'ObjectId') {
      return 'String';
    }
    if (type instanceof this.lianaOptions.Mongoose.Schema) {
      return this._schemaType(type);
    }

    switch (type) {
      case String:
        return 'String';
      case Boolean:
        return 'Boolean';
      case Number:
        return 'Number';
      case Date:
        return 'Date';
      default:
        return null;
    }
  }

  _getTypeFromMongoose(fieldInfo) {
    if (_.isPlainObject(fieldInfo) && !fieldInfo.path) {
      // Deal with Object
      return this._objectType(
        fieldInfo,
        (fieldName) => this._getTypeFromMongoose(fieldInfo[fieldName]),
      );
    }
    if (fieldInfo.instance === 'Array') {
      if (_.isEmpty(fieldInfo.options.type) && !_.isUndefined(fieldInfo.options.type)) {
        return 'Json';
      }

      // Deal with Array
      if (fieldInfo.caster.instance && (fieldInfo.caster.options.ref
        || _.keys(fieldInfo.caster.options).length === 0)) {
        return [this._getTypeFromMongoose(fieldInfo.caster)];
      }
      if (fieldInfo.options.type[0] instanceof this.lianaOptions.Mongoose.Schema) {
        // Schema
        return [this._schemaType(fieldInfo.options.type[0])];
      }

      // NOTICE: Object with `type` reserved keyword.
      //         See: https://mongoosejs.com/docs/schematypes.html#type-key
      if (fieldInfo.options.type[0] instanceof Object
        && fieldInfo.options.type[0].type
        // NOTICE: Bypass for schemas like `[{ type: {type: String}, ... }]` where "type" is used
        //         as property, and thus we are in the case of an array of embedded documents.
        //         See: https://mongoosejs.com/docs/faq.html#type-key
        && !fieldInfo.options.type[0].type.type) {
        return [this._getTypeFromNative(fieldInfo.options.type[0])];
      }

      // Object
      return [this._objectType(fieldInfo.options.type[0], (key) =>
        this._getTypeFromNative(fieldInfo.options.type[0][key]))];
    }
    if (fieldInfo.enumValues && fieldInfo.enumValues.length) {
      return 'Enum';
    }
    if (fieldInfo.instance === 'ObjectID' || fieldInfo.instance === 'ObjectId') {
      // Deal with ObjectID
      return 'String';
    }
    if (fieldInfo.instance === 'Embedded') {
      return this._objectType(fieldInfo.schema.obj, (fieldName) =>
        this._getTypeFromNative(fieldInfo.schema.obj[fieldName]));
    }
    if (fieldInfo.instance === 'Mixed') {
      // Deal with Mixed object

      // NOTICE: Object and {} are detected as Json type as they don't have schema.
      if (_.isEmpty(fieldInfo.options.type) && !_.isUndefined(fieldInfo.options.type)) {
        return 'Json';
      }
      if (_.isEmpty(fieldInfo.options) && !_.isUndefined(fieldInfo.options)) {
        return 'Json';
      }

      return null;
    }
    // Deal with primitive type
    return fieldInfo.instance
      || (fieldInfo.options && this._getTypeFromNative(fieldInfo.options.type))
      || null;
  }

  _schemaType(type) {
    return {
      fields: _.map(type.paths, (fieldType, fieldName) => {
        const field = {
          field: fieldName,
          type: this._getTypeFromMongoose(fieldType),
        };

        if (fieldName === '_id') {
          field.isPrimaryKey = true;
        }

        if (fieldType.enumValues && fieldType.enumValues.length) {
          field.enums = fieldType.enumValues;
        }

        return field;
      }),
    };
  }

  _getRequired(fieldInfo) {
    return fieldInfo.isRequired === true
      || (
        fieldInfo.path === '_id'
        && !fieldInfo.options.auto
        && fieldInfo.options.type !== this.lianaOptions.Mongoose.ObjectId
      );
  }

  static _getValidations(fieldInfo) {
    const validations = [];

    if (fieldInfo.validators && fieldInfo.validators.length > 0) {
      _.each(fieldInfo.validators, (validator) => {
        if (validator.type === 'required') {
          validations.push({
            type: 'is present',
          });
        }

        if (validator.type === 'minlength') {
          validations.push({
            type: 'is longer than',
            value: validator.minlength,
          });
        }

        if (validator.type === 'maxlength') {
          validations.push({
            type: 'is shorter than',
            value: validator.maxlength,
          });
        }

        if (validator.type === 'min') {
          validations.push({
            type: 'is greater than',
            value: validator.min,
          });
        }

        if (validator.type === 'max') {
          validations.push({
            type: 'is less than',
            value: validator.max,
          });
        }
      });
    }

    return validations;
  }

  getFieldSchema(path, fieldInfo) {
    const schema = { field: path, type: this._getTypeFromMongoose(fieldInfo) };

    const ref = this._detectReference(fieldInfo);
    if (ref) { schema.reference = ref; }

    if (fieldInfo.enumValues && fieldInfo.enumValues.length) {
      schema.enums = fieldInfo.enumValues;
    }

    // NOTICE: Create enums from caster (for ['Enum'] type).
    if (fieldInfo.caster && fieldInfo.caster.enumValues && fieldInfo.caster.enumValues.length) {
      schema.enums = fieldInfo.caster.enumValues;
    }

    const isRequired = this._getRequired(fieldInfo);
    if (isRequired) {
      schema.isRequired = isRequired;
    }

    if (schema.field === '_id') {
      schema.isPrimaryKey = true;
    }

    if (fieldInfo.options && !_.isNull(fieldInfo.options.default)
      && !_.isUndefined(fieldInfo.options.default)
      && !_.isFunction(fieldInfo.options.default)) {
      schema.defaultValue = fieldInfo.options.default;
    }

    schema.validations = FieldAnalyser._getValidations(fieldInfo);

    if (schema.validations.length === 0) {
      delete schema.validations;
    }

    return schema;
  }
}

module.exports = FieldAnalyser;