TechnologyAdvice/obey

View on GitHub
src/lib/validators.js

Summary

Maintainability
F
5 days
Test Coverage
/*
 * Copyright (c) 2015 TechnologyAdvice
 */
/* eslint no-console: 0, consistent-return: 0 */
const dot = require('dot-object')
const jexl = require('jexl')
const plugins = require('./plugins')
const cloneDeep = require('lodash/cloneDeep')

const validators = {
  /**
   * Validator default method, used by model
   * @param {Object} def The property configuration
   * @param {*} value The value being validated
   */
  default: function(def, value) {
    if (/^(number|boolean)$/.test(def.type)) {
      if (value === 0 || value === false) return value
    }
    return value || cloneDeep(def.default)
  },

  /**
   * Validator allowed method, used by model
   * @param {Object} def The property configuration
   * @param {*} value The value being validated
   * @param {string} key The key name of the property
   * @param {Array<{type: string, sub: string|number, key: string, value: *, message: string}>} errors An error array
   * to which any additional error objects will be added
   */
  allow: function(def, value, key, errors) {
    const type = 'allow'
    const sub = typeof def.allow === 'object' && !Array.isArray(def.allow) ? Object.keys(def.allow) : def.allow
    const subIsArray = Array.isArray(sub)
    if ((subIsArray && sub.indexOf(value) === -1) || (!subIsArray && sub !== value)) {
      if (value === '' && def.empty) return
      errors.push({ type, sub, key, value, message: `Value '${value}' is not allowed` })
    }
  },

  /**
   * Validator min method, used by model
   * @param {Object} def The property configuration
   * @param {*} value The value being validated
   * @param {string} key The key name of the property
   * @param {Array<{type: string, sub: string|number, key: string, value: *, message: string}>} errors An error array
   * to which any additional error objects will be added
   */
  min: function(def, value, key, errors) {
    const type = 'min'
    const sub = def.min
    if ((Array.isArray(value) || typeof value === 'string') && value.length < def.min) {
      errors.push({ type, sub, key, value, message: `Length must be greater than or equal to ${def.min}` })
    } else if (typeof value === 'number' && value < def.min) {
      errors.push({ type, sub, key, value, message: `Value must be greater than or equal to ${def.min}` })
    }
  },

  /**
   * Validator max method, used by model
   * @param {Object} def The property configuration
   * @param {*} value The value being validated
   * @param {string} key The key name of the property
   * @param {Array<{type: string, sub: string|number, key: string, value: *, message: string}>} errors An error array
   * to which any additional error objects will be added
   */
  max: function(def, value, key, errors) {
    const type = 'max'
    const sub = def.max
    if ((Array.isArray(value) || typeof value === 'string') && value.length > def.max) {
      errors.push({ type, sub, key, value, message: `Length must be less than or equal to ${def.max}` })
    } else if (typeof value === 'number' && value > def.max) {
      errors.push({ type, sub, key, value, message: `Value must be less than or equal to ${def.max}` })
    }
  },

  /**
   * Validator requiredIf method, used by model
   * @param {Object} def The property configuration
   * @param {*} value The value being validated
   * @param {string} key The key name of the property
   * @param {Array<{type: string, sub: string|number, key: string, value: *, message: string}>} errors An error array
   * to which any additional error objects will be added
   * @param {Object} data The full initial data object
   */
  requiredIf: function(def, value, key, errors, data) {
    const type = 'requiredIf'
    const sub = def.requiredIf
    if (typeof sub === 'object') {
      const field = Object.keys(sub)[0]
      const fieldArr = Array.isArray(sub[field]) ? sub[field] : [ sub[field] ]
      fieldArr.some(val => {
        /* istanbul ignore else */
        if (dot.pick(field, data) === val && value === undefined) {
          errors.push({ type, sub, key, value, message: `Value required by existing '${field}' value` })
          return true
        }
      })
    } else if (dot.pick(sub, data) !== undefined && value === undefined) {
      errors.push({ type, sub, key, value, message: `Value required because '${sub}' exists` })
    }
  },
  /**
   * Alias for requiredIf
   */
  requireIf: function(def, value, key, errors, data) {
    console.log('-----\nObey Warning: `requireIf` should be `requiredIf`\n-----')
    def.requiredIf = def.requireIf
    delete def.requireIf
    validators.requiredIf(def, value, key, errors, data)
  },

  /**
   * Validator requiredIfNot method, used by model
   * @param {Object} def The property configuration
   * @param {*} value The value being validated
   * @param {string} key The key name of the property
   * @param {Array<{type: string, sub: string|number, key: string, value: *, message: string}>} errors An error array
   * to which any additional error objects will be added
   * @param {Object} data The full initial data object
   */
  requiredIfNot: function(def, value, key, errors, data) {
    const type = 'requiredIfNot'
    const sub = def.requiredIfNot
    if (typeof sub === 'object') {
      const field = Object.keys(sub)[0]
      const fieldArr = Array.isArray(sub[field]) ? sub[field] : [ sub[field] ]
      fieldArr.some(val => {
        /* istanbul ignore else */
        if (dot.pick(field, data) !== val && value === undefined) {
          errors.push({ type, sub, key, value, message: `Value required because '${field}' value is not one specified` })
          return true
        }
      })
    } else if (dot.pick(sub, data) === undefined && value === undefined) {
      errors.push({ type, sub, key, value, message: `Value required because '${sub}' is undefined`})
    }
  },
  /**
   * Alias for requiredIfNot
   */
  requireIfNot: function(def, value, key, errors, data) {
    console.log('-----\nObey Warning: `requireIfNot` should be `requiredIfNot`\n-----')
    def.requiredIfNot = def.requireIfNot
    delete def.requireIfNot
    validators.requiredIfNot(def, value, key, errors, data)
  },

  /**
   * Validator equalTo method, used by model
   * @param {Object} def The property configuration
   * @param {*} value The value being validated
   * @param {string} key The key name of the property
   * @param {Array<{type: string, sub: string|number, key: string, value: *, message: string}>} errors An error array
   * to which any additional error objects will be added
   * @param {Object} data The full initial data object
   */
  equalTo: function(def, value, key, errors, data) {
    const type = 'equalTo'
    const sub = def.equalTo
    if (dot.pick(sub, data) !== value) {
      errors.push({ type, sub, key, value, message: `Value must match ${sub} value`})
    }
  },

  /**
   * Validator jexl method, used by model
   * @param {Object} def The property configuration
   * @param {*} value The value being validated
   * @param {string} key The key name of the property
   * @param {Array<{type: string, sub: string|number, key: string, value: *, message: string}>} errors An error array
   * to which any additional error objects will be added
   * @param {Object} data The full initial data object
   */
  jexl: (def, value, key, errors, data) => {
    const type = 'jexl'
    const sub = Array.isArray(def.jexl) ? def.jexl : [ def.jexl ]
    const promises = sub.map((obj) => {
      const {
        expr,
        message = 'Value failed Jexl evaluation'
      } = obj
      const instance = plugins.lib.jexl || jexl
      return instance.eval(expr, { root: data, value })
        .then(val => {
          if (!val) errors.push({ type, sub: obj, key, value, message })
        })
        .catch(() => {
          errors.push({ type, sub: obj, key, value, message })
        })
    })
    Promise.all(promises)
  }
}

module.exports = validators