UNPKG

7.38 kBJavaScriptView Raw
1/*!
2 * Copyright (c) 2012-2018 Digital Bazaar, Inc. All rights reserved.
3 */
4'use strict';
5
6const fs = require('fs');
7const PATH = require('path');
8const bedrock = require('bedrock');
9const logger = require('./logger');
10const Ajv = require('ajv');
11const {BedrockError} = bedrock.util;
12const ajv = new Ajv({verbose: true});
13
14// load config defaults
15require('./config');
16
17const api = {};
18module.exports = api;
19
20// available schemas
21const schemas = api.schemas = {};
22
23bedrock.events.on('bedrock.init', init);
24
25/**
26 * Initializes the validation system: loads all schemas, etc.
27 */
28function init() {
29 // schemas to skip loading
30 const skip = bedrock.config.validation.schema.skip.slice();
31
32 // load all schemas
33 const schemaDirs = bedrock.config.validation.schema.paths;
34 const jsExt = '.js';
35 schemaDirs.forEach(schemaDir => {
36 schemaDir = PATH.resolve(schemaDir);
37 logger.debug('loading schemas from: ' + schemaDir);
38 fs.readdirSync(schemaDir).filter(file => {
39 const js = PATH.extname(file) === jsExt;
40 const use = skip.indexOf(file) === -1;
41 return js && use;
42 }).forEach(file => {
43 const name = PATH.basename(file, PATH.extname(file));
44 const module = require(PATH.join(schemaDir, file));
45 if(typeof module === 'function') {
46 if(name in schemas) {
47 logger.debug(
48 'overwriting schema "' + name + '" with ' +
49 PATH.resolve(schemaDir, file));
50 }
51 schemas[name] = module;
52 schemas[name].instance = module();
53 logger.debug('loaded schema: ' + name);
54 } else {
55 for(const key in module) {
56 const tmp = name + '.' + key;
57 if(tmp in schemas) {
58 logger.debug('overwriting schema "' + tmp + '" with ' + file);
59 }
60 schemas[tmp] = module[key];
61 schemas[tmp].instance = schemas[tmp]();
62 logger.debug('loaded schema: ' + tmp);
63 }
64 }
65 });
66 });
67}
68
69/**
70 * This method takes one or three parameters.
71 *
72 * If only one parameter is given, returns express middleware that will be
73 * used to validate a request using the schema associated with the given name.
74 *
75 * If a string is provided for the first parameter, then it will be used
76 * as the schema name for validating the request body.
77 *
78 * If an object is provided for the first parameter, then the object can
79 * contain 'body' and 'query' schema names as properties of the object.
80 *
81 * If two parameters are given, the first parameter must be a string
82 * and the second parameter must be the data to validate. The return value
83 * will contain the result of the validation.
84 *
85 * If three parameters are given, the first parameter must be a string,
86 * the second parameter must be the data to validate and the third must be
87 * function to be called once the validation operation completes.
88 *
89 * @param name the name of the schema to use (or an object with names).
90 * @param [data] the data to validate.
91 * @param [callback(err)] called once the operation completes.
92 *
93 * @return the validation result.
94 */
95api.validate = function(name, data, callback) {
96 // NOTE: this cannot be an arrow function due to use of `arguments`
97 const options = {};
98
99 if(typeof name === 'object') {
100 if('body' in name) {
101 options.body = name.body;
102 }
103 if('query' in name) {
104 options.query = name.query;
105 }
106 } else {
107 options.body = name;
108 }
109
110 // look up schema(s) by name
111 const schemas = {};
112 let notFound = null;
113 Object.keys(options).forEach(key => {
114 schemas[key] = api.getSchema(options[key]);
115 if(!notFound && !schemas[key]) {
116 notFound = options[key];
117 }
118 });
119
120 // do immediate validation if data is present
121 if(arguments.length > 1) {
122 if(notFound) {
123 const err = new BedrockError(
124 'Could not validate data; unknown schema name (' + notFound + ').',
125 'UnknownSchema', {schema: notFound});
126 if(typeof callback === 'function') {
127 return callback(err);
128 }
129 throw err;
130 }
131 // use schema.body (assume 3 parameter is called w/string)
132 return api.validateInstance(data, schemas.body, callback);
133 }
134
135 // schema does not exist, return middle that raises error
136 if(notFound) {
137 return (req, res, next) => {
138 next(new BedrockError(
139 'Could not validate request; unknown schema name (' + notFound + ').',
140 'UnknownSchema', {schema: notFound}));
141 };
142 }
143
144 // return validation middleware
145 // both `query` and `body` may need to be validated
146 return (req, res, next) => {
147 if(schemas.query) {
148 const result = api.validateInstance(req.query, schemas.query);
149 if(!result.valid) {
150 return next(result.error);
151 }
152 }
153 if(schemas.body) {
154 const result = api.validateInstance(req.body, schemas.body);
155 if(!result.valid) {
156 return next(result.error);
157 }
158 }
159 next();
160 };
161};
162
163/**
164 * Retrieves a validation schema given a name for the schema.
165 *
166 * @param name the name of the schema to retrieve.
167 *
168 * @return the object for the schema, or null if the schema doesn't exist.
169 */
170api.getSchema = name => {
171 let schema = null;
172 if(name in api.schemas) {
173 schema = api.schemas[name].instance;
174 }
175 return schema;
176};
177
178/**
179 * Validates an instance against a schema.
180 *
181 * @param instance the instance to validate.
182 * @param schema the schema to use.
183 * @param [callback(err)] called once the operation completes.
184 *
185 * @return the validation result.
186 */
187api.validateInstance = (instance, schema, callback) => {
188 // do validation
189 const valid = ajv.validate(schema, instance);
190 if(valid) {
191 if(callback) {
192 return callback(null, {valid});
193 }
194 return {valid};
195 }
196
197 const result = {valid: false};
198
199 // create public error messages
200 const errors = [];
201 for(const error of ajv.errors) {
202 // create custom error details
203 const details = {
204 instance,
205 params: error.params,
206 path: error.dataPath,
207 public: true,
208 schemaPath: error.schemaPath,
209 };
210 let title;
211 if(Array.isArray(error.schema)) {
212 [title] = error.schema;
213 }
214 title = title || error.parentSchema.title || '',
215 details.schema = {
216 description: error.parentSchema.description || '',
217 title,
218 };
219 // include custom errors or use default
220 // FIXME: enable if ajv supports this parentSchema.errors property
221 // it appears that this is not the case
222 // details.errors = error.parentSchema.errors || {
223 // invalid: 'Invalid input.',
224 // missing: 'Missing input.'
225 // };
226 if(error.data) {
227 if(error.parentSchema.errors && 'mask' in error.parentSchema.errors) {
228 const mask = error.parentSchema.errors.mask;
229 if(mask === true) {
230 details.value = '***MASKED***';
231 } else {
232 details.value = mask;
233 }
234 } else {
235 details.value = error.data;
236 }
237 }
238
239 // add bedrock validation error
240 errors.push(new BedrockError(error.message, 'ValidationError', details));
241 }
242
243 const msg = schema.title ?
244 'A validation error occured in the \'' + schema.title + '\' validator.' :
245 'A validation error occured in an unnamed validator.';
246 const error = new BedrockError(
247 msg, 'ValidationError', {public: true, errors, httpStatusCode: 400});
248
249 result.error = error;
250 if(callback) {
251 return callback(null, result);
252 }
253 return result;
254};