UNPKG

egg-raml-validate

Version:
404 lines (328 loc) 9.37 kB
'use strict' const vm = require('vm') const debug = require('debug') const raml = require('raml-1-parser') const { isArray } = Array const isEmpty = require('lodash.isempty') const set = require('lodash.set') const get = require('lodash.get') const pathMatching = require('egg-path-matching') function _debugVerbose (object) { debug('egg-raml-validate:verbose')(JSON.stringify(object, '', 2)) } function _debugMeta (object) { debug('egg-raml-validate:meta')(JSON.stringify(object, '', 2)) } function sanitizeRules (rules) { for (const [ name, { type } ] of Object.entries(rules)) { const _type = type.replace(/^_*/, '') set(rules, [ name, 'type' ], _type) } return rules } /** * Covert bracket path to express-style path * * /user/{id}/{age} => /user/:id/:age * */ function bracketPathToExpressPath (path) { return path.replace(/\{/g, ':').replace(/\}/g, '') } function isNumberable (string) { const numberRe = /^[+-]?\d*\.?\d+(?:[Ee][+-]?\d+)?$/ return (numberRe.test(string)) } function isBooleanable (string) { return string === 'false' || string === 'true' } function toNumber (string) { return Number(string) } function toBoolean (string) { if (string === 'true') return true if (string === 'false') return false } /** * Lowercase keys of object * * Convert * * { * Authorization: {}, * Accept: {} * } * * to * * { * authorization: {}, * accept: {} * } * */ function lowerCaseObjectKey (object) { const _object = {} for (const [ key, value ] of Object.entries(object)) { const _key = key.toLowerCase() _object[_key] = value } return _object } /** * Convert values of object to a primative value * * Convert * * { * id: '1234', * admin: 'true', * password: 'password' * } * * to * * { * id: 1234, * admin: true, * password: 'password' * } * */ function primeObjectValue (object, rule) { const _object = {} for (const [ key, value ] of Object.entries(object)) { let _value const type = get(rule, [ key, 'type' ]) if (typeof value === 'string') { if ((isNumberable(value) && type === 'integer') || (isNumberable(value) && type === 'number')) { _value = toNumber(value) } else if (isBooleanable(value) && type === 'boolean') { _value = toBoolean(value) } else { _value = value } } else { _value = value } // Undefined / Null is ignored due to security reason. // Symbol is not necessary. _object[key] = _value } return _object } /** * Add more metadata for e throwed by egg-validate * * assign meta to every element in e.errors * */ function enrichValidateError (e, meta) { // ensure e is throw by egg-validate if (e.code === 'invalid_param') { for (const error of e.errors) { Object.assign(error, meta) } } } function matchedPath (_path, paths) { for (const path of paths) { const match = pathMatching({ match: path }) if (match({ path: _path })) { return path } } } function getFixedFacets (typeDeclaration) { const fixedFacets = typeDeclaration.fixedFacets() if (fixedFacets) { return fixedFacets.toJSON() } else { return {} } } function genRule (typeDeclaration) { const t = typeDeclaration const type = t.type()[0] const required = t.required() const requiredOptions = { type, required } const optionalOptions = getFixedFacets(t) const rule = Object.assign(requiredOptions, optionalOptions) return rule } function typeDeclarationsToRules (typeDeclarations) { const rules = {} for (const typeDeclaration of typeDeclarations) { const name = typeDeclaration.name() const rule = genRule(typeDeclaration) set(rules, name, rule) } return rules } function getAllUriParameters (res) { const rules = {} const resourcesChain = [] addParent(res) for (const resource of resourcesChain) { for (const uriParameter of resource.allUriParameters()) { const name = uriParameter.name() const rule = genRule(uriParameter) set(rules, name, rule) } } return rules function addParent (res) { resourcesChain.splice(0, 0, res) const parentRes = res.parentResource() if (parentRes) { addParent(parentRes) } } } function bodyTypeDeclarationsToRules (bodyTypeDeclarations) { if (isEmpty(bodyTypeDeclarations)) return {} for (const bodyTypeDeclaration of bodyTypeDeclarations) { const bodyType = bodyTypeDeclaration.name() const [ type ] = bodyTypeDeclaration.type() if (bodyType !== 'application/json') return {} if (type === 'object') { const rules = {} bodyTypeDeclaration.properties().forEach(function (prop) { const name = prop.name() const rule = genRule(prop) set(rules, name, rule) }) return rules } else { return {} } } } /** * Generate meta info from parsed RAML */ function genMeta (api) { const meta = { paths: [], verbose: {} } processResource(api, meta) // reverse sort pathsList for right order of paths meta.paths = meta.paths.sort().reverse() return meta function processResource (resource, meta) { for (const childResource of resource.resources()) { const path = bracketPathToExpressPath(childResource.completeRelativeUri()) meta.paths.push(path) let uriParameters = getAllUriParameters(childResource) uriParameters = sanitizeRules(uriParameters) // methods for (const method of childResource.methods()) { const methodName = method.method() // uriParameters set(meta, ['verbose', path, methodName, 'rules', 'uriParameters'], uriParameters) // headers let headersRules = typeDeclarationsToRules(method.headers()) headersRules = sanitizeRules(headersRules) headersRules = lowerCaseObjectKey(headersRules) // egg use lowercased headers set(meta, ['verbose', path, methodName, 'rules', 'headers'], headersRules) // queryParameters let queryParametersRules = typeDeclarationsToRules(method.queryParameters()) queryParametersRules = sanitizeRules(queryParametersRules) set(meta, ['verbose', path, methodName, 'rules', 'queryParameters'], queryParametersRules) // body let bodyRules = bodyTypeDeclarationsToRules(method.body()) bodyRules = sanitizeRules(bodyRules) set(meta, ['verbose', path, methodName, 'rules', 'body'], bodyRules) // middlewares & controller set(meta, ['verbose', path, methodName, 'middlewares'], []) set(meta, ['verbose', path, methodName, 'controller'], undefined) for (const annotation of method.annotations()) { const name = annotation.name() const value = annotation.structuredValue().toJSON() if (name === 'middlewares') { let middlewares = [] if (isArray(value)) { middlewares = value } set(meta, ['verbose', path, methodName, 'middlewares'], middlewares) } if (name === 'controller') { const controller = value set(meta, ['verbose', path, methodName, 'controller'], controller) } } } processResource(childResource, meta) } } } /** * bind paths to controllers */ function routes (app, meta, ramlValidate) { for (const [ path, pathObject ] of Object.entries(meta.verbose)) { for (const [ method, { middlewares, controller } ] of Object.entries(pathObject)) { const _middlewares = middlewares.map(mw => { const sandbox = { app } vm.runInNewContext(`mw = ${mw}`, sandbox) return sandbox.mw }) if (controller) { app[method](path, ..._middlewares, ramlValidate, controller) } } } } module.exports = (options, app) => { const { ramlFile } = options const api = raml.loadApiSync(ramlFile).expand() const meta = genMeta(api) _debugMeta(meta) app.beforeStart(() => { routes(app, meta, ramlValidate) }) async function ramlValidate (ctx, next) { const _path = ctx.request.path.toLowerCase() const _method = ctx.request.method.toLowerCase() const path = matchedPath(_path, meta.paths) const method = _method const rules = get(meta, [ 'verbose', path, method, 'rules' ]) if (!rules) { await next() return } const dataMap = { headers: ctx.header, uriParameters: ctx.params, queryParameters: ctx.query, body: ctx.request.body } _debugVerbose(dataMap) _debugVerbose(rules) for (let [location, rule] of Object.entries(rules)) { if (!isEmpty(rule)) { let data = dataMap[location] switch (location) { case 'uriParameters': Object.assign(ctx.params, primeObjectValue(ctx.params, rule)) break case 'queryParameters': Object.assign(ctx.query, primeObjectValue(ctx.query, rule)) break } try { ctx.validate(rule, data) } catch (e) { enrichValidateError(e, { in: location }) throw e } } } await next() } return async function (ctx, next) { await next() } }