'use strict'
const co = require('co')
const fs = require('fs')
const path = require('path')
const parse = require('co-body')
const ServerError = require('../errors/server-error')
const InvalidBodyError = require('../errors/invalid-body-error')
const InvalidUriParameterError =
require('../errors/invalid-uri-parameter-error')
const Ajv = require('ajv')
const BASE_DIR = path.join(__dirname, '/../schemas')
/**
* @typedef {Object} ValidationResult
* @property {Boolean} valid Whether the data matched the schema.
* @property {Object[]} errors A list of validation errors if any.
*/
/**
* Filter function for JSON data.
*
* @callback ValidatorFunction
* @param {Object} data Data to be validated
* @return {ValidationResult} Validation result
*/
/**
* Validation helper class.
*/
class Validator {
constructor () {
this.ajv = new Ajv()
this.loadedDirectories = new Set()
}
/**
* Load the schemas shipped with five-bells-shared.
*/
loadSharedSchemas () {
this.loadSchemasFromDirectory(BASE_DIR)
}
/**
* Load additional schemas from the provided directory.
*
* The schemas must be individual JSON files with the `.json` extension.
*
* @param {String} dirPath Absolute path to the schemas
*/
loadSchemasFromDirectory (dirPath) {
// Only load each directory once
if (this.loadedDirectories.has(dirPath)) return
this.loadedDirectories.add(dirPath)
fs.readdirSync(dirPath)
.filter((fileName) => {
return /^[\w\s]+\.json$/.test(fileName)
})
.forEach((fileName) => {
try {
let schemaJson = fs.readFileSync(path.join(dirPath, fileName), 'utf8')
this.ajv.addSchema(JSON.parse(schemaJson), fileName)
} catch (e) {
throw new ServerError('Failed to parse schema: ' + fileName + '. Reason: ' + e)
}
})
}
/**
* Create a validation function for schema `schema`.
*
* @return {ValidatorFunction} Validation function
*/
create (schema) {
return (data) => {
const isValid = this.ajv.validate(schema + '.json', data)
// Returning it in the same format as tv4.validateMultiple would do
return {
valid: isValid,
schema: schema,
errors: (isValid) ? undefined : this.ajv.errors
}
}
}
/**
* Validate path parameter.
*
* @param {String} paramId Name of URL parameter.
* @param {String} paramValue Value of URL parameter.
* @param {String} schema Name of JSON schema.
*
* @returns {void}
*/
validateUriParameter (paramId, paramValue, schema) {
const isValid = this.ajv.validate(schema + '.json', paramValue)
if (!isValid) {
throw new InvalidUriParameterError(paramId + ' is not a valid ' + schema,
this.ajv.errors)
}
}
/**
* Parse the request body JSON and optionally validate it against a schema.
*
* @param {Object} ctx Koa context.
* @param {String} schema Name of JSON schema.
*
* @returns {Promise.<Mixed>} Parsed JSON body
*/
validateBody (ctx, schema) {
return co.wrap(this._validateBody).call(this, ctx, schema)
}
* _validateBody (ctx, schema) {
const json = yield parse(ctx)
if (schema) {
const isValid = this.ajv.validate(schema + '.json', json)
if (!isValid) {
throw new InvalidBodyError('JSON request body is not a valid ' + schema,
this.ajv.errors)
}
}
return json
}
}
module.exports = Validator