method_validate.js

import joi from 'joi'

const excludedTypes = ['object', 'array']
const excludedNames = []

/**
 * @typedef {string[]} TValidatorString
 * @property {string} 0=alphanum
 * @property {string} 1=base64
 * @property {string} 2=case
 * @property {string} 3=creditCard
 * @property {string} 4=dataUri
 * @property {string} 5=email
 * @property {string} 6=guid
 * @property {string} 7=uuid
 * @property {string} 8=hex
 * @property {string} 9=hostname
 * @property {string} 10=insenstive
 * @property {string} 11=ip
 * @property {string} 12=isoDate
 * @property {string} 13=isoDuration
 * @property {string} 14=length
 * @property {string} 15=lowercase
 * @property {string} 16=max
 * @property {string} 17=min
 * @property {string} 18=normalize
 * @property {string} 19=pattern
 * @property {string} 20=regex
 * @property {string} 21=replace
 * @property {string} 22=token
 * @property {string} 23=trim
 * @property {string} 24=truncate
 * @property {string} 25=upercase
 * @property {string} 26=uri
 */

/**
 * @typedef {string[]} TValidatorNumber
 * @property {string} 0=great
 * @property {string} 1=less
 * @property {string} 2=max
 * @property {string} 3=min
 * @property {string} 4=multiple
 * @property {string} 5=negative
 * @property {string} 6=port
 * @property {string} 7=positive
 * @property {string} 8=sign
 * @property {string} 9=unsafe
 */

/**
 * @typedef {string[]} TValidatorBoolean
 * @property {string} 0=falsy
 * @property {string} 1=sensitive
 * @property {string} 2=truthy
 */

/**
 * @typedef {string[]} TValidatorDate
 * @property {string} 0=greater
 * @property {string} 1=iso
 * @property {string} 2=less
 * @property {string} 2=max
 * @property {string} 2=min
 */

/**
 * @typedef {string[]} TValidatorTimestamp
 * @property {string} 0=timestamp
 */

/**
 * @typedef {Object} TValidator
 * @property {TValidatorString} string
 * @property {TValidatorNumber} number
 * @property {TValidatorBoolean} boolean
 * @property {TValidatorDate} date
 * @property {TValidatorTimestamp} timestamp
 */
const validator = {
  string: ['alphanum', 'base64', 'case', 'creditCard', 'dataUri', 'domain', 'email', 'guid',
    'uuid', 'hex', 'hostname', 'insensitive', 'ip', 'isoDate', 'isoDuration', 'length', 'lowercase',
    'max', 'min', 'normalize', 'pattern', 'regex', 'replace', 'token', 'trim', 'truncate',
    'uppercase', 'uri'],
  number: ['great', 'less', 'max', 'min', 'multiple', 'negative', 'port', 'positive',
    'sign', 'unsafe'],
  boolean: ['falsy', 'sensitive', 'truthy'],
  date: ['greater', 'iso', 'less', 'max', 'min'],
  timestamp: ['timestamp']
}

