├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── lib ├── fieldValidator.js ├── fileActions.js ├── fileRules.js ├── fileValidator.js ├── filters.js ├── requiredRules.js ├── rules.js └── validate.js ├── package.json └── test ├── app ├── app.js ├── file_routes.js ├── header_routes.js ├── param_routes.js ├── post_routes.js └── query_routes.js ├── files └── redpanda.jpg ├── test_body.js ├── test_files.js ├── test_headers.js ├── test_params.js └── test_query.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | 4 | .idea -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | node_js: 2 | - "4.2" 3 | - "4.1" 4 | - "4.0" 5 | - "5.0" 6 | - "5.1" 7 | - "5.2" 8 | - "5.3" 9 | language: node_js 10 | script: "npm run test-travis" 11 | after_script: "npm install coveralls@2 && cat ./coverage/lcov.info | coveralls" 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Srinivas Iyer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Koa Validation 2 | 3 | Koa Validation is a validation middleware for Koa. Using Koa Validation, you can validate 4 | url params, url queries, request bodies, headers as well as files. The module also allows for nested 5 | queries and nested jsons to be validated. The plugin uses generators and hence syncronous database validations 6 | can alse be made. The module can also be used to filter values sent as well as performing after actions on files uploaded. 7 | 8 | The module can also be extended to add custom rules, filters and actions based on convenience. 9 | 10 | ##Installation 11 | 12 | ```npm install koa-validation``` 13 | 14 | ##Field Validations 15 | 16 | ```js 17 | 18 | var app = require('koa')(); 19 | var router = (new require('koa-router'))(); 20 | var koaBody = require('koa-better-body'); 21 | 22 | require('koa-qs')(app, 'extended'); 23 | 24 | var validate = require('koa-validation'); 25 | 26 | app.use(koaBody({ 27 | 'multipart': true 28 | })); 29 | 30 | app.use(validate()); 31 | 32 | router.post('/', function *(){ 33 | yield this.validateBody( 34 | { 35 | name: 'required|minLength:4', 36 | girlfiend: 'requiredIf:age,25', 37 | wife: 'requiredNotIf:age,22', 38 | foo: 'requiredWith:bar,baz', 39 | foobar: 'requiredWithAll:barbaz,bazbaz', 40 | gandalf: 'requiredWithout:Saruman', 41 | tyrion: 'requiredWithoutAll:tywin,cercei', 42 | age: 'numeric', 43 | teenage: 'digitsBetween:13,19', 44 | date: 'dateFormat:MMDDYYYY', 45 | birthdate: 'date', 46 | past: 'before:2015-10-06', 47 | future: 'after:2015-10-07', 48 | gender: 'in:male, female', 49 | genres: 'notIn:Pop,Metal', 50 | grade: 'accepted', 51 | nickname: 'alpha', 52 | nospaces: 'alphaDash', 53 | email: 'email', 54 | alphanum: 'alphaNumeric', 55 | password: 'between:6,15' 56 | }, 57 | { 58 | 'name.required': 'The name field is a required one' 59 | }, 60 | { 61 | before: { 62 | name: 'lowercase', 63 | nickname: 'uppercase', 64 | snum: 'integer', 65 | sword: 'trim', 66 | lword: 'ltrim', 67 | rword: 'rtrim', 68 | dnum: 'float', 69 | bword: 'boolean', 70 | }, 71 | 72 | after: { 73 | obj: 'json', 74 | eword: 'escape', 75 | reword: 'replace:come,came', 76 | shaword: 'sha1', 77 | mdword: 'md5', 78 | hexword: 'hex:sha256' 79 | } 80 | } 81 | ) 82 | 83 | if (this.validationErrors) { 84 | this.status = 422; 85 | this.body = this.validationErrors; 86 | } else { 87 | this.status = 200; 88 | this.body = { success: true } 89 | } 90 | }); 91 | 92 | ``` 93 | 94 | ##File Validations 95 | 96 | ```js 97 | 98 | var app = require('koa')(); 99 | var router = (new require('koa-router'))(); 100 | var koaBody = require('koa-better-body'); 101 | 102 | require('koa-qs')(app, 'extended'); 103 | 104 | var validate = require('koa-validation'); 105 | 106 | app.use(koaBody({ 107 | 'multipart': true 108 | })); 109 | 110 | app.use(validate()); 111 | 112 | router.post('/files', function *(){ 113 | yield this.validateFiles({ 114 | 'jsFile':'required|size:min,10kb,max,20kb', 115 | 'imgFile': 'required|image', 116 | 'imgFile1': 'mime:jpg', 117 | 'imgFile2': 'extension:jpg', 118 | 'pkgFile': 'name:package' 119 | },true, {}, { 120 | jsFile: { 121 | action: 'move', 122 | args: __dirname + '/../files/tmp/rules.js', 123 | callback: function *(validator, file, destination){ 124 | validator.addError(jsFile, 'action', 'move', 'Just checking if the callback action works!!') 125 | } 126 | }, 127 | imgFile: [ 128 | { 129 | action: 'copy', 130 | args: __dirname + '/../files/tmp/panda.jpg' 131 | }, 132 | { 133 | action: 'delete' 134 | } 135 | ] 136 | }); 137 | 138 | if (this.validationErrors) { 139 | this.status = 422; 140 | this.body = this.validationErrors; 141 | } else { 142 | this.status = 200; 143 | this.body = { success: true } 144 | } 145 | }); 146 | 147 | app.use(router.routes()).use(router.allowedMethods()); 148 | 149 | ``` 150 | 151 | Check out Detailed Documentation at [koa-validation.readme.io](https://koa-validation.readme.io) 152 | 153 | ## License 154 | 155 | [MIT](LICENSE) 156 | -------------------------------------------------------------------------------- /lib/fieldValidator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var RequiredRules = require('./requiredRules'); 4 | var Rules = require('./rules'); 5 | var Filters = require('./filters'); 6 | 7 | class FieldValidator{ 8 | constructor(context, fields, rules, messages, filters){ 9 | this.ctx = context; 10 | this.fields = fields; 11 | 12 | this.filters = new Filters(this); 13 | this.rules = new Rules(this); 14 | this.requiredRules = new RequiredRules(this); 15 | 16 | this.rule = {}; 17 | this.validations = {}; 18 | this.parseRulesAndFilters(rules, messages, filters); 19 | } 20 | 21 | parseKey(key, data){ 22 | var value; 23 | var self = this; 24 | 25 | let keySplit = key.split('.').filter(function(e){ return e !== ''; }); 26 | 27 | keySplit.map(function(item){ 28 | if(typeof value === 'undefined'){ 29 | value = data && data[item]; 30 | }else{ 31 | value = value[item]; 32 | } 33 | }); 34 | 35 | return value; 36 | } 37 | 38 | addError(key, type, rule, message){ 39 | if (!this.ctx.validationErrors) { 40 | this.ctx.validationErrors = []; 41 | } 42 | 43 | var e = {}; 44 | e[key] = { 45 | type: type, 46 | message: message 47 | }; 48 | 49 | if(type === 'filter') e[key].filter = rule; 50 | else e[key].rule = rule; 51 | 52 | this.ctx.validationErrors.push(e); 53 | } 54 | 55 | changeFieldValue(key, value){ 56 | key = key.split('.') 57 | 58 | if(typeof key[1] === 'undefined'){ 59 | if(typeof this.fields[key[0]] !== 'undefined') this.fields[key[0]] = value; 60 | }else{ 61 | var lastField = this.fields; 62 | for (var i = 0; i < key.length; ++i){ 63 | if(typeof lastField[key[i]] !== 'undefined'){ 64 | if(i === key.length - 1){ 65 | lastField[key[i]] = value; 66 | }else{ 67 | lastField = lastField[key[i]] 68 | } 69 | }else{ 70 | break; 71 | } 72 | } 73 | } 74 | 75 | return; 76 | } 77 | 78 | parseRulesAndFilters(rules, messages, filters) { 79 | 80 | let rsplit 81 | , argsplit 82 | , args 83 | , fieldfilters 84 | , fieldValue 85 | , fbSplit 86 | , faSplit 87 | , fbargsSplit 88 | , faargsSplit; 89 | 90 | if(typeof filters.before === 'undefined') { filters.before = {} }; 91 | if(typeof filters.after === 'undefined') { filters.after = {} }; 92 | 93 | if(Object.keys(rules).length){ 94 | for (var r in rules){ 95 | if(!this.validations[r]){ 96 | this.validations[r] = { 97 | field: r, 98 | value: this.parseKey(r, this.fields), 99 | required: false, 100 | rules: [], 101 | filters: { 102 | before: [], 103 | after: [] 104 | } 105 | }; 106 | } 107 | 108 | if(typeof rules[r] === 'object'){ 109 | for(var er in rules[r]){ 110 | this.rule = { rule: er }; 111 | 112 | if(Array.isArray(rules[r][er]) && rules[r][er].length){ 113 | this.rule.args = (rules[r][er].length > 1) ? rules[r][er]: rules[r][er][0]; 114 | } 115 | 116 | this.populateRule(r, rules[r], messages); 117 | } 118 | }else{ 119 | rsplit = rules[r].split('|'); 120 | 121 | for (var rs in rsplit){ 122 | argsplit = rsplit[rs].split(':'); 123 | if(typeof argsplit[1] !== 'undefined'){ 124 | args = argsplit[1].split(','); 125 | this.rule = { rule: argsplit[0], args: (args.length > 1) ? args: args[0] }; 126 | }else{ 127 | this.rule = { rule: argsplit[0] }; 128 | } 129 | 130 | this.populateRule(r, argsplit[0], messages); 131 | } 132 | } 133 | } 134 | } 135 | 136 | if(Object.keys(filters.before).length){ 137 | for(var bf in filters.before){ 138 | this.populateFilters('before', bf, filters.before[bf]); 139 | } 140 | } 141 | 142 | if(Object.keys(filters.after).length){ 143 | for(var ba in filters.after){ 144 | this.populateFilters('after', ba, filters.after[ba]); 145 | } 146 | } 147 | } 148 | 149 | populateRule(field, rule, messages){ 150 | if(typeof messages[field+'.'+rule] !== 'undefined'){ 151 | this.rule.message = messages[field+'.'+rule]; 152 | }else if(typeof messages[rule] !== 'undefined'){ 153 | this.rule.message = messages[rule]; 154 | } 155 | 156 | if(this.rule.message){ 157 | if(this.rule.message.indexOf(':attribute') !== -1){ 158 | this.rule.message = eachRule.message.replace(':attribute', r); 159 | } 160 | 161 | if(this.rule.message.indexOf(':value') !== -1){ 162 | if(typeof this.validations[field].value === 'object'){ 163 | this.rule.message = eachRule.message.replace(':value', JSON.stringify(this.validations[field].value)); 164 | }else if(typeof this.validations[field].value === 'undefined'){ 165 | this.rule.message = eachRule.message.replace(':value', 'undefined'); 166 | }else{ 167 | this.rule.message = eachRule.message.replace(':value', this.validations[field].value.toString()); 168 | } 169 | } 170 | } 171 | 172 | if(typeof this.requiredRules[this.rule.rule] === 'function'){ 173 | this.validations[field].rules.unshift(this.rule); 174 | }else{ 175 | this.validations[field].rules.push(this.rule); 176 | } 177 | 178 | this.rule = {}; 179 | } 180 | 181 | populateFilters(type, field, filters){ 182 | 183 | let fSplit, fargsSplit, typeFilters, args; 184 | 185 | if(!this.validations[field]){ 186 | this.validations[field] = { 187 | field: field, 188 | value: this.parseKey(field, this.fields), 189 | required: false, 190 | rules: [], 191 | filters: { 192 | before: [], 193 | after: [] 194 | } 195 | }; 196 | } 197 | 198 | if(type === 'before'){ 199 | typeFilters = this.validations[field].filters.before; 200 | }else if(type === 'after'){ 201 | typeFilters = this.validations[field].filters.after; 202 | } 203 | 204 | if(typeof filters === 'object'){ 205 | for(var ef in filters){ 206 | if(Array.isArray(filters[ef]) && filters[ef].length){ 207 | typeFilters.push({ filter: ef, args: filters[ef] }); 208 | } 209 | } 210 | }else{ 211 | fSplit = filters.split('|'); 212 | for (var f = 0; f < fSplit.length; ++f){ 213 | fargsSplit = fSplit[f].split(':'); 214 | if(typeof fargsSplit[1] !== 'undefined'){ 215 | args = fargsSplit[1].split(','); 216 | typeFilters.push({ filter: fargsSplit[0], args: args }); 217 | }else{ 218 | typeFilters.push({ filter: fargsSplit[0] }); 219 | } 220 | } 221 | } 222 | } 223 | 224 | get valid(){ 225 | return this.applyRulesAndFilters(); 226 | } 227 | 228 | * applyRulesAndFilters(){ 229 | let fieldValidations = []; 230 | 231 | for(var i in this.validations){ 232 | fieldValidations.push(this.evaluateField(this.validations[i])); 233 | } 234 | 235 | if (fieldValidations.length) 236 | yield fieldValidations; 237 | 238 | return (this.ctx.validationErrors && this.ctx.validationErrors.length) ? false : true; 239 | } 240 | 241 | * evaluateField(field){ 242 | let proceed = true; 243 | 244 | if(field.filters.before.length){ 245 | for (var fb = 0; fb < field.filters.before.length; ++fb){ 246 | if(typeof this.filters[field.filters.before[fb].filter] !== 'undefined'){ 247 | if(typeof field.value !== 'undefined' && field.value.toString().trim()){ 248 | field.value = yield this.filters[field.filters.before[fb].filter].apply(this.filters, [field.field, field.value].concat( field.filters.before[fb].args || [] )); 249 | if(typeof field.value === 'undefined'){ 250 | break; 251 | }else{ 252 | this.changeFieldValue(field.field, field.value); 253 | } 254 | } 255 | }else{ 256 | this.addError(field.field, 'filter', field.rules[r].rule,'Invalid filter: '+ field.filters.before[fb].filter +' does not exist'); 257 | proceed = false; 258 | break; 259 | } 260 | } 261 | } 262 | 263 | if(!proceed) { 264 | return; 265 | } 266 | 267 | if(field.rules.length){ 268 | for(var r = 0; r < field.rules.length; ++r){ 269 | if(typeof this.requiredRules[field.rules[r].rule] === 'function'){ 270 | var ruleArgs = [field.field, field.value]; 271 | 272 | if(field.rules[r].args) 273 | ruleArgs.push(field.rules[r].args); 274 | 275 | if(field.rules[r].message) 276 | ruleArgs.push(field.rules[r].message); 277 | 278 | if(yield this.requiredRules[field.rules[r].rule].apply(this.requiredRules, ruleArgs)){ 279 | field.required = true; 280 | }else{ 281 | proceed = false; 282 | break; 283 | } 284 | }else if(typeof this.rules[field.rules[r].rule] === 'function'){ 285 | if((!field.required && typeof field.value !== 'undefined') || field.required){ 286 | var ruleArgs = [field.field, field.value]; 287 | 288 | if(field.rules[r].args) 289 | ruleArgs.push(field.rules[r].args); 290 | 291 | if(field.rules[r].message) 292 | ruleArgs.push(field.rules[r].message); 293 | 294 | if(!(yield this.rules[field.rules[r].rule].apply(this.rules, ruleArgs))){ 295 | proceed = false; 296 | break; 297 | } 298 | }else{ 299 | proceed = false; 300 | break; 301 | } 302 | }else{ 303 | this.addError(field.field, 'rule', field.rules[r].rule, 'Invalid Validation Rule: '+ field.rules[r].rule +' does not exist'); 304 | proceed = false; 305 | break; 306 | } 307 | } 308 | } 309 | 310 | if(!proceed) { 311 | return; 312 | } 313 | 314 | if(field.filters.after.length){ 315 | for (var fa = 0; fa < field.filters.after.length; ++fa){ 316 | if(typeof this.filters[field.filters.after[fa].filter] !== 'undefined'){ 317 | if(typeof field.value !== 'undefined' && field.value.toString().trim()){ 318 | field.value = yield this.filters[field.filters.after[fa].filter].apply(this.filters, [field.field, field.value].concat( field.filters.after[fa].args || [] )); 319 | if(typeof field.value !== 'undefined'){ 320 | proceed = false; 321 | break; 322 | }else{ 323 | this.changeFieldValue(field.field, field.value); 324 | } 325 | } 326 | }else{ 327 | this.addError(field.field, 'filter', field.filters.after[fa].filter, 'Invalid filter: '+ field.filters.after[fa].filter +' does not exist'); 328 | proceed = false; 329 | break; 330 | } 331 | } 332 | } 333 | 334 | return; 335 | } 336 | } 337 | 338 | module.exports = FieldValidator; 339 | -------------------------------------------------------------------------------- /lib/fileActions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var fs = require('co-fs-extra') 4 | 5 | class FileActions { 6 | constructor(Validator){ 7 | this.validator = Validator; 8 | } 9 | 10 | * move(field, file, deleteOnFail, destination, callback){ 11 | try{ 12 | yield fs.move(file.path, destination, { clobber: true }); 13 | if(callback){ 14 | if(yield callback(this.validator, file, destination)){ 15 | return true; 16 | }else{ 17 | if(deleteOnFail){ 18 | if(file.path && (yield fs.exists(file.path))){ 19 | yield fs.remove(file.path); 20 | } 21 | } 22 | 23 | return false; 24 | } 25 | }else{ 26 | return true; 27 | } 28 | } catch (e){ 29 | this.validator.addError(field, 'action', 'move', 'The file could not be moved to the destination provided'); 30 | if(deleteOnFail){ 31 | if(file.path && (yield fs.exists(file.path))){ 32 | yield fs.remove(file.path); 33 | } 34 | } 35 | 36 | return false; 37 | } 38 | } 39 | 40 | * copy(field, file, deleteOnFail, destination, callback){ 41 | 42 | try { 43 | yield fs.copy(file.path, destination, { clobber: true }); 44 | 45 | if(callback){ 46 | if(yield callback(this.validator, file, destination)){ 47 | return true; 48 | }else{ 49 | if(deleteOnFail){ 50 | if(file.path && (yield fs.exists(file.path))){ 51 | yield fs.remove(file.path); 52 | } 53 | } 54 | return false; 55 | } 56 | }else { 57 | return true; 58 | } 59 | } catch (e){ 60 | this.validator.addError(field, 'action', 'copy', 'The file could not be copied to the destination provided'); 61 | if(deleteOnFail){ 62 | if(file.path && (yield fs.exists(file.path))){ 63 | yield fs.remove(file.path); 64 | } 65 | } 66 | return false; 67 | } 68 | } 69 | 70 | * remove(field, file, deleteOnFail, args, callback){ 71 | try { 72 | yield fs.remove(file.path); 73 | 74 | if(callback) { 75 | return (yield callback(this.validator, file.path)); 76 | }else{ 77 | return true; 78 | } 79 | 80 | }catch(e){ 81 | this.validator.addError(field, 'action', 'delete', 'The original uploaded file could not be deleted'); 82 | return false; 83 | } 84 | } 85 | } 86 | 87 | module.exports = FileActions; 88 | -------------------------------------------------------------------------------- /lib/fileRules.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('co-fs-extra') 4 | , mime = require('mime-types') 5 | , path = require('path'); 6 | 7 | class FileRules { 8 | constructor(Validator){ 9 | this.validator = Validator; 10 | } 11 | 12 | fetchByteSize(size){ 13 | size = size.toString().toLowerCase(); 14 | if(size.includes('gb') || size.includes('g')){ 15 | return parseInt(size.replace('gb','').replace('g','')) * 1024 * 1024 * 1024; 16 | }else if(size.includes('mb') || size.includes('m')){ 17 | return parseInt(size.replace('mb','').replace('m','')) * 1024 * 1024; 18 | }else if(size.includes('kb') || size.includes('k')){ 19 | return parseInt(size.replace('kb','').replace('k','')) * 1024; 20 | }else if(size.includes('b')){ 21 | return parseInt(size.replace('b','')); 22 | }else{ 23 | return parseInt(size) * 1024; 24 | } 25 | } 26 | 27 | * size(field, file, deleteOnFail, args, message){ 28 | let success = true; 29 | if(args && Array.isArray(args) && args.length >= 2 && args.length % 2 === 0){ 30 | let max, min; 31 | for (var i = 0; i < args.length; ++i){ 32 | if(args[i] === 'max'){ 33 | max = this.fetchByteSize(args[i + 1]); 34 | }else if(args[i] === 'min'){ 35 | min = this.fetchByteSize(args[i + 1]); 36 | } 37 | } 38 | 39 | if(!min && !max){ 40 | this.validator.addError(field, 'rule', 'size', 'Max or Min properties have not been provided'); 41 | success = false; 42 | }else{ 43 | if(max && file.size >= max){ 44 | this.validator.addError(field, 'rule', 'size', message || 'The file size exceeds the max size provided'); 45 | success = false; 46 | } 47 | 48 | if(min && file.size <= min){ 49 | this.validator.addError(field, 'rule', 'size', message || 'The file size is lower than the min size stated'); 50 | success = false; 51 | } 52 | } 53 | }else{ 54 | this.validator.addError(field, 'rule', 'size', 'Invalid Arguments provided for the size arguement') 55 | success = false; 56 | } 57 | 58 | if(!success){ 59 | if(deleteOnFail){ 60 | if(file.path && (yield fs.exists(file.path))){ 61 | yield fs.remove(file.path); 62 | } 63 | } 64 | 65 | return false; 66 | }else{ 67 | return true; 68 | } 69 | } 70 | 71 | * extension(field, file, deleteOnFail, args, message){ 72 | let success = true; 73 | 74 | if(Array.isArray(args)){ 75 | for(var i = 0; i < args.length; ++i){ 76 | if(path.extname(file.name).replace('.','').toLowerCase() !== args[i].toLowerCase()){ 77 | success = false; 78 | }else{ 79 | success = true; 80 | break; 81 | } 82 | } 83 | }else{ 84 | if( path.extname(file.name).replace('.','').toLowerCase() !== args.toLowerCase() ){ 85 | success = false; 86 | } 87 | } 88 | 89 | if(!success){ 90 | this.validator.addError(field, 'rule', 'extension', message || 'The extension mentioned did not match with the one of the file'); 91 | 92 | if(deleteOnFail){ 93 | if(file.path && (yield fs.exists(file.path))){ 94 | yield fs.remove(file.path); 95 | } 96 | } 97 | 98 | return false; 99 | }else{ 100 | return true; 101 | } 102 | } 103 | 104 | * mime(field, file, deleteOnFail, args, message){ 105 | let success = true; 106 | 107 | if(Array.isArray(args)){ 108 | for(var i = 0; i < args.length; ++i){ 109 | if(mime.lookup(args[i]) !== file.type){ 110 | success = false; 111 | }else{ 112 | success = true; 113 | break; 114 | } 115 | } 116 | }else{ 117 | if(mime.lookup(args) !== file.type){ 118 | success = false; 119 | } 120 | } 121 | 122 | if(!success){ 123 | this.validator.addError(field, 'rule', 'mime', message || 'The mime type mentioned does not match with the file type'); 124 | if(deleteOnFail){ 125 | if(file.path && (yield fs.exists(file.path))){ 126 | yield fs.remove(file.path); 127 | } 128 | } 129 | return false; 130 | }else{ 131 | return true; 132 | } 133 | } 134 | 135 | * image(field, file, deleteOnFail, message) { 136 | let success = true; 137 | 138 | if( 0 !== file.type.indexOf('image/')){ 139 | success = false; 140 | } 141 | 142 | if(!success){ 143 | this.validator.addError(field, 'rule', 'image', message || 'The file type posted is not an image'); 144 | 145 | if(deleteOnFail){ 146 | if(file.path && (yield fs.exists(file.path))){ 147 | yield fs.remove(file.path); 148 | } 149 | } 150 | 151 | return false; 152 | }else{ 153 | return true; 154 | } 155 | } 156 | 157 | * name(field, file, deleteOnFail, filename, message){ 158 | let success = true; 159 | 160 | if(path.basename(file.name, path.extname(file.name)) !== filename && path.basename(file.name) !== filename){ 161 | success = false 162 | } 163 | 164 | if(!success){ 165 | this.validator.addError(field, 'rule', 'name', message || 'The filename did not match with the posted file'); 166 | 167 | if(deleteOnFail){ 168 | if(file.path && (yield fs.exists(file.path))){ 169 | yield fs.remove(file.path); 170 | } 171 | } 172 | 173 | return false; 174 | }else{ 175 | return true; 176 | } 177 | } 178 | } 179 | 180 | module.exports = FileRules; 181 | -------------------------------------------------------------------------------- /lib/fileValidator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var RequiredRules = require('./requiredRules'); 4 | var FileRules = require('./fileRules'); 5 | var FileActions = require('./fileActions'); 6 | 7 | class FileValidator{ 8 | constructor(context, fields, rules, deleteOnFail, messages, actions) { 9 | this.ctx = context; 10 | this.fields = fields; 11 | 12 | this.requiredRules = new RequiredRules(this); 13 | this.rules = new FileRules(this); 14 | this.actions = new FileActions(this); 15 | 16 | this.rule = {}; 17 | this.validations = {}; 18 | this.parseRulesAndActions(rules, deleteOnFail, messages, actions); 19 | } 20 | 21 | parseKey(key, data){ 22 | var value; 23 | var self = this; 24 | 25 | let keySplit = key.split('.').filter(function(e){ return e !== ''; }); 26 | 27 | keySplit.map(function(item){ 28 | if(typeof value === 'undefined'){ 29 | value = data && data[item]; 30 | }else{ 31 | value = value[item]; 32 | } 33 | }); 34 | 35 | return value; 36 | } 37 | 38 | addError(key, type, rule, message){ 39 | if (!this.ctx.validationErrors) { 40 | this.ctx.validationErrors = []; 41 | } 42 | 43 | var e = {}; 44 | e[key] = { 45 | type: type, 46 | message: message 47 | }; 48 | 49 | if(type === 'action') e[key].action = rule; 50 | else e[key].rule = rule; 51 | 52 | this.ctx.validationErrors.push(e); 53 | } 54 | 55 | parseRulesAndActions(rules, deleteOnFail, messages, actions){ 56 | let rsplit 57 | , argsplit 58 | , args; 59 | 60 | if(Object.keys(rules).length){ 61 | for (var r in rules){ 62 | 63 | if(!this.validations[r]){ 64 | this.validations[r] = { 65 | field: r, 66 | value: this.parseKey(r, this.fields), 67 | required: false, 68 | rules: [], 69 | actions: [], 70 | deleteOnFail: deleteOnFail 71 | }; 72 | } 73 | 74 | if(typeof rules[r] === 'object'){ 75 | for(var er in rules[r]){ 76 | this.rule = { rule: er }; 77 | 78 | if(Array.isArray(rules[r][er]) && rules[r][er].length){ 79 | this.rule.args = (rules[r][er].length > 1) ? rules[r][er]: rules[r][er][0]; 80 | } 81 | 82 | this.populateRule(r, rules[r], messages); 83 | } 84 | }else{ 85 | rsplit = rules[r].split('|'); 86 | 87 | for (var rs in rsplit){ 88 | argsplit = rsplit[rs].split(':'); 89 | if(typeof argsplit[1] !== 'undefined'){ 90 | args = argsplit[1].split(','); 91 | this.rule = { rule: argsplit[0], args: (args.length > 1) ? args: args[0] }; 92 | }else{ 93 | this.rule = { rule: argsplit[0] }; 94 | } 95 | 96 | this.populateRule(r, argsplit[0], messages); 97 | } 98 | } 99 | } 100 | } 101 | 102 | if(Object.keys(actions).length){ 103 | for (var a in actions){ 104 | if(!this.validations[a]){ 105 | this.validations[a] = { 106 | field: a, 107 | value: this.parseKey(a, this.fields), 108 | required: false, 109 | rules: [], 110 | actions: [] 111 | }; 112 | } 113 | 114 | if(typeof actions[a] === 'object'){ 115 | if(Array.isArray(actions[a])){ 116 | for (var i = 0; i < actions[a].length; ++i){ 117 | this.populateAction(a, actions[a][i]); 118 | } 119 | }else{ 120 | this.populateAction(a, actions[a]); 121 | } 122 | } 123 | } 124 | } 125 | } 126 | 127 | populateRule(field, rule, messages){ 128 | if(typeof messages[field+'.'+rule] !== 'undefined'){ 129 | this.rule.message = messages[field+'.'+rule]; 130 | }else if(typeof messages[rule] !== 'undefined'){ 131 | this.rule.message = messages[rule]; 132 | } 133 | 134 | if(this.rule.message){ 135 | if(this.rule.message.indexOf(':attribute') !== -1){ 136 | this.rule.message = eachRule.message.replace(':attribute', r); 137 | } 138 | 139 | if(this.rule.message.indexOf(':value') !== -1){ 140 | if(typeof this.validations[field].value === 'object'){ 141 | this.rule.message = eachRule.message.replace(':value', JSON.stringify(this.validations[field].value)); 142 | }else if(typeof this.validations[field].value === 'undefined'){ 143 | this.rule.message = eachRule.message.replace(':value', 'undefined'); 144 | }else{ 145 | this.rule.message = eachRule.message.replace(':value', this.validations[field].value.toString()); 146 | } 147 | } 148 | } 149 | 150 | if(typeof this.requiredRules[this.rule.rule] === 'function'){ 151 | this.validations[field].rules.unshift(this.rule); 152 | }else{ 153 | this.validations[field].rules.push(this.rule); 154 | } 155 | 156 | this.rule = {}; 157 | } 158 | 159 | populateAction(field, action){ 160 | let eachAction, args; 161 | if(typeof action !== 'undefined'){ 162 | if(typeof action === 'object'){ 163 | if(Array.isArray(action)){ 164 | for(var i = 0; i < action.length; ++i){ 165 | eachAction = { action: action[i].action }; 166 | eachAction.args = action.args[i]; 167 | 168 | if(action.callback 169 | && typeof action.callback === 'function' 170 | && action.callback.constructor 171 | && 'GeneratorFunction' === action.callback.constructor.name){ 172 | 173 | eachAction.callback = action.callback; 174 | } 175 | 176 | this.validations[field].actions.push(eachAction); 177 | } 178 | }else{ 179 | eachAction = { action: action.action }; 180 | eachAction.args = action.args; 181 | 182 | if(action.callback 183 | && typeof action.callback === 'function' 184 | && action.callback.constructor 185 | && 'GeneratorFunction' === action.callback.constructor.name){ 186 | 187 | eachAction.callback = action.callback; 188 | } 189 | 190 | this.validations[field].actions.push(eachAction); 191 | } 192 | } 193 | 194 | eachAction = null; 195 | } 196 | } 197 | 198 | get valid(){ 199 | return this.applyRulesAndActions(); 200 | } 201 | 202 | * applyRulesAndActions(){ 203 | let fieldValidations = []; 204 | 205 | for(var i in this.validations){ 206 | fieldValidations.push(this.evaluateField(this.validations[i])); 207 | } 208 | 209 | if (fieldValidations.length) 210 | yield fieldValidations; 211 | 212 | return (this.ctx.validationErrors && this.ctx.validationErrors.length) ? false : true; 213 | } 214 | 215 | * evaluateField(field){ 216 | let proceed = true; 217 | 218 | if(field.rules.length){ 219 | for(var r = 0; r < field.rules.length; ++r){ 220 | if(typeof this.requiredRules[field.rules[r].rule] === 'function'){ 221 | var ruleArgs = [field.field, field.value]; 222 | 223 | if(field.rules[r].args && field.rules[r].args.length) 224 | ruleArgs.push(field.rules[r].args); 225 | 226 | if(field.rules[r].message) 227 | ruleArgs.push(field.rules[r].message); 228 | 229 | if(yield this.requiredRules[field.rules[r].rule].apply(this.requiredRules, ruleArgs)){ 230 | field.required = true; 231 | }else{ 232 | proceed = false; 233 | break; 234 | } 235 | }else if(typeof this.rules[field.rules[r].rule] === 'function'){ 236 | if((!field.required && typeof field.value !== 'undefined') || field.required){ 237 | var ruleArgs = [field.field, field.value, field.deleteOnFail]; 238 | 239 | if(field.rules[r].args && field.rules[r].args.length) 240 | ruleArgs.push(field.rules[r].args); 241 | 242 | if(field.rules[r].message) 243 | ruleArgs.push(field.rules[r].message); 244 | 245 | if(!(yield this.rules[field.rules[r].rule].apply(this.rules, ruleArgs))){ 246 | proceed = false; 247 | break; 248 | } 249 | }else{ 250 | proceed = false; 251 | break; 252 | } 253 | }else{ 254 | this.addError(field.field, 'rule', field.rules[r].rule, 'Invalid Validation Rule: '+ field.rules[r].rule +' does not exist'); 255 | proceed = false; 256 | break; 257 | } 258 | } 259 | } 260 | 261 | if(!proceed){ 262 | return; 263 | } 264 | 265 | let fieldAction; 266 | 267 | if (field.actions.length){ 268 | for (var a = 0; a < field.actions.length; ++a){ 269 | if(typeof this.actions[field.actions[a].action] !== 'undefined'){ 270 | if(typeof field.value !== 'undefined'){ 271 | let args = [field.field, field.value, field.deleteOnFail, field.actions[a].args || [] ]; 272 | if(field.actions[a].callback) args.push(field.actions[a].callback); 273 | fieldAction = yield this.actions[field.actions[a].action].apply( this.actions, args ); 274 | 275 | if(!fieldAction){ 276 | break; 277 | } 278 | } 279 | }else{ 280 | this.addError(field.field, 'action', field.actions[a].action, 'Invalid action: '+ field.actions[a].action +' does not exist'); 281 | proceed = false; 282 | break; 283 | } 284 | } 285 | } 286 | 287 | return; 288 | } 289 | } 290 | 291 | module.exports = FileValidator; 292 | -------------------------------------------------------------------------------- /lib/filters.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var v = require('validator') 4 | , crypto = require('crypto'); 5 | 6 | class Filters{ 7 | constructor(Validator){ 8 | this.validator = Validator; 9 | } 10 | 11 | * integer(field, value){ 12 | let eVal = v.toInt(value); 13 | if(!isNaN(eVal)){ 14 | return eVal; 15 | }else{ 16 | this.validator.addError(field, 'filter', 'integer', 'The value for '+field+' cannot be converted to an integer.'); 17 | return; 18 | } 19 | } 20 | 21 | * float(field, value){ 22 | let eVal = v.toFloat(value); 23 | if(!isNaN(eVal)){ 24 | return eVal; 25 | }else{ 26 | this.validator.addError(field, 'filter', 'float', 'The value for '+field+' cannot be converted to a Float.'); 27 | return; 28 | } 29 | } 30 | 31 | * lowercase(field, value){ 32 | try { 33 | return value.toLowerCase(); 34 | } catch(e) { 35 | this.validator.addError(field, 'filter', 'lowercase', 'The value for '+field+' cannot be converted to lowercase.'); 36 | return; 37 | } 38 | } 39 | 40 | * uppercase(field, value){ 41 | try { 42 | return value.toUpperCase(); 43 | } catch(e) { 44 | this.validator.addError(field, 'filter', 'uppercase', 'The value for '+field+' cannot be converted to uppercase.'); 45 | return; 46 | } 47 | } 48 | 49 | * boolean(field, value){ 50 | return v.toBoolean(value); 51 | } 52 | 53 | * json(field, value){ 54 | try { 55 | return JSON.stringify(value); 56 | } catch(e) { 57 | this.validator.addError(field, 'filter', 'json', 'Invalid string cannot be converted to JSON'); 58 | return 59 | } 60 | } 61 | 62 | * trim(field, value, separator){ 63 | let eVal = (separator) ? v.trim(value, separator) : v.trim(value); 64 | if(typeof eVal === 'string'){ 65 | return eVal; 66 | }else{ 67 | this.validator.addError(field, 'filter', 'trim', 'The value for '+field+' cannot be trimmed as the data type is invalid'); 68 | return 69 | } 70 | } 71 | 72 | * ltrim(field, value, separator){ 73 | let eVal = (separator) ? v.ltrim(value, separator) : v.ltrim(value); 74 | if(typeof eVal === 'string'){ 75 | return eVal; 76 | }else{ 77 | this.validator.addError(field, 'filter', 'ltrim', 'The value for '+field+' cannot be escaped as the data type is invalid'); 78 | return 79 | } 80 | } 81 | 82 | * rtrim(field, value, separator){ 83 | let eVal = (separator) ? v.rtrim(value, separator) : v.rtrim(value); 84 | if(typeof eVal === 'string'){ 85 | return eVal; 86 | }else{ 87 | this.validator.addError(field, 'filter', 'rtrim', 'The value for '+field+' cannot be trimmed as the data type is invalid'); 88 | return 89 | } 90 | } 91 | 92 | * escape(field, value){ 93 | let eVal = v.escape(value); 94 | 95 | if(typeof eVal === 'string'){ 96 | return eVal; 97 | }else{ 98 | this.validator.addError(field, 'filter', 'escape', 'The value for '+field+' cannot be trimmed as the data type is invalid'); 99 | return 100 | } 101 | } 102 | 103 | * replace(field, value, original, replacement){ 104 | if(!original || !replacement) { 105 | this.validator.addError(field, 'filter', 'replace', 'The arguements for relacing the provided string are missing'); 106 | return 107 | } 108 | 109 | try { 110 | return value.replace(original, replacement) 111 | } catch(e) { 112 | this.validator.addError(field, 'filter', 'replace', 'The value for '+field+' is not a valid string and hence cannot be replaced.'); 113 | return 114 | } 115 | } 116 | 117 | * hex(field, value, alg, enc){ 118 | enc = enc || 'hex'; 119 | try { 120 | return crypto.createHash(alg).update(value).digest(enc); 121 | }catch(e){ 122 | this.validator.addError(field, 'filter', 'hex', 'The valur or arguements required to hex the field are invalid'); 123 | return; 124 | } 125 | } 126 | 127 | * sha1(field, value){ 128 | try { 129 | return crypto.createHash('sha1').update(value).digest('hex'); 130 | }catch(e){ 131 | this.validator.addError(field, 'filter', 'sha1', 'The value you tried to sha1 is invalid'); 132 | return; 133 | } 134 | } 135 | 136 | * md5(field, value){ 137 | try { 138 | return crypto.createHash('md5').update(value).digest('hex'); 139 | }catch(e){ 140 | this.validator.addError(field, 'filter', 'md5', 'The value you tried to md5 is invalid'); 141 | return; 142 | } 143 | } 144 | } 145 | 146 | module.exports = Filters; 147 | -------------------------------------------------------------------------------- /lib/requiredRules.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class RequiredRules { 4 | constructor(Validator){ 5 | this.validator = Validator; 6 | } 7 | 8 | * required(field, value, message){ 9 | 10 | if(typeof value === 'undefined' || value === null || !value.toString().trim()){ 11 | if(this.validator.constructor.name === 'FileValidator'){ 12 | if(typeof value === 'undefined' || value.size <= 0){ 13 | this.validator.addError(field, 'requiredRule', 'required', message || 'The '+ field +' is mandatory.'); 14 | return false; 15 | } 16 | } 17 | 18 | this.validator.addError(field, 'requiredRule', 'required', message || 'The '+ field +' is mandatory.'); 19 | return false; 20 | } 21 | 22 | return true; 23 | } 24 | 25 | * requiredIf(field, value, args, message){ 26 | if(args.length >= 2){ 27 | if(args.length % 2 === 0){ 28 | var argGroup, start, required = false, sameField = false; 29 | for (var i = 0; i < args.length / 2; ++i){ 30 | if(!i) start = i 31 | else start += 2; 32 | 33 | argGroup = args.slice(start, start + 2); 34 | if(argGroup[0] != field){ 35 | if(typeof this.validator.fields[argGroup[0]] !== 'undefined' && this.validator.fields[argGroup[0]] == argGroup[1]){ 36 | required = true; 37 | }else{ 38 | required = false; 39 | break 40 | } 41 | }else{ 42 | sameField = true; 43 | break 44 | } 45 | } 46 | 47 | if(sameField) { 48 | this.validator.addError(field, 'requiredRule', 'requiredIf', message || 'The '+ field +' needs to contain another field name in the args.'); 49 | return false; 50 | } 51 | 52 | if(required && (typeof value === 'undefined' || value === null || !value.toString().trim())){ 53 | this.validator.addError(field, 'requiredRule', 'requiredIf', message || 'The '+ field +' is mandatory.'); 54 | return false; 55 | } 56 | 57 | return required; 58 | }else{ 59 | this.validator.addError( 60 | field, 61 | 'requiredRule', 62 | 'requiredIf', 63 | message || 'The '+ field +' has an incorrect number of arguements. The arguements length needs to be a multiple of 2' 64 | ); 65 | 66 | return false; 67 | } 68 | }else{ 69 | this.validator.addError(field, 'requiredRule', 'requiredIf', message || 'The '+ field +' required a minimum of two arguements'); 70 | return false; 71 | } 72 | } 73 | 74 | * requiredNotIf(field, value, args, message){ 75 | if(args.length >= 2){ 76 | if(args.length % 2 === 0){ 77 | var argGroup, start, required = false, sameField = false; 78 | for (var i = 0; i < args.length / 2; ++i){ 79 | if(!i) start = i 80 | else start += 2; 81 | 82 | argGroup = args.slice(start, start + 2); 83 | 84 | if(argGroup[0] != field){ 85 | if(typeof this.validator.fields[argGroup[0]] !== 'undefined' && this.validator.fields[argGroup[0]] != argGroup[1]){ 86 | required = true; 87 | }else{ 88 | required = false; 89 | break; 90 | } 91 | }else{ 92 | sameField = true; 93 | break; 94 | } 95 | } 96 | 97 | if(sameField) { 98 | this.validator.addError(field, 'requiredRule', 'requiredNotIf', message || 'The '+ field +' needs to contain another field name in the args.'); 99 | return false; 100 | } 101 | 102 | if(required && (typeof value === 'undefined' || value === null || !value.toString().trim())){ 103 | this.validator.addError(field, 'requiredRule', 'requiredNotIf', message || 'The '+ field +' is mandatory.'); 104 | return false; 105 | } 106 | 107 | return required; 108 | }else{ 109 | this.validator.addError( 110 | field, 111 | 'requiredRule', 112 | 'requiredNotIf', 113 | message || 'The '+ field +' has an incorrect number of arguements. The arguements length needs to be a multiple of 2' 114 | ); 115 | return false; 116 | } 117 | }else{ 118 | this.validator.addError(field, 'requiredRule', 'requiredNotIf', message || 'The '+ field +' required a minimum of two arguements'); 119 | return false; 120 | } 121 | } 122 | 123 | * requiredWith(field, value, args, message){ 124 | if(!Array.isArray(args)) args = [args]; 125 | if(args.length){ 126 | var required = false; 127 | for (var i = 0; i < args.length; ++i){ 128 | if(args[i] != field){ 129 | if(this.validator.fields[args[i]] && typeof this.validator.fields[args[i]] !== 'undefined'){ 130 | required = true; 131 | break; 132 | } 133 | } 134 | } 135 | 136 | if(required && (typeof value === 'undefined' || value === null || !value.toString().trim())){ 137 | this.validator.addError(field, 'requiredRule', 'requiredWith', message || 'The '+ field +' is mandatory.'); 138 | return false; 139 | } 140 | 141 | return required; 142 | }else{ 143 | this.validator.addError(field, 'requiredRule', 'requiredWith', message || 'The '+ field +' requires atleast one other field in the arguement'); 144 | return false; 145 | } 146 | } 147 | 148 | * requiredWithout(field, value, args, message){ 149 | if(!Array.isArray(args)) args = [args]; 150 | if(args.length){ 151 | var required = false; 152 | for (var i = 0; i < args.length; ++i){ 153 | if(args[i] != field){ 154 | if(!this.validator.fields[args[i]] || typeof this.validator.fields[args[i]] === 'undefined'){ 155 | required = true; 156 | break; 157 | } 158 | } 159 | } 160 | 161 | if(required && (typeof value === 'undefined' || value === null || !value.toString().trim())){ 162 | this.validator.addError(field, 'requiredRule', 'requiredWithout', message || 'The '+ field +' is mandatory.'); 163 | return false; 164 | } 165 | 166 | return required; 167 | }else{ 168 | this.validator.addError(field, 'requiredRule', 'requiredWithout', message || 'The '+ field +' requires atleast one other field in the arguement'); 169 | return false; 170 | } 171 | } 172 | 173 | * requiredWithAll(field, value, args, message){ 174 | if(!Array.isArray(args)) args = [args]; 175 | if(args.length){ 176 | var required = true; 177 | for (var i = 0; i < args.length; ++i){ 178 | if(args[i] != field){ 179 | if(!this.validator.fields[args[i]] || typeof this.validator.fields[args[i]] === 'undefined'){ 180 | required = false; 181 | break; 182 | } 183 | } 184 | } 185 | 186 | if(required && (typeof value === 'undefined' || value === null || !value.toString().trim())){ 187 | this.validator.addError(field, 'requiredRule', 'requiredWithAll', message || 'The '+ field +' is mandatory.'); 188 | return false; 189 | } 190 | 191 | return required; 192 | }else{ 193 | this.validator.addError(field, 'requiredRule', 'requiredWithAll', message || 'The '+ field +' requires atleast one other field in the arguement'); 194 | return false; 195 | } 196 | } 197 | 198 | * requiredWithoutAll(field, value, args, message){ 199 | if(!Array.isArray(args)) args = [args]; 200 | if(args.length){ 201 | var required = true; 202 | for (var i = 0; i < args.length; ++i){ 203 | if(args[i] != field){ 204 | if(this.validator.fields[args[i]] || typeof this.validator.fields[args[i]] !== 'undefined'){ 205 | required = false; 206 | break; 207 | } 208 | } 209 | } 210 | 211 | if(required && (typeof value === 'undefined' || value === null || !value.toString().trim())){ 212 | this.validator.addError(field, 'requiredRule', 'requiredWithoutAll', message || 'The '+ field +' is mandatory.'); 213 | return false; 214 | } 215 | 216 | return required; 217 | }else{ 218 | this.validator.addError(field, 'requiredRule', 'requiredWithoutAll', message || 'The '+ field +' requires atleast one other field in the arguement'); 219 | return false; 220 | } 221 | } 222 | } 223 | 224 | module.exports = RequiredRules; 225 | -------------------------------------------------------------------------------- /lib/rules.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var v = require('validator') 4 | , moment = require('moment-timezone'); 5 | 6 | const date_formats = [ 7 | moment.ISO_8601, 8 | 'DD-MM-YYYY', 9 | 'DD.MM.YYYY', 10 | 'DD/MM/YYYY', 11 | 'D-M-YYYY', 12 | 'D.M.YYYY', 13 | 'D/M/YYYY', 14 | 'YYYY-MM-DD HH:mm:Z', 15 | 'YYYY-MM-DD HH:mm:ZZ', 16 | 'YYYY-MM-DD HH:mm Z' 17 | ]; 18 | 19 | class Rules { 20 | constructor(Validator) { 21 | this.validator = Validator; 22 | } 23 | 24 | * accepted(field, value, message){ 25 | if(value === true || value === 'yes' || value === 'on' || value === 1 || value === "1"){ 26 | return true; 27 | }else{ 28 | this.validator.addError(field, 'rule', 'accepted', message || 'The value of the field needs to be between 1, yes, or true'); 29 | return false; 30 | } 31 | } 32 | 33 | * after(field, value, afterDate, message){ 34 | let mAfterDate, mDate; 35 | if(typeof this.validator.validations[field].date_format !== 'undefined'){ 36 | mAfterDate = moment(afterDate, date_formats.concat([this.validator.validations[field].date_format])); 37 | mDate = moment(value, this.validator.validations[field].date_format, true); 38 | }else{ 39 | mAfterDate = moment(afterDate, date_formats); 40 | mDate = moment(value, date_formats); 41 | } 42 | 43 | if(message){ 44 | message = message.replace(':afterDate', afterDate); 45 | } 46 | 47 | if(!mAfterDate.isValid()){ 48 | this.validator.addError(field, 'rule', 'after', 'The after date arguement is an invalid date'); 49 | return false; 50 | }else if(!mDate.isValid()){ 51 | this.validator.addError(field, 'rule', 'after', 'The value of the field is an invalid date'); 52 | return false; 53 | }else if(mAfterDate.valueOf() > mDate.valueOf()){ 54 | this.validator.addError(field, 'rule', 'after', message || 'The provided date does not fall after the date mentioned in the arguement'); 55 | return false; 56 | } 57 | 58 | return true; 59 | } 60 | 61 | * alpha(field, value, message){ 62 | if(!v.isAlpha(value)){ 63 | this.validator.addError(field, 'rule', 'alpha', message || 'The value of the field needs to be alphabetical'); 64 | return false; 65 | } 66 | 67 | return true; 68 | } 69 | 70 | * alphaDash(field, value, message){ 71 | if(!(/^[A-Z0-9_-]+$/i.test(value))){ 72 | this.validator.addError(field, 'rule', 'alphaDash', message || 'The field value can only contain aplhabets, _ and -'); 73 | return false; 74 | } 75 | 76 | return true; 77 | } 78 | 79 | * alphaNumeric(field, value, message){ 80 | if(!v.isAlphanumeric(value)){ 81 | this.validator.addError(field, 'rule', 'alphaNumeric', message || 'The value of the field can only contain letters and numbers'); 82 | return false; 83 | } 84 | 85 | return true; 86 | } 87 | 88 | * before(field, value, beforeDate, message){ 89 | let mBeforeDate, mDate; 90 | if(typeof this.validator.validations[field].date_format !== 'undefined'){ 91 | mBeforeDate = moment(beforeDate, date_formats.concat([this.validator.validations[field].date_format])); 92 | mDate = moment(value, this.validator.validations[field].date_format, true); 93 | }else{ 94 | mBeforeDate = moment(beforeDate, date_formats); 95 | mDate = moment(value, date_formats); 96 | } 97 | 98 | if(message){ 99 | message = message.replace(':beforeDate', beforeDate); 100 | } 101 | 102 | if(!mBeforeDate.isValid()){ 103 | this.validator.addError(field, 'rule', 'before', message || 'The before date arguement is an invalid date'); 104 | return false; 105 | }else if(!mDate.isValid()){ 106 | this.validator.addError(field, 'rule', 'before', message || 'The value of the field is an invalid date'); 107 | return false; 108 | }else if(mBeforeDate.valueOf() < mDate.valueOf()){ 109 | this.validator.addError(field, 'rule', 'before', message || 'The provided date does not come before the date mentioned in the arguement'); 110 | return false; 111 | } 112 | 113 | return true; 114 | } 115 | 116 | //Length of charachters 117 | * between(field, value, args, message){ 118 | 119 | if(!Array.isArray(args) && args.length !== 2){ 120 | this.validator.addError(field, 'rule', 'between', 'The number of arguements in the field are invalid'); 121 | return false; 122 | }else{ 123 | if(!v.isInt(args[0]) || !v.isInt(args[1])){ 124 | this.validator.addError(field, 'rule', 'between', 'The rule arguements for the field need to be integers'); 125 | return false; 126 | }else if( parseInt(args[0]) >= parseInt(args[1]) ){ 127 | this.validator.addError(field, 'rule', 'between', 'The rule arguement for the min value cannot be greater than or equal to the max value'); 128 | return false; 129 | }else if(value.toString().length < parseInt(args[0]) || value.toString().length > parseInt(args[1])){ 130 | if(message){ 131 | if(message){ 132 | message = message.replace(':minLength', args[0]).replace(':maxLength', args[1]); 133 | } 134 | } 135 | 136 | this.validator.addError(field, 'rule', 'between', 'The size of the field is not within the specified range'); 137 | return false; 138 | } 139 | } 140 | 141 | return true; 142 | } 143 | 144 | * boolean(field, value, message){ 145 | if(value === true || value === false || value === 0 || value === "0" ||value === 1 || value === "1"){ 146 | return true; 147 | }else{ 148 | this.validator.addError(field, 'rule', 'boolean', 'The value of the field needs to be between true, false, 0 and 1'); 149 | return false; 150 | } 151 | } 152 | 153 | * contains(field, value, inString, message){ 154 | if(typeof inString !== "string"){ 155 | this.validator.addError(field, 'rule', 'contains', 'The number of arguements provided is invalid. Please provide one single string'); 156 | return false; 157 | }else{ 158 | if(!v.contains(value, inString)){ 159 | if(message){ 160 | message.replace(':substring', inString); 161 | } 162 | this.validator.addError(field, 'rule', 'contains', message || 'The value of the field can only contain letters and numbers'); 163 | return false; 164 | } 165 | } 166 | 167 | return true; 168 | } 169 | 170 | * date(field, value, message){ 171 | if(!moment(value, date_formats, true).isValid()){ 172 | this.validator.addError(field, 'rule', 'date', message || 'The value provided for the field is an invalid date'); 173 | return false; 174 | } 175 | 176 | return true; 177 | } 178 | 179 | * dateFormat(field, value, format, message){ 180 | if(!moment(value, format, true).isValid()){ 181 | if(message){ 182 | message.replace(':format', format); 183 | } 184 | this.validator.addError(field, 'rule', 'dateFormat', message || 'The value provided for the field is either invalid or not in the format mentioned'); 185 | return false; 186 | } 187 | 188 | this.validator.validations[field].date_format = format; 189 | return true; 190 | } 191 | 192 | * different(field, value, otherField, message){ 193 | if(typeof otherField !== "string"){ 194 | this.validator.addError(field, 'rule', 'different', 'The number of arguements provided is invalid. Please provide one single string'); 195 | return false; 196 | }else{ 197 | otherField = otherField.split('.').filter(function(e){ return e !== ''; }); 198 | let otherValue; 199 | let self = this; 200 | 201 | otherField.map(function(item){ 202 | if(typeof otherValue === 'undefined'){ 203 | otherValue = self.validator.fields && self.validator.fields[item]; 204 | }else{ 205 | otherValue = otherValue[item]; 206 | } 207 | }); 208 | 209 | if(typeof otherValue === 'undefined'){ 210 | this.validator.addError(field, 'rule', 'different', message || 'The field you are comparing the value against does not exist'); 211 | return false; 212 | }else if(otherValue == value){ 213 | this.validator.addError(field, 'rule', 'different', message || 'The field you are comparing the value against is the same'); 214 | return false; 215 | } 216 | } 217 | 218 | return true; 219 | } 220 | 221 | * digits(field, value, dNumber, message){ 222 | 223 | if(message){ 224 | message = message.replace(':digits', dNumber.toString()); 225 | } 226 | 227 | if(!v.isInt(dNumber)){ 228 | this.validator.addError(field, 'rule', 'digits', 'The arguement entered is an invalid. Please enter digits'); 229 | return false; 230 | }else if(value != dNumber){ 231 | this.validator.addError(field, 'rule', 'digits', message || 'The value does not match with the mentioned number'); 232 | return false; 233 | } 234 | 235 | return true; 236 | } 237 | 238 | /** 239 | * [digitsBetween - Check if the value provided is in digits that fall between the args] 240 | * @param {string} field The name of the field getting validated 241 | * @param {integer} value The value of the field getting validated 242 | * @param {array} args Contains the minimum and maximum number with the array 243 | * @param {string} message An optional custom error message to be displayed in case of the error 244 | * @return {boolean} True if field validates false if it doesn't 245 | */ 246 | 247 | * digitsBetween(field, value, args, message){ 248 | if(!Array.isArray(args) && args.length !== 2){ 249 | this.validator.addError(field, 'rule', 'digitsBetween', 'The number of arguements in the field are invalid'); 250 | return false; 251 | }else{ 252 | if(!v.isInt(args[0]) || !v.isInt(args[1])){ 253 | this.validator.addError(field, 'rule', 'digitsBetween', 'The rule arguements for the field need to be integers'); 254 | return false; 255 | }else if(parseInt(args[0]) >= parseInt(args[1])){ 256 | this.validator.addError(field, 'rule', 'digitsBetween', 'The rule arguement for the min value cannot be greater than or equal to the max value'); 257 | return false; 258 | }else if(parseInt(value) < parseInt(args[0]) || parseInt(value) > parseInt(args[1])){ 259 | if(message){ 260 | message = message.replace(':min', args[0]).replace(':max', args[1]); 261 | } 262 | 263 | this.validator.addError(field, 'rule', 'digitsBetween', message || 'The digits are not within the specified range'); 264 | return false; 265 | } 266 | } 267 | 268 | return true; 269 | } 270 | 271 | * email(field, value, message){ 272 | if(!v.isEmail(value)){ 273 | this.validator.addError(field, 'rule', 'email', message || 'The value entered is not a valid email'); 274 | return false; 275 | } 276 | 277 | return true; 278 | } 279 | 280 | * equals(field, value, arg, message){ 281 | if(value != arg){ 282 | this.validator.addError(field, 'rule', 'equals', message || 'The value entered does not match with the arguement'); 283 | return false; 284 | } 285 | 286 | return true; 287 | } 288 | 289 | * in(field, value, args, message){ 290 | if(!Array.isArray(args)) args = [args]; 291 | 292 | let match = false; 293 | 294 | for(let i = 0; i < args.length; i++){ 295 | if(value == args[i]){ 296 | match = true; 297 | } 298 | } 299 | 300 | if(!match){ 301 | this.validator.addError(field, 'rule', 'in', message || 'The value entered does not exist in the arguements supplied'); 302 | return false; 303 | } 304 | 305 | return true; 306 | } 307 | 308 | * integer(field, value, message){ 309 | if(!v.isInt(value)){ 310 | this.validator.addError(field, 'rule', 'integer', message || 'The value entered is not an integer'); 311 | return false; 312 | } 313 | 314 | return true; 315 | } 316 | 317 | * ip(field, value, message){ 318 | if(!v.isIP(value)){ 319 | this.validator.addError(field, 'rule', 'ip', message || 'The value entered is not an IP Address'); 320 | return false; 321 | } 322 | 323 | return true; 324 | } 325 | 326 | * json(field, value, message){ 327 | if(!v.isJSON(value)){ 328 | this.validator.addError(field, 'rule', 'json', message || 'The value entered is not a JSON string'); 329 | return false; 330 | } 331 | 332 | return true; 333 | } 334 | 335 | * max(field, value, maxNum, message){ 336 | if(!v.isInt(maxNum)){ 337 | this.validator.addError(field, 'rule', 'max', message || 'The rule arguements for max fields needs to be an integer'); 338 | return false; 339 | }else if(parseInt(value) > parseInt(maxNum)){ 340 | if(message){ 341 | message.replace(':max', maxNum) 342 | } 343 | this.validator.addError(field, 'rule', 'max', message || 'The value of the field is greater than the max arguement'); 344 | return false; 345 | } 346 | 347 | return true; 348 | } 349 | 350 | * maxLength(field, value, maxNum, message){ 351 | if(!v.isInt(maxNum)){ 352 | this.validator.addError(field, 'rule', 'max', message || 'The rule arguements for max fields needs to be an integer'); 353 | return false; 354 | }else if(value.toString().length > parseInt(maxNum)){ 355 | if(message){ 356 | message.replace(':maxLength', maxNum) 357 | } 358 | this.validator.addError(field, 'rule', 'maxLength', message || 'The size of the field is greater than the max arguement'); 359 | return false; 360 | } 361 | 362 | return true; 363 | } 364 | 365 | * min(field, value, minNum, message){ 366 | if(!v.isInt(minNum)){ 367 | this.validator.addError(field, 'rule', 'min', message || 'The rule arguements for min fields needs to be an integer'); 368 | return false; 369 | }else if(parseInt(value) < parseInt(minNum)){ 370 | if(message){ 371 | message.replace(':min', minNum) 372 | } 373 | 374 | this.validator.addError(field, 'rule', 'min', message || 'The value of the field is lesser than the min arguement'); 375 | return false; 376 | } 377 | 378 | return true; 379 | } 380 | 381 | * minLength(field, value, minNum, message){ 382 | if(!v.isInt(minNum)){ 383 | this.validator.addError(field, 'rule', 'min', 'The rule arguements for min fields needs to be an integer'); 384 | return false; 385 | }else if(value.toString().length < parseInt(minNum)){ 386 | if(message){ 387 | message.replace(':minLength', minNum) 388 | } 389 | 390 | this.validator.addError(field, 'rule', 'minLength', message || 'The size of the field is lesser than the min arguement'); 391 | return false; 392 | } 393 | 394 | return true; 395 | } 396 | 397 | * notContains(field, value, inString, message){ 398 | if(typeof inString !== "string"){ 399 | this.validator.addError(field, 'rule', 'notContains', 'The number of arguements provided is invalid. Please provide one single string'); 400 | return false; 401 | }else{ 402 | if(v.contains(value, inString)){ 403 | if(message){ 404 | message.replace(':substring', inString); 405 | } 406 | 407 | this.validator.addError(field, 'rule', 'notContains', message || 'The value of the field can only contain letters and numbers'); 408 | return false; 409 | } 410 | } 411 | 412 | return true; 413 | } 414 | 415 | * notIn(field, value, args, message){ 416 | if(!Array.isArray(args)) args = [args]; 417 | 418 | let noMatch = true; 419 | 420 | for(let i = 0; i < args.length; i++){ 421 | if(value == args[i]){ 422 | noMatch = false; 423 | } 424 | } 425 | 426 | if(!noMatch){ 427 | this.validator.addError(field, 'rule', 'notIn', message || 'The value entered exists in the arguements supplied'); 428 | return false; 429 | } 430 | 431 | return true; 432 | } 433 | 434 | * numeric(field, value, message){ 435 | if(!v.isNumeric(value.toString())){ 436 | this.validator.addError(field, 'rule', 'numeric', message || 'The value entered is not numeric'); 437 | return false; 438 | } 439 | 440 | return true; 441 | } 442 | 443 | * regex(field, value, regexp, message){ 444 | if(!(regexp instanceof RegExp)){ 445 | this.validator.addError(field, 'rule', 'regex', message || 'The regex arguement is not a valid regular expression'); 446 | return false; 447 | }else if(!regexp.test(value)){ 448 | if(message){ 449 | message = message.replace(':regexp', regexp); 450 | } 451 | 452 | this.validator.addError(field, 'rule', 'regex', message || 'The value provided did not match with the regex format'); 453 | return false; 454 | } 455 | 456 | return true; 457 | } 458 | 459 | * same(field, value, otherField, message){ 460 | if(typeof otherField !== 'string'){ 461 | this.validator.addError(field, 'rule', 'same', message || 'The number of arguements provided is invalid. Please provide one single string'); 462 | return false; 463 | }else{ 464 | otherField = otherField.split('.').filter(function(e){ return e !== ''; }); 465 | let otherValue; 466 | let self = this; 467 | 468 | otherField.map(function(item){ 469 | if(typeof otherValue === 'undefined'){ 470 | otherValue = self.validator.fields && self.validator.fields[item]; 471 | }else{ 472 | otherValue = otherValue[item]; 473 | } 474 | }); 475 | 476 | if(typeof otherValue === 'undefined'){ 477 | this.validator.addError(field, 'rule', 'same', message || 'The field you are comparing the value against does not exist'); 478 | return false; 479 | }else if(otherValue != value){ 480 | this.validator.addError(field, 'rule', 'same', message || 'The field you are comparing the value against are different'); 481 | return false; 482 | } 483 | } 484 | 485 | return true; 486 | } 487 | 488 | * string(field, value, message){ 489 | if(typeof value !== 'string'){ 490 | this.validator.addError(field, 'rule', 'string', message || 'The value provided is not a string'); 491 | return false; 492 | } 493 | 494 | return true; 495 | } 496 | 497 | * timezone(field, value, message){ 498 | if(!moment.tz.zone(value)){ 499 | this.validator.addError(field, 'rule', 'timezone', message || 'The value provided is not a valid timezone'); 500 | return false; 501 | } 502 | 503 | return true; 504 | } 505 | 506 | * url(field, value, message){ 507 | if(!v.isURL(value)){ 508 | this.validator.addError(field, 'rule', 'url', message || 'The value provided is not a URL'); 509 | return false; 510 | } 511 | 512 | return true; 513 | } 514 | } 515 | 516 | module.exports = Rules; 517 | -------------------------------------------------------------------------------- /lib/validate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let FieldValidator = require('./fieldValidator'); 4 | let FileValidator = require('./fileValidator'); 5 | let RequiredRules = require('./requiredRules'); 6 | let Rules = require('./rules'); 7 | let Filters = require('./filters'); 8 | let FileRules = require('./fileRules'); 9 | let FileActions = require('./fileActions'); 10 | 11 | module.exports = function() { 12 | return function * (next) { 13 | let v; 14 | this.validateBody = function *(rules, messages, filters){ 15 | let fields; 16 | 17 | if(this.request.body && this.request.body.fields) fields = this.request.body.fields; 18 | else if (this.request.body) fields = this.request.body; 19 | else fields = {}; 20 | 21 | v = new FieldValidator( 22 | this, 23 | fields, 24 | rules, 25 | messages || {}, 26 | filters || {} 27 | ); 28 | 29 | yield v.valid; 30 | }; 31 | 32 | this.validateParams = function *(rules, messages, filters){ 33 | v = new FieldValidator( 34 | this, 35 | this.params || {}, 36 | rules, 37 | messages || {}, 38 | filters || {} 39 | ); 40 | 41 | yield v.valid; 42 | }; 43 | 44 | this.validateQueries = function *(rules, messages, filters){ 45 | v = new FieldValidator( 46 | this, 47 | this.request.query || {}, 48 | rules, 49 | messages || {}, 50 | filters || {} 51 | ); 52 | 53 | yield v.valid; 54 | }; 55 | 56 | this.validateHeaders = function *(rules, messages, filters){ 57 | v = new FieldValidator( 58 | this, 59 | this.headers || {}, 60 | rules, 61 | messages || {}, 62 | filters || {} 63 | ); 64 | 65 | yield v.valid; 66 | }; 67 | 68 | this.validateFiles = function *(rules, deleteOnFail, messages, actions) { 69 | var files = (this.request.body && this.request.body.files) ? this.request.body.files : {}; 70 | 71 | v = new FileValidator( 72 | this, 73 | files, 74 | rules, 75 | deleteOnFail || false, 76 | messages || {}, 77 | actions || {} 78 | ); 79 | 80 | yield v.valid; 81 | }; 82 | 83 | yield next; 84 | }; 85 | }; 86 | 87 | module.exports.RequiredRules = RequiredRules; 88 | module.exports.Rules = Rules; 89 | module.exports.FileRules = FileRules; 90 | module.exports.Filters = Filters; 91 | module.exports.FileActions = FileActions; 92 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koa-validation", 3 | "version": "0.1.9", 4 | "description": "The Koa Validation is a single point validation library that enables you to validate params, body, queries, files and headers", 5 | "main": "lib/validate.js", 6 | "scripts": { 7 | "test": "NODE_ENV=test mocha --require should --reporter spec", 8 | "test-cov": "NODE_ENV=test node ./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha -- --require should", 9 | "test-travis": "NODE_ENV=test node ./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha --report lcovonly -- --require should" 10 | }, 11 | "keywords": [ 12 | "koa", 13 | "middleware", 14 | "validation", 15 | "filters", 16 | "validator", 17 | "koa-validation", 18 | "koa validation", 19 | "file validation" 20 | ], 21 | "repository": "srinivasiyer/koa-validation", 22 | "author": { 23 | "name": "Srinivas Iyer", 24 | "email": "srinivasiyer87@gmail.com" 25 | }, 26 | "license": "ISC", 27 | "dependencies": { 28 | "co-fs-extra": "^1.1.0", 29 | "mime-types": "^2.1.7", 30 | "moment-timezone": "^0.4.1", 31 | "validator": "^4.0.5" 32 | }, 33 | "devDependencies": { 34 | "co": "^4.6.0", 35 | "istanbul-harmony": "^0.3.16", 36 | "koa": "^1.0.0", 37 | "koa-better-body": "^1.0.17", 38 | "koa-qs": "^2.0.0", 39 | "koa-router": "^5.2.1", 40 | "mocha": "^2.3.3", 41 | "should": "^7.1.1", 42 | "supertest": "^1.1.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/app/app.js: -------------------------------------------------------------------------------- 1 | var app = require('koa')(); 2 | var router = (new require('koa-router'))(); 3 | var koaBody = require('koa-better-body'); 4 | 5 | require('koa-qs')(app, 'extended'); 6 | 7 | var validate = require('../../lib/validate'); 8 | 9 | app.use(koaBody({ 10 | 'multipart': true 11 | })); 12 | app.use(validate()); 13 | 14 | require('./query_routes')(app, router); 15 | require('./header_routes')(app, router); 16 | require('./param_routes')(app, router); 17 | require('./post_routes')(app, router); 18 | require('./file_routes')(app, router); 19 | 20 | module.exports = app; 21 | -------------------------------------------------------------------------------- /test/app/file_routes.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app, router){ 2 | 3 | router.post('/files', function *(){ 4 | yield this.validateFiles({ 5 | 'jsFile':'required|size:min,10kb,max,20kb', 6 | 'imgFile': 'required|image', 7 | 'imgFile1': 'mime:jpg', 8 | 'imgFile2': 'extension:jpg', 9 | 'pkgFile': 'name:package' 10 | }); 11 | 12 | if(this.validationErrors){ 13 | this.status = 422; 14 | this.body = this.validationErrors; 15 | }else{ 16 | this.status = 200; 17 | this.body = { success: true } 18 | } 19 | }); 20 | 21 | router.post('/deleteOnFail', function *(){ 22 | yield this.validateFiles({ 23 | 'jsFile':'required|size:min,10kb,max,20kb', 24 | 'imgFile': 'required|image' 25 | }, true); 26 | 27 | if(this.validationErrors){ 28 | this.status = 422; 29 | var tmpfiles = [] 30 | for (var f in this.request.body.files){ 31 | tmpfiles.push(this.request.body.files[f].path); 32 | } 33 | 34 | this.body = tmpfiles; 35 | }else{ 36 | this.status = 200; 37 | this.body = { success: true } 38 | } 39 | }); 40 | 41 | router.post('/fileActions', function *(){ 42 | yield this.validateFiles({ 43 | 'jsFile':'required|size:min,10kb,max,20kb', 44 | 'imgFile': 'required|image', 45 | },true, {}, { 46 | jsFile: { 47 | action: 'move', 48 | args: __dirname + '/../files/tmp/rules.js', 49 | callback: function *(validator, file, destination){ 50 | validator.addError(jsFile, 'action', 'move', 'Just checking if the callback action works!!') 51 | } 52 | }, 53 | imgFile: [ 54 | { 55 | action: 'copy', 56 | args: __dirname + '/../files/tmp/panda.jpg' 57 | }, 58 | { 59 | action: 'delete' 60 | } 61 | ] 62 | }); 63 | 64 | if(this.validationErrors){ 65 | this.status = 422; 66 | var tmpfiles = {} 67 | for (var f in this.request.body.files){ 68 | tmpfiles[f] = this.request.body.files[f].path; 69 | } 70 | this.body = { 71 | tmpFiles: tmpfiles, 72 | errors: this.validationErrors 73 | }; 74 | }else{ 75 | this.status = 200; 76 | this.body = { success: true } 77 | } 78 | }); 79 | 80 | app.use(router.routes()).use(router.allowedMethods()); 81 | } 82 | -------------------------------------------------------------------------------- /test/app/header_routes.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app, router){ 2 | 3 | router.get('/headers', function *(){ 4 | yield this.validateHeaders( 5 | { 6 | 'content-type': 'required|equals:application/json', 7 | 'x-authorization': 'required|between:20,30', 8 | 'x-origin-ip': 'required|ip', 9 | }, 10 | {}, 11 | { 12 | before:{ 13 | 'content-type': 'trim|lowercase' 14 | } 15 | } 16 | ); 17 | 18 | if(this.validationErrors){ 19 | this.status = 422; 20 | this.body = this.validationErrors; 21 | }else{ 22 | this.status = 200; 23 | this.body = { success: true } 24 | } 25 | }); 26 | 27 | app.use(router.routes()).use(router.allowedMethods()); 28 | } 29 | -------------------------------------------------------------------------------- /test/app/param_routes.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app, router){ 2 | 3 | router.post('/params/:username/post/:postId', function *(){ 4 | yield this.validateParams({ 5 | 'username': 'alphaDash|between:6,15', 6 | 'postId': 'numeric|digitsBetween:10000,99999' 7 | }); 8 | 9 | if(this.validationErrors){ 10 | this.status = 422; 11 | this.body = this.validationErrors; 12 | }else{ 13 | this.status = 200; 14 | this.body = { success: true }; 15 | } 16 | }); 17 | 18 | app.use(router.routes()).use(router.allowedMethods()); 19 | } 20 | -------------------------------------------------------------------------------- /test/app/post_routes.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app, router){ 2 | 3 | router.put('/', function *(){ 4 | yield this.validateBody( 5 | { 6 | name: 'required|minLength:4', 7 | girlfiend: 'requiredIf:age,25', 8 | wife: 'requiredNotIf:age,22', 9 | foo: 'requiredWith:bar,baz', 10 | foobar: 'requiredWithAll:barbaz,bazbaz', 11 | gandalf: 'requiredWithout:Saruman', 12 | tyrion: 'requiredWithoutAll:tywin,cercei', 13 | age: 'numeric', 14 | teenage: 'digitsBetween:13,19', 15 | date: 'dateFormat:MMDDYYYY', 16 | birthdate: 'date', 17 | past: 'before:2015-10-06', 18 | future: 'after:2015-10-07', 19 | gender: 'in:male, female', 20 | genres: 'notIn:Pop,Metal', 21 | grade: 'accepted', 22 | nickname: 'alpha', 23 | nospaces: 'alphaDash', 24 | email: 'email', 25 | alphanum: 'alphaNumeric', 26 | password: 'between:6,15', 27 | iaccept: 'boolean', 28 | partofit: 'contains:cam', 29 | notpartofit: 'notContains:ward', 30 | cpassword: 'same:password', 31 | spousegender: 'different:gender', 32 | luckynum: 'digits:8974', 33 | thesaurus: 'equals:dictionary', 34 | number: 'integer', 35 | ipaddress: 'ip', 36 | object: 'json', 37 | chocolates: 'max:90', 38 | watts: 'min:25', 39 | longword: 'minLength:25', 40 | shortword: 'maxLength:10', 41 | tendigits: { regex: [/^\d{10}$/g] }, 42 | watch: 'timezone', 43 | website: 'url', 44 | }, 45 | { 46 | 'name.required': 'The name field is a required one' 47 | } 48 | ) 49 | 50 | if(this.validationErrors){ 51 | this.status = 422; 52 | this.body = this.validationErrors; 53 | }else{ 54 | this.status = 200; 55 | this.body = { success: true } 56 | } 57 | }); 58 | 59 | router.put('/filters/before', function *(){ 60 | yield this.validateBody({},{},{ 61 | before: { 62 | name: 'lowercase', 63 | nickname: 'uppercase', 64 | snum: 'integer', 65 | sword: 'trim', 66 | lword: 'ltrim', 67 | rword: 'rtrim', 68 | dnum: 'float', 69 | bword: 'boolean', 70 | obj: 'json', 71 | eword: 'escape', 72 | reword: 'replace:come,came', 73 | shaword: 'sha1', 74 | mdword: 'md5', 75 | hexword: 'hex:sha256' 76 | } 77 | }) 78 | 79 | this.body = this.request.body.fields || this.request.body || {}; 80 | }); 81 | 82 | router.put('/filters/after', function *(){ 83 | yield this.validateBody({},{},{ 84 | after: { 85 | name: 'lowercase', 86 | nickname: 'uppercase', 87 | snum: 'integer', 88 | sword: 'trim', 89 | lword: 'ltrim', 90 | rword: 'rtrim', 91 | dnum: 'float', 92 | bword: 'boolean', 93 | obj: 'json', 94 | eword: 'escape', 95 | reword: 'replace:come,came', 96 | shaword: 'sha1', 97 | mdword: 'md5', 98 | hexword: 'hex:sha256' 99 | } 100 | }); 101 | 102 | this.body = this.request.body.fields || this.request.body || {}; 103 | }); 104 | 105 | app.use(router.routes()).use(router.allowedMethods()); 106 | }; 107 | -------------------------------------------------------------------------------- /test/app/query_routes.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app, router){ 2 | 3 | router.get('/', function *(){ 4 | yield this.validateQueries( 5 | { 6 | name: 'required|minLength:4', 7 | girlfiend: 'requiredIf:age,25', 8 | wife: 'requiredNotIf:age,22', 9 | foo: 'requiredWith:bar,baz', 10 | foobar: 'requiredWithAll:barbaz,bazbaz', 11 | gandalf: 'requiredWithout:Saruman', 12 | tyrion: 'requiredWithoutAll:tywin,cercei', 13 | age: 'numeric', 14 | teenage: 'digitsBetween:13,19', 15 | date: 'dateFormat:MMDDYYYY', 16 | birthdate: 'date', 17 | past: 'before:2015-10-06', 18 | future: 'after:2015-10-07', 19 | gender: 'in:male, female', 20 | genres: 'notIn:Pop,Metal', 21 | grade: 'accepted', 22 | nickname: 'alpha', 23 | nospaces: 'alphaDash', 24 | email: 'email', 25 | alphanum: 'alphaNumeric', 26 | password: 'between:6,15', 27 | iaccept: 'boolean', 28 | partofit: 'contains:cam', 29 | notpartofit: 'notContains:ward', 30 | cpassword: 'same:password', 31 | spousegender: 'different:gender', 32 | luckynum: 'digits:8974', 33 | thesaurus: 'equals:dictionary', 34 | number: 'integer', 35 | ipaddress: 'ip', 36 | object: 'json', 37 | chocolates: 'max:90', 38 | watts: 'min:25', 39 | longword: 'minLength:25', 40 | shortword: 'maxLength:10', 41 | tendigits: { regex: [/^\d{10}$/g] }, 42 | watch: 'timezone', 43 | website: 'url' 44 | }, 45 | { 46 | 'name.required': 'The name field is a required one' 47 | } 48 | ); 49 | 50 | if(this.validationErrors){ 51 | this.status = 422; 52 | this.body = this.validationErrors; 53 | }else{ 54 | this.status = 200; 55 | this.body = { success: true } 56 | } 57 | }); 58 | 59 | router.get('/filters/before', function *(){ 60 | yield this.validateQueries({},{},{ 61 | before: { 62 | name: 'lowercase', 63 | nickname: 'uppercase', 64 | snum: 'integer', 65 | sword: 'trim', 66 | lword: 'ltrim', 67 | rword: 'rtrim', 68 | dnum: 'float', 69 | bword: 'boolean', 70 | obj: 'json', 71 | eword: 'escape', 72 | reword: 'replace:come,came', 73 | shaword: 'sha1', 74 | mdword: 'md5', 75 | hexword: 'hex:sha256' 76 | } 77 | }); 78 | 79 | this.body = this.query; 80 | }); 81 | 82 | router.get('/filters/after', function *(){ 83 | yield this.validateQueries({},{},{ 84 | after: { 85 | name: 'lowercase', 86 | nickname: 'uppercase', 87 | snum: 'integer', 88 | sword: 'trim', 89 | lword: 'ltrim', 90 | rword: 'rtrim', 91 | dnum: 'float', 92 | bword: 'boolean', 93 | obj: 'json', 94 | eword: 'escape', 95 | reword: 'replace:come,came', 96 | shaword: 'sha1', 97 | mdword: 'md5', 98 | hexword: 'hex:sha256' 99 | } 100 | }); 101 | 102 | this.body = this.query; 103 | }); 104 | 105 | app.use(router.routes()).use(router.allowedMethods()); 106 | }; 107 | -------------------------------------------------------------------------------- /test/files/redpanda.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srinivasiyer/koa-validation/7e76a714eafa9434a9934dd3240221da9804b772/test/files/redpanda.jpg -------------------------------------------------------------------------------- /test/test_body.js: -------------------------------------------------------------------------------- 1 | var request = require('supertest') 2 | , app = require('./app/app'); 3 | 4 | describe('Koa request body validation', function(){ 5 | it('Should throw the required rule errors when conditions dont match', function(done){ 6 | request(app.listen()).put('/') 7 | .send({ bar: 'barbaz' }) 8 | .send({ baz: 'foobar' }) 9 | .send({ barbaz: 'foobar' }) 10 | .send({ bazbaz: 'barbaz' }) 11 | .send({ age: 25}) 12 | .end(function(err, res){ 13 | res.statusCode.should.equal(422); 14 | res.body.should.be.an.Array; 15 | errorFields = {}; 16 | res.body.forEach(function(objs){ 17 | for(var o in objs){ 18 | errorFields[o] = objs[o].rule; 19 | } 20 | }); 21 | 22 | errorFields.should.have.properties({ 23 | foo: 'requiredWith', 24 | girlfiend: 'requiredIf', 25 | wife: 'requiredNotIf', 26 | foobar: 'requiredWithAll', 27 | gandalf: 'requiredWithout', 28 | name: 'required', 29 | tyrion: 'requiredWithoutAll' 30 | }); 31 | 32 | done(); 33 | }); 34 | }) 35 | 36 | it('Should throw errors on all fields', function(done){ 37 | request(app.listen()).put('/') 38 | .send({ bar: 'barbaz' }) 39 | .send({ baz: 'foobar' }) 40 | .send({ teenage: 20 }) 41 | .send({ age: 'twenty' }) 42 | .send({ bazbaz: 'barbaz' }) 43 | .send({ date: '2015-14-23' }) 44 | .send({ past: '2015-11-01' }) 45 | .send({ future: '2015-01-01' }) 46 | .send({ birthdate: 'barbaz' }) 47 | .send({ gender: 'other' }) 48 | .send({ genres: 'Pop' }) 49 | .send({ grade: 'no' }) 50 | .send({ nickname: 1234 }) 51 | .send({ nospaces: '2345&^$' }) 52 | .send({ email: 'lucky@name' }) 53 | .send({ alphanum: '&*^%' }) 54 | .send({ password: 'abnsd' }) 55 | .send({ iaccept: 'yes' }) 56 | .send({ partofit: 'become' }) 57 | .send({ notpartofit: 'forward' }) 58 | .send({ cpassword: 'word' }) 59 | .send({ spousegender: 'other' }) 60 | .send({ luckynum: 1234 }) 61 | .send({ thesaurus: 'synonyms' }) 62 | .send({ number: 'abdc' }) 63 | .send({ ipaddress: '200.200.200' }) 64 | .send({ object: 'notaJSON' }) 65 | .send({ chocolates: 95 }) 66 | .send({ watts: 20 }) 67 | .send({ longword: 'jsdsakjdsad' }) 68 | .send({ shortword: 'thisismorethan10chars' }) 69 | .send({ tendigits: 123456789 }) 70 | .send({ watch: 'asia' }) 71 | .send({ website: 'world.' }) 72 | .end(function(err, res){ 73 | res.statusCode.should.equal(422); 74 | res.body.should.be.an.Array; 75 | errorFields = {}; 76 | res.body.forEach(function(objs){ 77 | for(var o in objs){ 78 | errorFields[o] = objs[o].rule; 79 | } 80 | }); 81 | 82 | errorFields.should.have.properties({ 83 | age: 'numeric', 84 | teenage: 'digitsBetween', 85 | date: 'dateFormat', 86 | birthdate: 'date', 87 | past: 'before', 88 | future: 'after', 89 | gender: 'in', 90 | genres: 'notIn', 91 | grade: 'accepted', 92 | nickname: 'alpha', 93 | nospaces: 'alphaDash', 94 | email: 'email', 95 | alphanum: 'alphaNumeric', 96 | password: 'between', 97 | iaccept: 'boolean', 98 | partofit: 'contains', 99 | notpartofit: 'notContains', 100 | cpassword: 'same', 101 | spousegender: 'different', 102 | luckynum: 'digits', 103 | ipaddress: 'ip', 104 | thesaurus: 'equals', 105 | number: 'integer', 106 | object: 'json', 107 | chocolates: 'max', 108 | watts: 'min', 109 | shortword: 'maxLength', 110 | longword: 'minLength', 111 | tendigits: 'regex', 112 | watch: 'timezone', 113 | website: 'url' 114 | }); 115 | 116 | done(); 117 | }); 118 | }) 119 | 120 | it('Should throw no errors when proper values are sent', function(done){ 121 | request(app.listen()).put('/') 122 | .send({ name: 'Srinivas Iyer' }) 123 | .send({ bar: 'barbaz' }) 124 | .send({ baz: 'foobar' }) 125 | .send({ foo: 'bar' }) 126 | .send({ tyrion: 'lanister' }) 127 | .send({ gandalf: 'grey' }) 128 | .send({ teenage: 17 }) 129 | .send({ age: 22 }) 130 | .send({ bazbaz: 'barbaz' }) 131 | .send({ date: '12232015' }) 132 | .send({ past: '2015-06-01' }) 133 | .send({ future: '2015-11-01' }) 134 | .send({ birthdate: '2016-12-25' }) 135 | .send({ gender: 'male' }) 136 | .send({ genres: 'jazz' }) 137 | .send({ grade: 'yes' }) 138 | .send({ nickname: 'srini' }) 139 | .send({ nospaces: 'this_is-what' }) 140 | .send({ email: 'lucky@strike.com' }) 141 | .send({ alphanum: 'abcd928921' }) 142 | .send({ password: 'abcd1234' }) 143 | .send({ iaccept: 1 }) 144 | .send({ partofit: 'became' }) 145 | .send({ notpartofit: 'forewarn' }) 146 | .send({ cpassword: 'abcd1234' }) 147 | .send({ spousegender: 'female' }) 148 | .send({ luckynum: 8974 }) 149 | .send({ thesaurus: 'dictionary' }) 150 | .send({ number: 1234 }) 151 | .send({ ipaddress: '192.168.0.1' }) 152 | .send({ object: '{ "foo": "bar" }' }) 153 | .send({ chocolates: 80 }) 154 | .send({ watts: 30 }) 155 | .send({ longword: 'This is a super long string which stretches beyond 25 charachters' }) 156 | .send({ shortword: 'lessthan10' }) 157 | .send({ tendigits: 1234567890 }) 158 | .send({ watch: 'asia/kolkata' }) 159 | .send({ website: 'srinivasiyer.com' }) 160 | .end(function(err, res){ 161 | res.statusCode.should.equal(200); 162 | res.body.should.be.an.Object; 163 | done(); 164 | }); 165 | }); 166 | 167 | 168 | it('Should throw errors when required null values are sent', function(done){ 169 | request(app.listen()).put('/') 170 | .send({ name: null }) 171 | .send({ bar: 'barbaz' }) 172 | .send({ baz: 'foobar' }) 173 | .send({ foo: null }) 174 | .send({ gandalf: null }) 175 | .send({ teenage: 17 }) 176 | .send({ age: 25 }) 177 | .send({ barbaz: 'barbaz' }) 178 | .send({ bazbaz: 'bazbaz' }) 179 | .send({ date: '12232015' }) 180 | .send({ past: '2015-06-01' }) 181 | .send({ future: '2015-11-01' }) 182 | .send({ birthdate: '2016-12-25' }) 183 | .send({ gender: 'male' }) 184 | .send({ genres: 'jazz' }) 185 | .send({ grade: 'yes' }) 186 | .send({ nickname: 'srini' }) 187 | .send({ nospaces: 'this_is-what' }) 188 | .send({ email: 'lucky@strike.com' }) 189 | .send({ alphanum: 'abcd928921' }) 190 | .send({ password: 'abcd1234' }) 191 | .send({ iaccept: 1 }) 192 | .send({ partofit: 'became' }) 193 | .send({ notpartofit: 'forewarn' }) 194 | .send({ cpassword: 'abcd1234' }) 195 | .send({ spousegender: 'female' }) 196 | .send({ luckynum: 8974 }) 197 | .send({ thesaurus: 'dictionary' }) 198 | .send({ number: 1234 }) 199 | .send({ ipaddress: '192.168.0.1' }) 200 | .send({ object: '{ "foo": "bar" }' }) 201 | .send({ chocolates: 80 }) 202 | .send({ watts: 30 }) 203 | .send({ longword: 'This is a super long string which stretches beyond 25 charachters' }) 204 | .send({ shortword: 'lessthan10' }) 205 | .send({ tendigits: 1234567890 }) 206 | .send({ watch: 'asia/kolkata' }) 207 | .send({ website: 'srinivasiyer.com' }) 208 | .end(function(err, res){ 209 | res.statusCode.should.equal(422); 210 | res.body.should.be.an.Array; 211 | errorFields = {}; 212 | res.body.forEach(function(objs){ 213 | for(var o in objs){ 214 | errorFields[o] = objs[o].rule; 215 | } 216 | }); 217 | 218 | errorFields.should.have.properties({ 219 | name: 'required', 220 | girlfiend: 'requiredIf', 221 | foo: 'requiredWith', 222 | wife: 'requiredNotIf', 223 | foobar: 'requiredWithAll', 224 | gandalf: 'requiredWithout', 225 | tyrion: 'requiredWithoutAll', 226 | }); 227 | 228 | done(); 229 | }); 230 | }); 231 | 232 | it('should return changed queries when before filters are applied', function(done){ 233 | 234 | request(app.listen()) 235 | .put('/filters/before') 236 | .send({name: 'LOWERCASE'}) 237 | .send({nickname: 'uppercase'}) 238 | .send({snum: '92349021'}) 239 | .send({sword: ' trim '}) 240 | .send({lword: ' ltrim'}) 241 | .send({rword: 'rtrim '}) 242 | .send({dnum: '1234.23'}) 243 | .send({bword: 'false'}) 244 | .send({obj: { 'foo': 'bar' }}) 245 | .send({eword: ''}) 246 | .send({reword: 'become'}) 247 | .send({shaword: 'password'}) 248 | .send({mdword: 'password'}) 249 | .send({hexword: 'password'}) 250 | .end(function(err, res){ 251 | res.body.should.be.an.object; 252 | res.statusCode.should.equal(200); 253 | res.body.should.have.properties({ 254 | name: 'lowercase', 255 | nickname: 'UPPERCASE', 256 | snum: 92349021, 257 | sword: 'trim', 258 | lword: 'ltrim', 259 | rword: 'rtrim', 260 | dnum: 1234.23, 261 | bword: false, 262 | obj: '{"foo":"bar"}', 263 | eword: '<html></html>', 264 | reword: 'became', 265 | shaword: '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8', 266 | mdword: '5f4dcc3b5aa765d61d8327deb882cf99', 267 | hexword: '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8' 268 | }); 269 | 270 | done(); 271 | }); 272 | }); 273 | 274 | it('should return changed queries when filters are applied', function(done){ 275 | request(app.listen()) 276 | .put('/filters/before') 277 | .send({name: 'LOWERCASE'}) 278 | .send({nickname: 'uppercase'}) 279 | .send({snum: '92349021'}) 280 | .send({sword: ' trim '}) 281 | .send({lword: ' ltrim'}) 282 | .send({rword: 'rtrim '}) 283 | .send({dnum: '1234.23'}) 284 | .send({bword: 'false'}) 285 | .send({obj: { 'foo': 'bar' }}) 286 | .send({eword: ''}) 287 | .send({reword: 'become'}) 288 | .send({shaword: 'password'}) 289 | .send({mdword: 'password'}) 290 | .send({hexword: 'password'}) 291 | .end(function(err, res){ 292 | res.body.should.be.an.object; 293 | res.statusCode.should.equal(200); 294 | res.body.should.have.properties({ 295 | name: 'lowercase', 296 | nickname: 'UPPERCASE', 297 | snum: 92349021, 298 | sword: 'trim', 299 | lword: 'ltrim', 300 | rword: 'rtrim', 301 | dnum: 1234.23, 302 | bword: false, 303 | obj: '{"foo":"bar"}', 304 | eword: '<html></html>', 305 | reword: 'became', 306 | shaword: '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8', 307 | mdword: '5f4dcc3b5aa765d61d8327deb882cf99', 308 | hexword: '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8' 309 | }); 310 | 311 | done(); 312 | }); 313 | }) 314 | 315 | }); 316 | -------------------------------------------------------------------------------- /test/test_files.js: -------------------------------------------------------------------------------- 1 | var request = require('supertest') 2 | , app = require('./app/app') 3 | , fs = require('co-fs-extra') 4 | , co = require('co'); 5 | 6 | describe('Koa request file validation', function(){ 7 | it('should throw the required rule errors when conditions don\'t match', function(done){ 8 | request(app.listen()).post('/files') 9 | .end(function(err, res){ 10 | res.statusCode.should.equal(422); 11 | res.body.should.be.an.Array; 12 | errorFields = {}; 13 | res.body.forEach(function(objs){ 14 | for(var o in objs){ 15 | errorFields[o] = objs[o].rule; 16 | } 17 | }); 18 | 19 | errorFields.should.have.properties({ 20 | 'jsFile': 'required', 21 | 'imgFile': 'required' 22 | }); 23 | 24 | done(); 25 | }); 26 | }); 27 | 28 | it('should throw proper errors when the file validation condition fails', function(done){ 29 | request(app.listen()).post('/files') 30 | .attach('jsFile', __dirname + '/../lib/validate.js') 31 | .attach('imgFile', __dirname + '/../lib/filters.js') 32 | .attach('imgFile1', __dirname + '/../lib/rules.js') 33 | .attach('imgFile2', __dirname + '/../lib/fileRules.js') 34 | .attach('pkgFile', __dirname + '/../README.md') 35 | .end(function(err, res){ 36 | res.statusCode.should.equal(422); 37 | res.body.should.be.an.Array; 38 | errorFields = {}; 39 | res.body.forEach(function(objs){ 40 | for(var o in objs){ 41 | errorFields[o] = objs[o].rule; 42 | } 43 | }); 44 | 45 | errorFields.should.have.properties({ 46 | imgFile1: 'mime', 47 | imgFile2: 'extension', 48 | pkgFile: 'name', 49 | jsFile: 'size', 50 | imgFile: 'image' 51 | }); 52 | 53 | done(); 54 | }); 55 | }) 56 | 57 | it('should pass when the validations return no errors', function(done){ 58 | request(app.listen()).post('/files') 59 | .attach('jsFile', __dirname + '/../lib/rules.js') 60 | .attach('imgFile', __dirname + '/files/redpanda.jpg') 61 | .attach('imgFile1', __dirname + '/files/redpanda.jpg') 62 | .attach('imgFile2', __dirname + '/files/redpanda.jpg') 63 | .attach('pkgFile', __dirname + '/../package.json') 64 | .end(function(err, res){ 65 | res.statusCode.should.equal(200); 66 | res.body.should.be.an.Object; 67 | done(); 68 | }); 69 | }); 70 | 71 | it('should delete the temp uploaded file when the validation fails and deleteOnFail set to true', function(done){ 72 | request(app.listen()).post('/deleteOnFail') 73 | .attach('imgFile', __dirname + '/../lib/rules.js') 74 | .attach('jsFile', __dirname + '/files/redpanda.jpg') 75 | .end(function (err, res){ 76 | res.statusCode.should.equal(422); 77 | res.body.should.be.an.Array; 78 | (co.wrap(function *(){ 79 | for(var i = 0; i < res.body.length; ++i){ 80 | (yield fs.exists(res.body[i])).should.equal(false); 81 | } 82 | })()).then(done, done); 83 | }); 84 | }); 85 | 86 | it('should apply the file action after the file has been validated', function(done){ 87 | request(app.listen()).post('/fileActions') 88 | .attach('jsFile', __dirname + '/../lib/rules.js') 89 | .attach('imgFile', __dirname + '/files/redpanda.jpg') 90 | .end(function(err, res){ 91 | //console.log(res.body); 92 | res.statusCode.should.equal(422); 93 | res.body.should.be.an.Object; 94 | res.body.should.have.property('tmpFiles'); 95 | res.body.should.have.property('errors'); 96 | res.body.tmpFiles.should.have.property('jsFile'); 97 | res.body.tmpFiles.should.have.property('imgFile'); 98 | 99 | (co.wrap(function *(){ 100 | //console.log(yield fs.exists( __dirname + '/app/tmp/rules.js')); 101 | (yield fs.exists( __dirname + '/files/tmp/rules.js')).should.equal(true); 102 | (yield fs.exists(__dirname + '/files/tmp/panda.jpg')).should.equal(true); 103 | 104 | yield fs.remove(__dirname + '/app/tmp'); 105 | })()).then(done, done) 106 | }); 107 | }); 108 | 109 | after(function() { 110 | (co.wrap(function *(){ 111 | yield fs.remove(__dirname + '/files/tmp'); 112 | })()) 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /test/test_headers.js: -------------------------------------------------------------------------------- 1 | var request = require('supertest') 2 | , app = require('./app/app'); 3 | 4 | describe('Koa header fields Validation', function(){ 5 | it('Should throw the required rule errors when conditions dont match', function(done){ 6 | request(app.listen()).get('/headers').end(function(err, res){ 7 | res.statusCode.should.equal(422); 8 | res.body.should.be.an.Array; 9 | errorFields = {}; 10 | res.body.forEach(function(objs){ 11 | for(var o in objs){ 12 | errorFields[o] = objs[o].rule; 13 | } 14 | }); 15 | 16 | errorFields.should.have.properties({ 17 | 'content-type': 'required', 18 | 'x-authorization': 'required', 19 | 'x-origin-ip': 'required', 20 | }); 21 | 22 | done(); 23 | }); 24 | }); 25 | 26 | it('should throw an error on all fields when values are incorrect', function(done){ 27 | request(app.listen()).get('/headers') 28 | .set('content-type', 'application/flash') 29 | .set('x-authorization', 'woodoo') 30 | .set('x-origin-ip', '2000.123.234.234') 31 | .end(function(err, res){ 32 | res.statusCode.should.equal(422); 33 | res.body.should.be.an.Array; 34 | errorFields = {}; 35 | res.body.forEach(function(objs){ 36 | for(var o in objs){ 37 | errorFields[o] = objs[o].rule; 38 | } 39 | }); 40 | 41 | errorFields.should.have.properties({ 42 | 'content-type': 'equals', 43 | 'x-authorization': 'between', 44 | 'x-origin-ip': 'ip', 45 | }); 46 | 47 | done(); 48 | }); 49 | }); 50 | 51 | it('should throw no errors when all values are correct', function(done){ 52 | request(app.listen()).get('/headers') 53 | .set('content-type', ' application/JSON ') 54 | .set('x-authorization', 'thisismorethantwentychars') 55 | .set('x-origin-ip', '200.123.234.234') 56 | .end(function(err, res){ 57 | res.statusCode.should.equal(200); 58 | res.body.should.be.an.Object; 59 | done(); 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /test/test_params.js: -------------------------------------------------------------------------------- 1 | var request = require('supertest') 2 | , app = require('./app/app'); 3 | 4 | describe('Koa URL params validation', function(){ 5 | it('should throw an error on all fields when values are incorrect', function(done){ 6 | request(app.listen()).post('/params/dhsud823893ej**$8/post/ajdii') 7 | .end(function(err, res){ 8 | res.statusCode.should.equal(422); 9 | res.body.should.be.an.Array; 10 | errorFields = {}; 11 | res.body.forEach(function(objs){ 12 | for(var o in objs){ 13 | errorFields[o] = objs[o].rule; 14 | } 15 | }); 16 | 17 | errorFields.should.have.properties({ 18 | 'username': 'alphaDash', 19 | 'postId': 'numeric', 20 | }); 21 | 22 | done(); 23 | }); 24 | }); 25 | 26 | it('should throw no errors when all values are correct', function(done){ 27 | request(app.listen()).post('/params/flash_is_here/post/88888') 28 | .end(function(err, res){ 29 | res.statusCode.should.equal(200); 30 | res.body.should.be.an.Object; 31 | done(); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/test_query.js: -------------------------------------------------------------------------------- 1 | var request = require('supertest') 2 | , app = require('./app/app'); 3 | 4 | describe('Koa URL Query Validation', function(){ 5 | it('Should throw the required rule errors when conditions dont match', function(done){ 6 | request(app.listen()).get('/') 7 | .query({ bar: 'barbaz' }) 8 | .query({ baz: 'foobar' }) 9 | .query({ barbaz: 'foobar' }) 10 | .query({ bazbaz: 'barbaz' }) 11 | .query({ age: 25}) 12 | .end(function(err, res){ 13 | res.statusCode.should.equal(422); 14 | res.body.should.be.an.Array; 15 | errorFields = {}; 16 | res.body.forEach(function(objs){ 17 | for(var o in objs){ 18 | errorFields[o] = objs[o].rule; 19 | } 20 | }); 21 | 22 | errorFields.should.have.properties({ 23 | foo: 'requiredWith', 24 | girlfiend: 'requiredIf', 25 | wife: 'requiredNotIf', 26 | foobar: 'requiredWithAll', 27 | gandalf: 'requiredWithout', 28 | name: 'required', 29 | tyrion: 'requiredWithoutAll' 30 | }); 31 | 32 | done(); 33 | }); 34 | }) 35 | 36 | it('Should throw errors on all fields', function(done){ 37 | request(app.listen()).get('/') 38 | .query({ bar: 'barbaz' }) 39 | .query({ baz: 'foobar' }) 40 | .query({ teenage: 20 }) 41 | .query({ age: 'twenty' }) 42 | .query({ bazbaz: 'barbaz' }) 43 | .query({ date: '2015-14-23' }) 44 | .query({ past: '2015-11-01' }) 45 | .query({ future: '2015-01-01' }) 46 | .query({ birthdate: 'barbaz' }) 47 | .query({ gender: 'other' }) 48 | .query({ genres: 'Pop' }) 49 | .query({ grade: 'no' }) 50 | .query({ nickname: 1234 }) 51 | .query({ nospaces: '124%6$' }) 52 | .query({ email: 'lucky@name' }) 53 | .query({ alphanum: '&*^%' }) 54 | .query({ password: 'abnsd' }) 55 | .query({ iaccept: 'yes' }) 56 | .query({ partofit: 'become' }) 57 | .query({ notpartofit: 'forward' }) 58 | .query({ cpassword: 'word' }) 59 | .query({ spousegender: 'other' }) 60 | .query({ luckynum: 1234 }) 61 | .query({ thesaurus: 'synonyms' }) 62 | .query({ number: 'abdc' }) 63 | .query({ ipaddress: '200.200.200' }) 64 | .query({ object: 'notaJSON' }) 65 | .query({ chocolates: 95 }) 66 | .query({ watts: 20 }) 67 | .query({ longword: 'jsdsakjdsad' }) 68 | .query({ shortword: 'thisismorethan10chars' }) 69 | .query({ tendigits: 123456789 }) 70 | .query({ watch: 'asia' }) 71 | .query({ website: 'world.' }) 72 | .end(function(err, res){ 73 | res.statusCode.should.equal(422); 74 | res.body.should.be.an.Array; 75 | errorFields = {}; 76 | res.body.forEach(function(objs){ 77 | for(var o in objs){ 78 | errorFields[o] = objs[o].rule; 79 | } 80 | }); 81 | 82 | errorFields.should.have.properties({ 83 | age: 'numeric', 84 | teenage: 'digitsBetween', 85 | date: 'dateFormat', 86 | birthdate: 'date', 87 | past: 'before', 88 | future: 'after', 89 | gender: 'in', 90 | genres: 'notIn', 91 | grade: 'accepted', 92 | nickname: 'alpha', 93 | nospaces: 'alphaDash', 94 | email: 'email', 95 | alphanum: 'alphaNumeric', 96 | password: 'between', 97 | iaccept: 'boolean', 98 | partofit: 'contains', 99 | notpartofit: 'notContains', 100 | cpassword: 'same', 101 | spousegender: 'different', 102 | luckynum: 'digits', 103 | ipaddress: 'ip', 104 | thesaurus: 'equals', 105 | number: 'integer', 106 | object: 'json', 107 | chocolates: 'max', 108 | watts: 'min', 109 | shortword: 'maxLength', 110 | longword: 'minLength', 111 | tendigits: 'regex', 112 | watch: 'timezone', 113 | website: 'url' 114 | }); 115 | 116 | done(); 117 | }); 118 | }) 119 | 120 | it('Should throw no errors when proper values are sent', function(done){ 121 | request(app.listen()).get('/') 122 | .query({ name: 'Srinivas Iyer' }) 123 | .query({ bar: 'barbaz' }) 124 | .query({ baz: 'foobar' }) 125 | .query({ foo: 'bar' }) 126 | .query({ tyrion: 'lanister' }) 127 | .query({ gandalf: 'grey' }) 128 | .query({ teenage: 17 }) 129 | .query({ age: 22 }) 130 | .query({ bazbaz: 'barbaz' }) 131 | .query({ date: '12232015' }) 132 | .query({ past: '2015-06-01' }) 133 | .query({ future: '2015-11-01' }) 134 | .query({ birthdate: '2016-12-25' }) 135 | .query({ gender: 'male' }) 136 | .query({ genres: 'jazz' }) 137 | .query({ grade: 'yes' }) 138 | .query({ nickname: 'srini' }) 139 | .query({ nospaces: 'this_is-what' }) 140 | .query({ email: 'lucky@strike.com' }) 141 | .query({ alphanum: 'abcd928921' }) 142 | .query({ password: 'abcd1234' }) 143 | .query({ iaccept: 1 }) 144 | .query({ partofit: 'became' }) 145 | .query({ notpartofit: 'forewarn' }) 146 | .query({ cpassword: 'abcd1234' }) 147 | .query({ spousegender: 'female' }) 148 | .query({ luckynum: 8974 }) 149 | .query({ thesaurus: 'dictionary' }) 150 | .query({ number: 1234 }) 151 | .query({ ipaddress: '192.168.0.1' }) 152 | .query({ object: '{ "foo": "bar" }' }) 153 | .query({ chocolates: 80 }) 154 | .query({ watts: 30 }) 155 | .query({ longword: 'This is a super long string which stretches beyond 25 charachters' }) 156 | .query({ shortword: 'lessthan10' }) 157 | .query({ tendigits: 1234567890 }) 158 | .query({ watch: 'asia/kolkata' }) 159 | .query({ website: 'srinivasiyer.com' }) 160 | .end(function(err, res){ 161 | res.statusCode.should.equal(200); 162 | res.body.should.be.an.Object; 163 | done(); 164 | }); 165 | }); 166 | 167 | it('should return changed queries when before filters are applied', function(done){ 168 | 169 | request(app.listen()) 170 | .get('/filters/before') 171 | .query({name: 'LOWERCASE'}) 172 | .query({nickname: 'uppercase'}) 173 | .query({snum: '92349021'}) 174 | .query({sword: ' trim '}) 175 | .query({lword: ' ltrim'}) 176 | .query({rword: 'rtrim '}) 177 | .query({dnum: '1234.23'}) 178 | .query({bword: 'false'}) 179 | .query({obj: { 'foo': 'bar' }}) 180 | .query({eword: ''}) 181 | .query({reword: 'become'}) 182 | .query({shaword: 'password'}) 183 | .query({mdword: 'password'}) 184 | .query({hexword: 'password'}) 185 | .end(function(err, res){ 186 | res.body.should.be.an.object; 187 | res.statusCode.should.equal(200); 188 | res.body.should.have.properties({ 189 | name: 'lowercase', 190 | nickname: 'UPPERCASE', 191 | snum: 92349021, 192 | sword: 'trim', 193 | lword: 'ltrim', 194 | rword: 'rtrim', 195 | dnum: 1234.23, 196 | bword: false, 197 | obj: '{"foo":"bar"}', 198 | eword: '<html></html>', 199 | reword: 'became', 200 | shaword: '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8', 201 | mdword: '5f4dcc3b5aa765d61d8327deb882cf99', 202 | hexword: '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8' 203 | }); 204 | 205 | done(); 206 | }); 207 | }); 208 | 209 | it('should return changed queries when filters are applied', function(done){ 210 | request(app.listen()) 211 | .get('/filters/before') 212 | .query({name: 'LOWERCASE'}) 213 | .query({nickname: 'uppercase'}) 214 | .query({snum: '92349021'}) 215 | .query({sword: ' trim '}) 216 | .query({lword: ' ltrim'}) 217 | .query({rword: 'rtrim '}) 218 | .query({dnum: '1234.23'}) 219 | .query({bword: 'false'}) 220 | .query({obj: { 'foo': 'bar' }}) 221 | .query({eword: ''}) 222 | .query({reword: 'become'}) 223 | .query({shaword: 'password'}) 224 | .query({mdword: 'password'}) 225 | .query({hexword: 'password'}) 226 | .end(function(err, res){ 227 | res.body.should.be.an.object; 228 | res.statusCode.should.equal(200); 229 | res.body.should.have.properties({ 230 | name: 'lowercase', 231 | nickname: 'UPPERCASE', 232 | snum: 92349021, 233 | sword: 'trim', 234 | lword: 'ltrim', 235 | rword: 'rtrim', 236 | dnum: 1234.23, 237 | bword: false, 238 | obj: '{"foo":"bar"}', 239 | eword: '<html></html>', 240 | reword: 'became', 241 | shaword: '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8', 242 | mdword: '5f4dcc3b5aa765d61d8327deb882cf99', 243 | hexword: '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8' 244 | }); 245 | 246 | done(); 247 | }); 248 | }) 249 | 250 | }); 251 | --------------------------------------------------------------------------------