dobo
Version:
DBMS for Bajo Framework
250 lines (236 loc) • 8.2 kB
JavaScript
import joi from 'joi'
const excludedTypes = ['object']
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'],
array: ['length', 'max', 'min']
}
async function buildFromDbModel (opts = {}) {
const { isPlainObject, get, isEmpty, isString, keys, find, has, without } = this.app.lib._
const { fields = [], rule = {}, extFields = [] } = opts
const obj = {}
const { propertyType: propType } = this.app.baseClass.Dobo
const refs = []
const me = this
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 }
}
async function applyFieldRules (prop, obj) {
const minMax = { min: false, max: false }
const rules = get(rule, prop.name, prop.rules ?? [])
if (!Array.isArray(rules)) return rules
for (const r of rules) {
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)
continue
}
if (!key || !types.includes(key)) continue
obj = obj[key](value)
}
if (refs.includes(prop.name)) return obj
if (['string', 'text'].includes(prop.type)) {
for (const k in minMax) {
if (minMax[k]) continue
if (has(prop, `${k}Length`)) obj = obj[k](prop[`${k}Length`])
}
}
if (!['id'].includes(prop.name) && prop.required) obj = obj.required()
if (prop.values) {
const values = await me.buildPropValues(prop, opts)
const items = values.map(item => item.value)
if (prop.type === 'array') {
obj = obj.items(joi.string().valid(...items))
} else obj = obj.valid(...items)
}
if (prop.rulesMsg) {
const msgs = {}
for (const k in prop.rulesMsg) {
msgs[k] = '~' + prop.rulesMsg[k] // note: to tell bajo error formatter that this is a custom error message
}
obj = obj.messages(msgs)
}
return obj
}
const props = [...this.properties, ...extFields]
for (const p of props) {
if (excludedTypes.includes(p.type) || excludedNames.includes(p.name)) continue
if (opts.partial && fields.length > 0 && !fields.includes(p.name)) continue
let item
switch (p.type) {
case 'text':
case 'string': {
item = await applyFieldRules(p, joi.string())
break
}
case 'smallint':
case 'integer':
item = await applyFieldRules(p, joi.number().integer())
break
case 'float':
case 'double':
if (p.precision) item = await applyFieldRules(p, joi.number().precision(p.precision))
else item = await applyFieldRules(p, joi.number())
break
case 'time':
case 'date':
case 'datetime':
item = await applyFieldRules(p, joi.date())
break
case 'timestamp':
item = await applyFieldRules(p, joi.number().integer())
break
case 'boolean':
item = await applyFieldRules(p, joi.boolean())
break
case 'array':
item = await applyFieldRules(p, joi.array())
break
}
if (item) {
if (item.$_root && !p.required) obj[p.name] = item.allow(null, '')
else obj[p.name] = item
}
}
if (isEmpty(obj)) return false
for (const r of this.rules) {
for (const k of without(keys(obj), ...refs)) {
const prop = find(props, { name: k })
if (!prop) continue
const types = validator[propType[prop.type].validator]
const { key, value, columns = [] } = getRuleKv(r)
if (!types.includes(key)) continue
if (columns.length === 0 || columns.includes(k)) obj[k] = obj[k][key](value)
}
}
const result = joi.object(obj)
for (const k of ['with', 'xor', 'without']) {
const item = get(this, `extRule.${k}`)
if (item) result[k](...item)
}
return result
}
/**
* Validate value against JOI model
*
* @method
* @memberof Dobo
* @async
* @param {Object} value - value to validate
* @param {Object} joiModel - JOI model
* @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 (body, joiModel, opts = {}) {
const { defaultsDeep } = this.app.lib.aneka
const { isEmpty } = this.app.lib._
let { extFields = [], params = {}, partial } = opts
params = defaultsDeep(params, this.app.dobo.config.validationParams)
const { rule = {} } = params
delete params.rule
const fields = partial ? Object.keys(body) : [...opts.fields]
if (isEmpty(joiModel)) joiModel = await buildFromDbModel.call(this, { fields, rule, extFields, partial })
if (!joiModel) return { value: body }
try {
return await joiModel.validateAsync(body, params)
} catch (err) {
const payload = { details: err.details, statusCode: 422, code: 'DB_VALIDATION', model: this.name }
if (err.values) payload.values = err.values
throw this.plugin.error('validationError', payload)
}
}
export default validate