ForestAdmin/forest-express-sequelize

View on GitHub
src/services/apimap-field-builder.js

Summary

Maintainability
F
4 days
Test Coverage
B
88%
const _ = require('lodash');
const ApimapFieldTypeDetector = require('./apimap-field-type-detector');

function ApimapFieldBuilder(model, column, options) {
  const DataTypes = options.Sequelize;

  function isRequired() {
    // eslint-disable-next-line
    return column._autoGenerated !== true 
      && (column.allowNull === false || Boolean(column.primaryKey));
  }

  function getValidations(automaticValue) {
    const validations = [];

    // NOTICE: Do not inspect validation for autogenerated fields, it would
    //         block the record creation/update.
    // eslint-disable-next-line
    if (automaticValue || column._autoGenerated === true) {
      return validations;
    }

    if (column.allowNull === false) {
      validations.push({
        type: 'is present',
      });
    }

    if (!column.validate) { return validations; }

    if (column.validate.min) {
      validations.push({
        type: 'is greater than',
        value: column.validate.min.args || column.validate.min,
        message: column.validate.min.msg,
      });
    }

    if (column.validate.max) {
      validations.push({
        type: 'is less than',
        value: column.validate.max.args || column.validate.max,
        message: column.validate.max.msg,
      });
    }

    if (column.validate.isBefore) {
      validations.push({
        type: 'is before',
        value: column.validate.isBefore.args || column.validate.isBefore,
        message: column.validate.isBefore.msg,
      });
    }

    if (column.validate.isAfter) {
      validations.push({
        type: 'is after',
        value: column.validate.isAfter.args || column.validate.isAfter,
        message: column.validate.isAfter.msg,
      });
    }

    if (column.validate.len) {
      const length = column.validate.len.args || column.validate.len;

      if (_.isArray(length) && !_.isNull(length[0]) && !_.isUndefined(length[0])) {
        validations.push({
          type: 'is longer than',
          value: length[0],
          message: column.validate.len.msg,
        });

        if (length[1]) {
          validations.push({
            type: 'is shorter than',
            value: length[1],
            message: column.validate.len.msg,
          });
        }
      } else {
        validations.push({
          type: 'is longer than',
          value: length,
          message: column.validate.len.msg,
        });
      }
    }

    if (column.validate.contains) {
      validations.push({
        type: 'contains',
        value: column.validate.contains.args || column.validate.contains,
        message: column.validate.contains.msg,
      });
    }

    if (column.validate.is && !_.isArray(column.validate.is)) {
      const value = column.validate.is.args || column.validate.is;

      validations.push({
        type: 'is like',
        value: value.toString(),
        message: column.validate.is.msg,
      });
    }

    return validations;
  }


  // NOTICE: Remove Sequelize.Utils.Literal wrapper to display actual value in UI.
  //         Keep only simple values, and hide expressions.
  //         Do not export literal values to UI by default.
  function unwrapLiteral(literalValue, columnType) {
    let value;

    if (_.isString(literalValue)) {
      if (['true', 'false'].includes(literalValue.toLowerCase())) {
        value = Boolean(literalValue);
      } else if (!_.isNaN(_.toNumber(literalValue))) {
        if (columnType instanceof DataTypes.NUMBER) {
          value = _.toNumber(literalValue);
        } else {
          value = literalValue;
        }
        // NOTICE: Only single quotes are widely considered valid to delimitate string values.
      } else if (literalValue.match(/^'.*'$/)) {
        value = literalValue.substring(1, literalValue.length - 1);
      }
    } else if (_.isBoolean(literalValue) || _.isNumber(literalValue)) {
      value = literalValue;
    }

    return value;
  }

  this.perform = () => {
    const schema = {
      field: column.fieldName,
      type: new ApimapFieldTypeDetector(column, options).perform(),
      // NOTICE: Necessary only for fields with different field and database
      //         column names
      columnName: column.field,
    };

    if (column.primaryKey === true) {
      schema.isPrimaryKey = true;
    }

    if (schema.type === 'Enum') {
      schema.enums = column.values;
    }

    // NOTICE: Create enums from sub-type (for ['Enum'] type).
    if (Array.isArray(schema.type) && schema.type[0] === 'Enum') {
      schema.enums = column.type.type.values;
    }

    if (isRequired()) {
      schema.isRequired = true;
    }

    const canHaveDynamicDefaultValue = ['Date', 'Dateonly'].indexOf(schema.type) !== -1
      || column.type instanceof DataTypes.UUID;
    const isDefaultValueFunction = (typeof column.defaultValue) === 'function'
      || (canHaveDynamicDefaultValue && (typeof column.defaultValue) === 'object');

    if (!_.isNull(column.defaultValue) && !_.isUndefined(column.defaultValue)) {
      // NOTICE: Prevent sequelize.Sequelize.NOW to be defined as the default value as the client
      //         does not manage it properly so far.
      if (isDefaultValueFunction) {
        schema.isRequired = false;
      // NOTICE: Do not use the primary keys default values to prevent issues with UUID fields
      //         (defaultValue: DataTypes.UUIDV4).
      } else if (!_.includes(_.keys(model.primaryKeys), column.fieldName)) {
        // FIXME: `column.defaultValue instanceof Sequelize.Utils.Literal` fails for unknown reason.
        if (_.isObject(column.defaultValue) && (column.defaultValue.constructor.name === 'Literal')) {
          schema.defaultValue = unwrapLiteral(column.defaultValue.val, column.type);
        } else {
          schema.defaultValue = column.defaultValue;
        }
      }
    }

    schema.validations = getValidations(isDefaultValueFunction);

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

    return schema;
  };
}

module.exports = ApimapFieldBuilder;