function buildFromDbSchema (schema, { fields = [], rule = {}, extFields = [] } = {}) {
  // if (schema.validation) return schema.validation
  const {
    isPlainObject, get, each, isEmpty, isString, forOwn, keys,
    find, isArray, has, cloneDeep, concat, without
  } = this.app.lib._
  const obj = {}
  const { propType } = this.app.pluginClass.dobo
  const refs = []

  function getRuleKv (kvRule) {
    let key
    let value
    let columns
    if (isPlainObject(kvRule)) {
      key = kvRule.rule
      value = kvRule.params
      columns = kvRule.fields
    } else if (isString(kvRule)) {
      [key, value, columns] = kvRule.split(':')
    }
    return { key, value, columns }
  }

  function applyFieldRules (prop, obj) {
    const minMax = { min: false, max: false }
    const rules = get(rule, prop.name, prop.rules ?? [])
    if (!isArray(rules)) return rules
    each(rules, r => {
      const types = validator[propType[prop.type].validator]
      const { key, value } = getRuleKv(r)
      if (keys(minMax).includes(key)) minMax[key] = true
      if (key === 'ref') {
        refs.push(prop.name)
        obj = joi.ref(value)
        return undefined
      }
      if (!key || !types.includes(key)) return undefined
      obj = obj[key](value)
    })
    if (refs.includes(prop.name)) return obj
    if (['string', 'text'].includes(prop.type)) {
      forOwn(minMax, (v, k) => {
        if (v) return undefined
        if (has(prop, `${k}Length`)) obj = obj[k](prop[`${k}Length`])
      })
    }
    if (isArray(prop.values)) obj = obj.valid(...prop.values)
    if (!['id'].includes(prop.name) && prop.required) obj = obj.required()
    return obj
  }

  const props = concat(cloneDeep(schema.properties), extFields)

  for (const p of props) {
    if (excludedTypes.includes(p.type) || excludedNames.includes(p.name)) continue
    if (fields.length > 0 && !fields.includes(p.name)) continue
    let item
    switch (p.type) {
      case 'text':
      case 'string': {
        item = applyFieldRules(p, joi.string())
        break
      }
      case 'smallint':
      case 'integer':
        item = applyFieldRules(p, joi.number().integer())
        break
      case 'float':
      case 'double':
        if (p.precision) item = applyFieldRules(p, joi.number().precision(p.precision))
        else item = applyFieldRules(p, joi.number())
        break
      case 'time':
      case 'date':
      case 'datetime':
        item = applyFieldRules(p, joi.date())
        break
      case 'timestamp':
        item = applyFieldRules(p, joi.number().integer())
        break
      case 'boolean':
        item = applyFieldRules(p, joi.boolean())
        break
    }
    if (item) {
      if (item.$_root) obj[p.name] = item.allow(null)
      else obj[p.name] = item
    }
  }
  if (isEmpty(obj)) return false
  each(get(schema, 'globalRules', []), r => {
    each(without(keys(obj), ...refs), k => {
      const prop = find(props, { name: k })
      if (!prop) return undefined
      const types = validator[propType[prop.type].validator]
      const { key, value, columns = [] } = getRuleKv(r)
      if (!types.includes(key)) return undefined
      if (columns.length === 0 || columns.includes(k)) obj[k] = obj[k][key](value)
    })
  })
  const result = joi.object(obj)
  if (fields.length === 0) return result
  each(['with', 'xor', 'without'], k => {
    const item = get(schema, `extRule.${k}`)
    if (item) result[k](...item)
  })
  return result
}

/**
 * Validate value against JOI schema
 *
 * @method
 * @memberof Dobo
 * @async
 * @param {Object} value - value to validate
 * @param {Object} joiSchema - JOI schema
 * @param {Object} [options={}] - Options object
 * @param {string} [options.ns=dobo] - Scope's namespace
 * @param {Array} [options.fields=[]]
 * @param {Array} [options.extFields=[]]
 * @param {Object} [options.params={}] - Validation parameters. See {@tutorial config} and {@link https://joi.dev/api/?v=17.13.3#anyvalidateasyncvalue-options|JOI validate's options}
 * @returns {Object}
 */
async function validate (value, joiSchema, { ns, fields, extFields, params } = {}) {
  const { defaultsDeep, isSet } = this.app.lib.aneka
  const { isString, forOwn, find } = this.app.lib._

  ns = ns ?? [this.ns]
  params = defaultsDeep(params, this.config.validationParams)
  const { rule = {} } = params
  delete params.rule
  if (isString(joiSchema)) {
    const { schema } = this.getInfo(joiSchema)
    forOwn(value, (v, k) => {
      if (!isSet(v)) return undefined
      const p = find(schema.properties, { name: k })
      if (!p) return undefined
      for (const t of ['date|YYYY-MM-DD', 'time|HH:mm:ss']) {
        const [type, input] = t.split('|')
        if (p.type === type) value[k] = this.sanitizeDate(value[k], { input, output: 'native' })
      }
    })
    joiSchema = buildFromDbSchema.call(this, schema, { fields, rule, extFields })
  }
  if (!joiSchema) return value
  try {
    return await joiSchema.validateAsync(value, params)
  } catch (err) {
    throw this.error('validationError', { details: err.details, values: err.values, ns, statusCode: 422, code: 'DB_VALIDATION' })
  }
}

export default validate