UNPKG

layer-one-validator

Version:

Expressjs middleware to validate data from client side requests

448 lines (325 loc) 13.6 kB
'use strict'; // NOTE:: Status codes - Client erros // https://docs.github.com/en/rest/overview/resources-in-the-rest-api#client-errors const source = 'layer-one-validator'; const message_500 = 'If you own the server, check the logs'; const failMessages = { base: { fields: 'input-fields :: Miss match', type: 'input-types', biz: 'biz', }, append: { array: 'Tip: Test the item, not the array.', }, source, message_500 }; /** * @param {object} requestProps incoming request properties (from client, ex: `id`, `name`) * @param { {prop:string, optional:boolean|undefined}[] } expectedProps ...Array ou `stringA`, `stringB` * @returns */ const hasAllRequiredProps = function (incomingProps, expectedProps) { const required = expectedProps .filter(v => !v.optional || (v.optional && incomingProps[v.prop])) .map(v => v.prop); if (Object.keys(incomingProps).length !== required.length) return false; const hopObject = prop => Object.prototype.hasOwnProperty.call(incomingProps, prop); return required.every(prop => hopObject(prop)); }; const makeRequestProp = function (req) { const requestProps = ['body', 'params', 'query']; if (this.requestProp && !requestProps.includes(this.requestProp)) { throw new Error('`requestProp` must be "body" or "params"'); } const requestProp = req[this.requestProp] ?? req.body; return requestProp; }; const fields = function () { return (req, res, next) => () => { const expectedProps = this.data.map(({ prop, optional }) => ({ prop, optional })); const hasAllProps = hasAllRequiredProps(this.reqProp, expectedProps); if (hasAllProps) return next(); const expected = Array.isArray(this.data) ? this.data : [this.data]; const actual = { ...this.reqProp }; const missing = expected.filter(v => !(v.prop in actual)).map(v => v.prop); const extra = Object.keys(actual).filter(v => !expected.some(vv => vv.prop === v)); const fail = { missing, extra }; const layer = this.requestProp; res.status(400).json({ success: false, message: failMessages.base.fields, fail, layer, source }); } }; const type = function () { return (req, res, next) => () => { const tests = this.data.map(({ prop, type: fn, optional }) => { if (!fn) return { test: true }; const value = this.reqProp[prop]; if (value === undefined && optional) return { test: true }; const arr = Array.isArray(value) ? value : [value]; // NOTE:: Até à v0.2.0 verificava se testava contra o item do array... // Em v0.3.0+ retorna 500, validado em `checkTypeAgainstItemValidator()` // file:///C:\Users\vik\Dropbox\dev\minor\layer-one-validator\_sketch\_old-type.js // NOTE:: "[].every()" retorna `true` em empty array return { prop, test: [...arr].every(fn) && !!arr.length }; }); if (tests.every(v => v.test)) return next(); const fail = tests.find(v => !v.test).prop; const message = failMessages.base.type; const layer = this.requestProp; res.status(400).json({ success: false, message, fail, layer, source }); } }; const biz = function () { let message = failMessages.base.biz; return (req, res, next) => () => { const tests = this.data.map(({ prop, biz: fn, optional }) => { if (!fn) return { test: true }; const value = this.reqProp[prop]; if (value === undefined && optional) return { test: true }; const arr = Array.isArray(value) ? value : [value]; if (Array.isArray(value)) { if (fn(value)) return { prop, test: true }; } // NOTE:: "[].every()" retorna `true` em empty array return { prop, test: [...arr].every(fn) && !!arr.length }; }); if (tests.every(v => v.test)) return next(); const fail = tests.find(v => !v.test).prop; const layer = this.requestProp; res.status(422).json({ success: false, message, fail, layer, source }); } }; const tryPrintError = error => { if (process.argv[5] === 'no-print-error') return; const reset = '\x1b[0m'; const fgRed = '\x1b[31m'; const message = `${fgRed}${error.message}${reset}`; const repo = 'https://github.com/vikcch/layer-one-validator'; const help = `\n➡ Check the documentation at: ${repo}` console.error(error.name, '❌ layer-one-validator #', message, help); }; /** * The `type` property must be checked against the item, not the array * * @param { { prop:string, type:function, biz:function }[] } data * @returns */ const checkTypeAgainstItemValidator = data => { const hasType = value => Object.keys(value).includes('type'); const types = ['', 0, false, {}]; const fail = data.find(v => hasType(v) && types.every(vv => !v.type(vv))); if (fail) { const lines = [ `Ensure that the 'type' property of '${fail.prop}' is checked for its type.`, "For example: `type: v => typeof v === 'string'`", 'Hint: Perform the test on the individual item, not the entire array.' ]; throw new Error(lines.join('\n')); } }; /** * * @param { { prop:string, type:function, biz:function }[] } data * @returns */ const dataValidator = data => { const propFail = data.find(v => !v.prop || typeof v.prop !== 'string'); if (propFail) { const message = propFail.prop ? `The 'prop' property of '${propFail.prop}' must be a string` : `The 'prop' property is mandatory`; throw new Error(message); } const typeFail = data.find(v => Object.keys(v).includes('type') && typeof v.type !== 'function'); if (typeFail) { throw new Error(`The 'type' property of '${typeFail.prop}' must be a function`); } checkTypeAgainstItemValidator(data); const bizFail = data.find(v => Object.keys(v).includes('biz') && typeof v.biz !== 'function') if (bizFail) { throw new Error(`The 'biz' property of '${bizFail.prop}' must be a function`); } }; const arrayValidator = mixArray => { const extraPropsCount = !!mixArray.requestProp | 0; // NOTE:: Não copia properties de "mix array/object" (.requestProp) const array = mixArray.slice(); const alloweds = ['prop', 'type', 'biz', 'optional']; const keys = Object.keys(mixArray); if (keys.length !== array.length + extraPropsCount) { throw new Error('Prohibited properties. Allowed: `prop` (mandatory), `type` and `biz` (optional).'); } const isDirty = array.some(item => { const itemKeys = Object.keys(item); return itemKeys.some(v => !alloweds.includes(v)); }); if (isDirty) { throw new Error('Prohibited properties. Allowed: `prop` (mandatory), `type` and `biz` (optional).'); } }; const requestValidator = (requestProp, req) => { if (req.prop) throw new Error('Multiple properties must be on an array'); if (requestProp !== 'body') return; if (process.env.TEST === 'LOV') return; // NOTE:: key / field names are case-insensitive // https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 const contentType = req.headers['content-type']?.toLowerCase(); // NOTE:: Pode ter `charset=utf-8` no inicio ou fim if (!contentType.includes('application/json')) { throw new Error(`Set the headers { 'Content-Type': 'application/json' } `); } }; /** * * @param { (object|array) } value Pode ser "mixArray" - array com props */ const thisValidator = (value) => { const removeRequestProp = (value) => { if (Array.isArray(value)) return value.slice(); const { requestProp, ...target } = value return target; }; const target = removeRequestProp(value); if (!target) { throw new Error('An `object` must be provided to the bind function'); } const alloweds = ['prop', 'type', 'biz', 'optional']; const keys = Object.keys(target); if (!Array.isArray(target) && keys.some(v => !alloweds.includes(v))) { throw new Error('Prohibited properties. Allowed: `prop` (mandatory), `type` and `biz` (optional).'); } if (Array.isArray(target)) arrayValidator(value); }; /** * @summary * This function is used along with _expressjs_ as a _middleware_ to validate * data from requests, should be called using the `bind()` method. The * first and only _argument_ could be an `object` or an `array`: * * `{ prop:string [, type:function][, biz:function] }` * `{ prop:string [, type:function][, biz:function] }[]` * * An additional property `requestProp:string` could be provided, being `'body'`, * `'params'` or `'query'`, when ommited, `'body'` will be the _default_. * * If the value to be tested is an _array_, the `type:function` must be tested * against each element of that `array`, for the `biz:function` it could be tested * against the whole `array` or against each element. * * {@link} DOCUMENTATION: https://github.com/vikcch/layer-one-validator * * @example * layerOneValidator.bind(Object.assign([ * {prop:'id', type: v => Number.isInteger(v), biz: v => v>0}, * {prop:'username', biz: v => /^[a-z]{4,8}$/.test(v)} * ]), { requestProp: 'body' }); * * @example * layerOneValidator.bind({prop:'id'}) * */ const start = function (req, res, next) { // NOTE:: Validações sem condicional, tem "throw" na function try { requestValidator(this.requestProp, req); thisValidator(this); const that = { reqProp: makeRequestProp.call(this, req), data: Array.isArray(this) ? this.slice() : [this], requestProp: this.requestProp // layer }; dataValidator(that.data); const absxs = [ fields.call(that), type.call(that), biz.call(that), ]; const run = absxs.reduceRight((acc, cur) => cur(req, res, acc), next); run(); } catch (err) { tryPrintError(err); const layer = this.requestProp; res.status(500).json({ success: false, message: message_500, layer, source }); } }; /** * @summary * Those callback functions are used along with _expressjs_ as a _middleware_ to * validate data from requests, should be called using the `bind()` method. The * first and only _argument_ could be an `object` or an `array`: * * `{ prop:string [, type:function][, biz:function] }` * `{ prop:string [, type:function][, biz:function] }[]` * * If the value to be tested is an _array_, the `type:function` must be tested * against each element of that `array`, for the `biz:function` it could be tested * against the whole `array` or against each element. * * {@link} DOCUMENTATION: https://github.com/vikcch/layer-one-validator * * @property {object} object * @property {function} object.body - Express request.body * @property {function} object.params - Express request.params * @property {function} object.query - Express request.query * */ module.exports = { // export default { /** * {@link} DOCUMENTATION: https://github.com/vikcch/layer-one-validator * * @example * layerOneValidator.body.bind([ * {prop: 'id', type: v => Number.isInteger(v), biz: v => v>0}, * {prop: 'username', biz: v => /^[a-z]{4,8}$/.test(v)} * ]) * * @example * layerOneValidator.body.bind({prop:'id'}) */ body(req, res, next) { // NOTE:: Aqui para não poluir o "exports" if (this === 'messages') return { failMessages }; const binding = Object.assign(this || {}, { requestProp: 'body' }); start.call(binding, req, res, next); }, /** * {@link} DOCUMENTATION: https://github.com/vikcch/layer-one-validator * * @example * layerOneValidator.params.bind([ * {prop: 'id', type: v => Number.isInteger(v), biz: v => v>0}, * {prop: 'username', biz: v => /^[a-z]{4,8}$/.test(v)} * ]) * * @example * layerOneValidator.params.bind({prop:'id'}) */ params(req, res, next) { const binding = Object.assign(this || {}, { requestProp: 'params' }); start.call(binding, req, res, next); }, /** * {@link} DOCUMENTATION: https://github.com/vikcch/layer-one-validator * * @example * layerOneValidator.query.bind([ * {prop: 'id', type: v => Number.isInteger(v), biz: v => v>0}, * {prop: 'username', biz: v => /^[a-z]{4,8}$/.test(v)} * ]) * * @example * layerOneValidator.query.bind({prop:'id'}) */ query(req, res, next) { const binding = Object.assign(this || {}, { requestProp: 'query' }); start.call(binding, req, res, next); } }; // module.exports = { // ...module.exports, // testables: { // hasAllRequiredProps // } // };