├── .gitignore ├── LICENSE ├── README.md ├── TODO.md ├── index.js ├── lib ├── field.js ├── form.js └── utils.js ├── package.json └── test ├── express.test.js ├── filter.test.js ├── form.test.js ├── more.test.js └── validate.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2010 Dan Dean 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the 'Software'), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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 | ## Primary fork has moved to [freewil/express-form](https://github.com/freewil/express-form) 2 | 3 | 4 | Express Form provides data filtering and validation as route middleware to your Express applications. 5 | 6 | Usage: 7 | ------ 8 | 9 | var form = require("express-form"), 10 | field = form.field; 11 | 12 | var app = express.createServer(); 13 | 14 | app.configure(function() { 15 | app.use(express.bodyDecoder()); 16 | app.use(app.router); 17 | }); 18 | 19 | app.post( 20 | 21 | // Route 22 | '/user', 23 | 24 | // Form filter and validation middleware 25 | form( 26 | field("username").trim().required().is(/^[a-z]+$/), 27 | field("password").trim().required().is(/^[0-9]+$/), 28 | field("email").trim().isEmail() 29 | ), 30 | 31 | // Express request-handler now receives filtered and validated data 32 | function(req, res){ 33 | if (!req.form.isValid) { 34 | // Handle errors 35 | console.log(req.form.errors); 36 | 37 | } else { 38 | // Or, use filtered form data from the form object: 39 | console.log("Username:", req.form.username); 40 | console.log("Password:", req.form.password); 41 | console.log("Email:", req.form.email); 42 | } 43 | } 44 | ); 45 | 46 | Documentation: 47 | -------------- 48 | 49 | ### Module 50 | 51 | The Express Form **module** returns an Express [Route Middleware](http://expressjs.com/guide.html#Route-Middleware) function. You specify filtering and validation by passing filters and validators as arguments to the main module function. For example: 52 | 53 | var form = require("express-form"); 54 | 55 | app.post('/user', 56 | 57 | // Express Form Route Middleware: trims whitespace off of 58 | // the `username` field. 59 | form(form.field("username").trim()), 60 | 61 | // standard Express handler 62 | function(req, res) { 63 | // ... 64 | } 65 | ); 66 | 67 | 68 | ### Fields 69 | 70 | The `field` property of the module creates a filter/validator object tied to a specific field. 71 | 72 | field(fieldname[, label]); 73 | 74 | You can access nested properties with either dot or square-bracket notation. 75 | 76 | field("post.content").minLength(50), 77 | field("post[user][id]").isInt(), 78 | field("post.super.nested.property").required() 79 | 80 | Simply specifying a property like this, makes sure it exists. So, even if `req.body.post` was undefined, `req.form.post.content` would be defined. This helps avoid any unwanted errors in your code. 81 | 82 | The API is chainable, so you can keep calling filter/validator methods one after the other: 83 | 84 | filter("username").trim().toLower().truncate(5).required().isAlphanumeric() 85 | 86 | #### Filter API: 87 | 88 | Type Coercion 89 | 90 | toFloat() -> Number 91 | 92 | toInt() -> Number, rounded down 93 | 94 | toBoolean() -> Boolean from truthy and falsy values 95 | 96 | toBooleanStrict() -> Only true, "true", 1 and "1" are `true` 97 | 98 | ifNull(replacement) -> "", undefined and null get replaced by `replacement` 99 | 100 | 101 | HTML Encoding for `& " < >` 102 | 103 | entityEncode() -> encodes HTML entities 104 | 105 | entityDecode() -> decodes HTML entities 106 | 107 | 108 | String Transformations 109 | 110 | trim(chars) -> `chars` defaults to whitespace 111 | 112 | ltrim(chars) 113 | 114 | rtrim(chars) 115 | 116 | toLower() / toLowerCase() 117 | 118 | toUpper() / toUpperCase() 119 | 120 | truncate(length) -> Chops value at (length - 3), appends `...` 121 | 122 | 123 | #### Validator API: 124 | 125 | **Validation messages**: each validator has its own default validation message. These can easily be overridden at runtime by passing a custom validation message to the validator. The custom message is always the **last** argument passed to the validator. 126 | 127 | Use "%s" in the message to have the field name or label printed in the message: 128 | 129 | validate("username").required() 130 | // -> "username is required" 131 | 132 | validate("username").required("What is your %s?") 133 | // -> "What is your username?" 134 | 135 | validate("username", "Username").required("What is your %s?") 136 | // -> "What is your Username?" 137 | 138 | 139 | **Validation Methods** 140 | 141 | *By Regular Expressions* 142 | 143 | regex(pattern[, modifiers[, message]]) 144 | - pattern (RegExp|String): RegExp (with flags) or String pattern. 145 | - modifiers (String): Optional, and only if `pattern` is a String. 146 | - message (String): Optional validation message. 147 | 148 | alias: is 149 | 150 | Checks that the value matches the given regular expression. 151 | 152 | Example: 153 | 154 | validate("username").is("[a-z]", "i", "Only letters are valid in %s") 155 | validate("username").is(/[a-z]/i, "Only letters are valid in %s") 156 | 157 | 158 | notRegex(pattern[, modifiers[, message]]) 159 | - pattern (RegExp|String): RegExp (with flags) or String pattern. 160 | - modifiers (String): Optional, and only if `pattern` is a String. 161 | - message (String): Optional validation message. 162 | 163 | alias: not 164 | 165 | Checks that the value does NOT match the given regular expression. 166 | 167 | Example: 168 | 169 | validate("username").not("[a-z]", "i", "Letters are not valid in %s") 170 | validate("username").not(/[a-z]/i, "Letters are not valid in %s") 171 | 172 | 173 | *By Type* 174 | 175 | isNumeric([message]) 176 | 177 | isInt([message]) 178 | 179 | isDecimal([message]) 180 | 181 | isFloat([message]) 182 | 183 | 184 | *By Format* 185 | 186 | isEmail([message]) 187 | 188 | isUrl([message]) 189 | 190 | isIP([message]) 191 | 192 | isAlpha([message]) 193 | 194 | isAlphanumeric([message]) 195 | 196 | isLowercase([message]) 197 | 198 | isUppercase([message]) 199 | 200 | 201 | *By Content* 202 | 203 | notEmpty([message]) 204 | 205 | Checks if the value is not just whitespace. 206 | 207 | 208 | equals( value [, message] ) 209 | - value (String): A value that should match the field value OR a fieldname 210 | token to match another field, ie, `field::password`. 211 | 212 | Compares the field to `value`. 213 | 214 | Example: 215 | validate("username").equals("admin") 216 | 217 | validate("password").is(/^\w{6,20}$/) 218 | validate("password_confirmation").equals("field::password") 219 | 220 | 221 | contains(value[, message]) 222 | - value (String): The value to test for. 223 | 224 | Checks if the field contains `value`. 225 | 226 | 227 | notContains(string[, message]) 228 | - value (String): A value that should not exist in the field. 229 | 230 | Checks if the field does NOT contain `value`. 231 | 232 | 233 | *Other* 234 | 235 | required([message]) 236 | 237 | Checks that the field is present in form data, and has a value. 238 | 239 | ### Array Method 240 | 241 | array() 242 | Using the array() flag means that field always gives an array. If the field value is an array, but there is no flag, then the first value in that array is used instead. 243 | 244 | This means that you don't have to worry about unexpected post data that might break your code. Eg/ when you call an array method on what is actually a string. 245 | 246 | field("project.users").array(), 247 | // undefined => [], "" => [], "q" => ["q"], ["a", "b"] => ["a", "b"] 248 | 249 | field("project.block"), 250 | // project.block: ["a", "b"] => "a". No "array()", so only first value used. 251 | 252 | In addition, any other methods called with the array method, are applied to every value within the array. 253 | 254 | field("post.users").array().toUpper() 255 | // post.users: ["one", "two", "three"] => ["ONE", "TWO", "THREE"] 256 | 257 | ### Custom Methods 258 | 259 | custom(function[, message]) 260 | - function (Function): A custom filter or validation function. 261 | 262 | This method can be utilised as either a filter or validator method. 263 | 264 | If the function throws an error, then an error is added to the form. (If `message` is not provided, the thrown error message is used.) 265 | 266 | If the function returns a value, then it is considered a filter method, with the field then becoming the returned value. 267 | 268 | If the function returns undefined, then the method has no effect on the field. 269 | 270 | Examples: 271 | 272 | If the `name` field has a value of "hello there", this would 273 | transform it to "hello-there". 274 | 275 | field("name").custom(function(value) { 276 | return value.replace(/\s+/g, "-"); 277 | }); 278 | 279 | Throws an error if `username` field does not have value "admin". 280 | 281 | field("username").custom(function(value) { 282 | if (value !== "admin") { 283 | throw new Error("%s must be 'admin'."); 284 | } 285 | }); 286 | 287 | 288 | ### http.ServerRequest.prototype.form 289 | 290 | Express Form adds a `form` object with various properties to the request. 291 | 292 | isValid -> Boolean 293 | 294 | errors -> Array 295 | 296 | flashErrors(name) -> undefined 297 | 298 | Flashes all errors. Configurable, enabled by default. 299 | 300 | getErrors(name) -> Array or Object if no name given 301 | - fieldname (String): The name of the field 302 | 303 | Gets all errors for the field with the given name. 304 | 305 | You can also call this method with no parameters to get a map of errors for all of the fields. 306 | 307 | Example request handler: 308 | 309 | function(req, res) { 310 | if (!req.form.isValid) { 311 | console.log(req.errors); 312 | console.log(req.getErrors("username")); 313 | console.log(req.getErrors()); 314 | } 315 | } 316 | 317 | ### Configuration 318 | 319 | Express Form has various configuration options, but aims for sensible defaults for a typical Express application. 320 | 321 | form.configure(options) -> self 322 | - options (Object): An object with configuration options. 323 | 324 | flashErrors (Boolean): If validation errors should be automatically passed to Express’ flash() method. Default: true. 325 | 326 | autoLocals (Boolean): If field values from Express’ request.body should be passed into Express’ response.locals object. This is helpful when a form is invalid an you want to repopulate the form elements with their submitted values. Default: true. 327 | 328 | Note: if a field name dash-separated, the name used for the locals object will be in camelCase. 329 | 330 | dataSources (Array): An array of Express request properties to use as data sources when filtering and validating data. Default: ["body", "query", "params"]. 331 | 332 | autoTrim (Boolean): If true, all fields will be automatically trimmed. Default: false. 333 | 334 | passThrough (Boolean): If true, all data sources will be merged with `req.form`. Default: false. 335 | 336 | 337 | Installation: 338 | ------------- 339 | 340 | npm install express-form 341 | 342 | 343 | Credits 344 | ------- 345 | 346 | Currently, Express Form uses many of the validation and filtering functions provided by Chris O'Hara's [node-validator](https://github.com/chriso/node-validator). 347 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | TODO 2 | ================================================================================ 3 | 4 | 5 | Features 6 | -------- 7 | 8 | ### Configurability 9 | 10 | What configuration options should be available? 11 | 12 | * Debugging? 13 | * Default message overrides? 14 | 15 | 16 | Testing and Compatibility 17 | ------------------------- 18 | 19 | * Formidable and Connect-Form 20 | * Do not assume data is a String. May need to drop `node-validator` dependency. 21 | 22 | 23 | Other 24 | ----- 25 | 26 | * **Async validation (for databases, etc)** 27 | * Add notes on how to extend the filters and validators 28 | * Change node-validator toUppercase/toLowercase to use standard JS caps: toUpper**C**ase, toLower**C**ase. 29 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./lib/form"); 2 | -------------------------------------------------------------------------------- /lib/field.js: -------------------------------------------------------------------------------- 1 | var validator = require("validator") 2 | , FilterPrototype = validator.Filter.prototype 3 | , ValidatorPrototype = validator.Validator.prototype 4 | , externalFilter = new validator.Filter() 5 | , externalValidator = new validator.Validator() 6 | , object = require("object-additions").object 7 | , utils = require("./utils"); 8 | 9 | function Field(property, label) { 10 | var stack = [] 11 | , isArray = false 12 | , fieldLabel = label || property; 13 | 14 | this.name = property; 15 | this.__required = false; 16 | 17 | this.add = function(func) { 18 | stack.push(func); 19 | return this; 20 | }; 21 | 22 | this.array = function() { 23 | isArray = true; 24 | return this; 25 | }; 26 | 27 | this.run = function (source, form, options) { 28 | var self = this 29 | , errors = [] 30 | , value = utils.getProp(property, form) || utils.getProp(property, source); 31 | 32 | if (options.autoTrim) { 33 | stack.unshift(function (value) { 34 | if (object.isString(value)) { 35 | return FilterPrototype.trim.apply(externalFilter.sanitize(value)); 36 | } 37 | return value; 38 | }); 39 | } 40 | 41 | function runStack(foo) { 42 | 43 | stack.forEach(function (proc) { 44 | var result = proc(foo, source); // Pass source for "equals" proc. 45 | if (result.valid) return; 46 | if (result.error) { 47 | // If this field is not required and it doesn't have a value, ignore error. 48 | if (!utils.hasValue(value) && !self.__required) return; 49 | 50 | return errors.push(result.error.replace("%s", fieldLabel)); 51 | } 52 | foo = result; 53 | }); 54 | 55 | return foo; 56 | } 57 | 58 | if (isArray) { 59 | if (!utils.hasValue(value)) value = []; 60 | if (!Array.isArray(value)) value = [value]; 61 | value = value.map(runStack); 62 | 63 | } else { 64 | if (Array.isArray(value)) value = value[0]; 65 | value = runStack(value); 66 | } 67 | 68 | utils.setProp(property, form, value); 69 | 70 | if (errors.length) return errors; 71 | }; 72 | } 73 | 74 | // ARRAY METHODS 75 | 76 | Field.prototype.array = function () { 77 | return this.array(); 78 | }; 79 | 80 | Field.prototype.arrLength = function (from, to) { 81 | return this.add(function (arr) { 82 | if (value.length < from) { 83 | return { error: message || e.message || "%s is too short" }; 84 | } 85 | if (value.length > to) { 86 | return { error: message || e.message || "%s is too long" }; 87 | } 88 | return { valid: true }; 89 | }); 90 | } 91 | 92 | // HYBRID METHODS 93 | 94 | Field.prototype.custom = function(func, message) { 95 | return this.add(function (value, source) { 96 | try { 97 | var result = func(value, source); 98 | } catch (e) { 99 | return { error: message || e.message || "%s is invalid" }; 100 | } 101 | // Functions that return values are filters. 102 | if (result != null) return result; 103 | 104 | return { valid: true }; 105 | }); 106 | }; 107 | 108 | // FILTER METHODS 109 | 110 | Object.keys(FilterPrototype).forEach(function (name) { 111 | if (name.match(/^ifNull$/)) return; 112 | 113 | Field.prototype[name] = function () { 114 | var args = arguments; 115 | return this.add(function (value) { 116 | var a = FilterPrototype[name].apply(externalFilter.sanitize(value), args); 117 | return a; 118 | }); 119 | }; 120 | }); 121 | 122 | Field.prototype.ifNull = function (replacement) { 123 | return this.add(function (value) { 124 | if (object.isUndefined(value) || null === value || '' === value) { 125 | return replacement; 126 | } 127 | return value; 128 | }); 129 | }; 130 | 131 | Field.prototype.toUpper = Field.prototype.toUpperCase = function () { 132 | return this.add(function (value) { 133 | return value.toUpperCase(); 134 | }); 135 | }; 136 | 137 | Field.prototype.toLower = Field.prototype.toLowerCase = function () { 138 | return this.add(function (value) { 139 | return value.toLowerCase(); 140 | }); 141 | }; 142 | 143 | Field.prototype.truncate = function (length) { 144 | return this.add(function (value) { 145 | if (value.length <= length) { 146 | return value; 147 | } 148 | 149 | if (length <= 3) return "..."; 150 | 151 | if (value.length > length - 3) { 152 | return value.substr(0,length - 3) + "..."; 153 | } 154 | 155 | return value; 156 | }); 157 | }; 158 | 159 | Field.prototype.customFilter = function (func) { 160 | return this.add(func); 161 | }; 162 | 163 | // VALIDATE METHODS 164 | 165 | var MESSAGES = { 166 | isEmail: "%s is not an email address", 167 | isUrl: "%s is not a URL", 168 | isIP: "%s is not an IP address", 169 | isAlpha: "%s contains non-letter characters", 170 | isAlphanumeric: "%s contains non alpha-numeric characters", 171 | isNumeric: "%s is not numeric", 172 | isLowercase: "%s contains uppercase letters", 173 | isUppercase: "%s contains lowercase letters", 174 | isInt: "%s is not an integer", 175 | notEmpty: "%s has no value or is only whitespace" 176 | }; 177 | 178 | Object.keys(ValidatorPrototype).forEach(function (name) { 179 | if (name.match(/^(contains|notContains|equals|check|validate|assert|error|len|isNumeric|isDecimal|isFloat|regex|notRegex|is|not|notNull|isNull)$/)) { 180 | return; 181 | } 182 | 183 | Field.prototype[name] = function (message) { 184 | var args = arguments; 185 | message = message || MESSAGES[name]; 186 | 187 | return this.add(function(value) { 188 | try { 189 | ValidatorPrototype[name].apply(externalValidator.check(value, message), args); 190 | } catch (e) { 191 | return { error: e.message || e.toString() }; 192 | } 193 | return { valid: true }; 194 | }); 195 | }; 196 | }); 197 | 198 | Field.prototype.contains = function (test, message) { 199 | return this.add(function(value) { 200 | try { 201 | ValidatorPrototype.contains.call(externalValidator.check(value, message), test); 202 | } catch (e) { 203 | return { error: message || "%s does not contain required characters" }; 204 | } 205 | return { valid: true }; 206 | }); 207 | }; 208 | 209 | Field.prototype.notContains = function (test, message) { 210 | return this.add(function (value) { 211 | try { 212 | ValidatorPrototype.notContains.call(externalValidator.check(value, message), test); 213 | } catch (e) { 214 | return { error: message || "%s contains invalid characters" }; 215 | } 216 | return { valid: true }; 217 | }); 218 | }; 219 | 220 | 221 | Field.prototype.equals = function (other, message) { 222 | if (object.isString(other) && other.match(/^field::/)) { 223 | this.__required = true; 224 | } 225 | 226 | return this.add(function (value, source) { 227 | // If other is a field token (field::fieldname), grab the value of fieldname 228 | // and use that as the OTHER value. 229 | var test = other; 230 | if (object.isString(other) && other.match(/^field::/)) { 231 | test = utils.getProp(other.replace(/^field::/, ""), source); 232 | } 233 | if (value != test) { 234 | return { error: message || "%s does not equal " + String(test) }; 235 | } 236 | return { valid: true }; 237 | }); 238 | }; 239 | 240 | // node-validator's numeric validator seems unintuitive. All numeric values should be valid, not just int. 241 | Field.prototype.isNumeric = function (message) { 242 | return this.add(function (value) { 243 | if (object.isNumber(value) || (object.isString(value) && value.match(/^[-+]?[0-9]*\.?[0-9]+$/))) { 244 | } else { 245 | return { error: message || "%s is not a number" }; 246 | } 247 | return { valid: true }; 248 | }); 249 | }; 250 | 251 | // node-validator's decimal/float validator incorrectly thinks Ints are valid. 252 | Field.prototype.isFloat = Field.prototype.isDecimal = function (message) { 253 | return this.add(function (value) { 254 | if ((object.isNumber(value) && value % 1 == 0) || (object.isString(value) && value.match(/^[-+]?[0-9]*\.[0-9]+$/))) { 255 | } else { 256 | return { error: message || "%s is not a decimal" }; 257 | } 258 | return { valid: true }; 259 | }); 260 | }; 261 | 262 | Field.prototype.regex = Field.prototype.is = function (pattern, modifiers, message) { 263 | // regex(/pattern/) 264 | // regex(/pattern/, "message") 265 | // regex("pattern") 266 | // regex("pattern", "modifiers") 267 | // regex("pattern", "message") 268 | // regex("pattern", "modifiers", "message") 269 | 270 | if (pattern instanceof RegExp) { 271 | if (object.isString(modifiers) && modifiers.match(/^[gimy]+$/)) { 272 | throw new Error("Invalid arguments: `modifiers` can only be passed in if `pattern` is a string."); 273 | } 274 | 275 | message = modifiers; 276 | modifiers = undefined; 277 | 278 | } else if (object.isString(pattern)) { 279 | if (arguments.length == 2 && !modifiers.match(/^[gimy]+$/)) { 280 | // 2nd arg doesn't look like modifier flags, it's the message (might also be undefined) 281 | message = modifiers; 282 | modifiers = undefined; 283 | } 284 | pattern = new RegExp(pattern, modifiers); 285 | } 286 | 287 | return this.add(function (value) { 288 | if (pattern.test(value) === false) { 289 | return { error: message || "%s has invalid characters" }; 290 | } 291 | return { valid: true }; 292 | }); 293 | }; 294 | 295 | Field.prototype.notRegex = Field.prototype.not = function(pattern, modifiers, message) { 296 | // notRegex(/pattern/) 297 | // notRegex(/pattern/, "message") 298 | // notRegex("pattern") 299 | // notRegex("pattern", "modifiers") 300 | // notRegex("pattern", "message") 301 | // notRegex("pattern", "modifiers", "message") 302 | 303 | if (pattern instanceof RegExp) { 304 | if (object.isString(modifiers) && modifiers.match(/^[gimy]+$/)) { 305 | throw new Error("Invalid arguments: `modifiers` can only be passed in if `pattern` is a string."); 306 | } 307 | 308 | message = modifiers; 309 | modifiers = undefined; 310 | 311 | } else if (object.isString(pattern)) { 312 | if (arguments.length == 2 && !modifiers.match(/^[gimy]+$/)) { 313 | // 2nd arg doesn't look like modifier flags, it's the message (might also be undefined) 314 | message = modifiers; 315 | modifiers = undefined; 316 | } 317 | pattern = new RegExp(pattern, modifiers); 318 | } 319 | 320 | return this.add(function(value) { 321 | if (pattern.test(value) === true) { 322 | return { error: message || "%s has invalid characters" }; 323 | } 324 | return { valid: true }; 325 | }); 326 | }; 327 | 328 | Field.prototype.required = function (placeholderValue, message) { 329 | this.__required = true; 330 | return this.add(function (value) { 331 | if (!utils.hasValue(value) || value == placeholderValue) { 332 | return { error: message || "%s is required" }; 333 | } 334 | return { valid: true }; 335 | }); 336 | }; 337 | 338 | Field.prototype.minLength = function (length, message) { 339 | return this.add(function(value) { 340 | if (value.toString().length < length) { 341 | return { error: message || "%s is too short" }; 342 | } 343 | return { valid: true }; 344 | }); 345 | }; 346 | 347 | Field.prototype.maxLength = function (length, message) { 348 | return this.add(function(value) { 349 | if (value.toString().length > length) { 350 | return { error: message || "%s is too long" }; 351 | } 352 | return { valid: true }; 353 | }); 354 | }; 355 | 356 | Field.prototype.customValidator = function(func, message) { 357 | return this.add(function(value, source) { 358 | try { 359 | func(value, source); 360 | } catch (e) { 361 | return { error: message || e.message || "%s is invalid" }; 362 | } 363 | return { valid: true }; 364 | }); 365 | }; 366 | 367 | module.exports = Field; -------------------------------------------------------------------------------- /lib/form.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Express - Form 3 | * Copyright(c) 2010 Dan Dean 4 | * MIT Licensed 5 | */ 6 | 7 | var utils = require("./utils") 8 | , Field = require("./field"); 9 | 10 | function form() { 11 | var routines = Array.prototype.slice.call(arguments) 12 | , options = form._options; 13 | 14 | return function (req, res, next) { 15 | var map = {} 16 | , flashed = {} 17 | , mergedSource = {}; 18 | 19 | if (!req.form) req.form = {}; 20 | 21 | options.dataSources.forEach(function (source) { 22 | utils.merge(mergedSource, req[source]); 23 | }); 24 | 25 | if (options.passThrough) req.form = utils.clone(mergedSource); 26 | 27 | if (options.autoLocals) { 28 | for (var prop in req.body) { 29 | if (!req.body.hasOwnProperty(prop)) continue; 30 | 31 | if (typeof res.locals === "function") { // Express 2.0 Support 32 | res.local(utils.camelize(prop), req.body[prop]); 33 | } else { 34 | if (!res.locals) res.locals = {}; 35 | res.locals[utils.camelize(prop)] = req.body[prop]; 36 | } 37 | } 38 | } 39 | 40 | Object.defineProperties(req.form, { 41 | "errors": { 42 | value: [], 43 | enumerable: false 44 | }, 45 | "getErrors": { 46 | value: function (name) { 47 | if(!name) return map; 48 | 49 | return map[name] || []; 50 | }, 51 | enumerable: false 52 | }, 53 | "isValid": { 54 | get: function () { 55 | return this.errors.length === 0; 56 | }, 57 | enumerable: false 58 | }, 59 | "flashErrors": { 60 | value: function () { 61 | if (typeof req.flash !== "function") return; 62 | this.errors.forEach(function (error) { 63 | if (flashed[error]) return; 64 | 65 | flashed[error] = true; 66 | req.flash("error", error); 67 | }); 68 | }, 69 | enumerable: false 70 | } 71 | }); 72 | 73 | routines.forEach(function (routine) { 74 | var result = routine.run(mergedSource, req.form, options); 75 | 76 | if (!Array.isArray(result) || !result.length) return; 77 | 78 | var errors = req.form.errors = req.form.errors || [] 79 | , name = routine.name; 80 | 81 | map[name] = map[name] || []; 82 | 83 | result.forEach(function (error) { 84 | errors.push(error); 85 | map[name].push(error); 86 | }); 87 | }); 88 | 89 | if (options.flashErrors) req.form.flashErrors(); 90 | 91 | if (next) next(); 92 | } 93 | } 94 | 95 | form.field = function (property, label) { 96 | return new Field(property, label); 97 | }; 98 | 99 | form.filter = form.validate = form.field; 100 | 101 | form._options = { 102 | dataSources: ["body", "query", "params"], 103 | autoTrim: false, 104 | autoLocals: true, 105 | passThrough: false, 106 | flashErrors: true 107 | }; 108 | 109 | form.configure = function (options) { 110 | for (var p in options) { 111 | if (!Array.isArray(options[p]) && p === "dataSources") { 112 | options[p] = [options[p]]; 113 | } 114 | this._options[p] = options[p]; 115 | } 116 | return this; 117 | } 118 | 119 | module.exports = form; -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | // Convert square-bracket to dot notation. 2 | var toDotNotation = exports.toDotNotation = function (str) { 3 | return str.replace(/\[((.)*?)\]/g, ".$1"); 4 | }; 5 | 6 | // Gets nested properties without throwing errors. 7 | var getProp = exports.getProp = function (property, obj) { 8 | var levels = toDotNotation(property).split("."); 9 | 10 | while (obj != null && levels[0]) { 11 | obj = obj[levels.shift()]; 12 | if (obj == null) obj = ""; 13 | } 14 | 15 | return obj; 16 | } 17 | 18 | // Sets nested properties. 19 | var setProp = exports.setProp = function (property, obj, value) { 20 | var levels = toDotNotation(property).split("."); 21 | 22 | while (levels[0]) { 23 | var p = levels.shift(); 24 | if (typeof obj[p] !== "object") obj[p] = {}; 25 | if (!levels.length) obj[p] = value; 26 | obj = obj[p]; 27 | } 28 | 29 | return obj; 30 | } 31 | 32 | var clone = exports.clone = function (obj) { 33 | // Untested, probably better:-> return Object.create(obj).__proto__; 34 | return JSON.parse(JSON.stringify(obj)); 35 | } 36 | 37 | /** 38 | * camelize(str): -> String 39 | * - str (String): The string to make camel-case. 40 | * 41 | * Converts dash-separated words into camelCase words. Cribbed from Prototype.js. 42 | * 43 | * field-name -> fieldName 44 | * -field-name -> FieldName 45 | **/ 46 | var camelize = exports.camelize = function (str) { 47 | return (str || "").replace(/-+(.)?/g, function(match, chr) { 48 | return chr ? chr.toUpperCase() : ''; 49 | }); 50 | } 51 | 52 | /* 53 | * Recursively merge properties of two objects 54 | * http://stackoverflow.com/questions/171251/how-can-i-merge-properties-of-two-javascript-objects-dynamically/383245#383245 55 | */ 56 | var merge = exports.merge = function (obj1, obj2) { 57 | for (var p in obj2) { 58 | try { 59 | // Property in destination object set; update its value. 60 | if ( obj2[p].constructor==Object ) { 61 | obj1[p] = merge(obj1[p], obj2[p]); 62 | } else { 63 | obj1[p] = obj2[p]; 64 | } 65 | } catch (e) { 66 | // Property in destination object not set; create it and set its value. 67 | obj1[p] = obj2[p]; 68 | } 69 | } 70 | return obj1; 71 | } 72 | 73 | var hasValue = exports.hasValue = function (value) { 74 | return !(undefined === value || null === value || "" === value); 75 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Dan Dean <@dandean> (http://dandean.com)", 3 | "name": "express-form", 4 | "description": "Form validation and data filtering for Express", 5 | "version": "0.6.3", 6 | "homepage": "http://dandean.github.com/express-form", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/dandean/express-form.git" 10 | }, 11 | "contributors": [ 12 | "Marc Harter ", 13 | "Sugarstack <@sugarstack>" 14 | ], 15 | "keywords" : ["form", "validator", "validation", "express"], 16 | "dependencies": { 17 | "validator": ">= 0.1.2", 18 | "object-additions": ">= 0.5.0" 19 | }, 20 | "main" : "index", 21 | "bugs" : { "url" : "http://github.com/dandean/express-form/issues" }, 22 | "scripts": { "test": "expresso" }, 23 | "engines": { "node": ">=0.2.2" }, 24 | "licenses" : [{ 25 | "type" : "MIT", 26 | "url" : "http://github.com/dandean/express-form/raw/master/LICENSE" 27 | }] 28 | } 29 | -------------------------------------------------------------------------------- /test/express.test.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"), 2 | form = require("../index"), 3 | filter = form.filter, 4 | validate = form.validate, 5 | express = require("express"), 6 | app = express.createServer(); 7 | 8 | app.configure(function() { 9 | app.use(express.bodyParser()); 10 | app.use(app.router); 11 | }); 12 | 13 | module.exports = { 14 | 'express : middleware : valid-form': function() { 15 | app.post( 16 | '/user', 17 | form( 18 | filter("username").trim(), 19 | validate("username").required().is(/^[a-z]+$/), 20 | filter("password").trim(), 21 | validate("password").required().is(/^[0-9]+$/) 22 | ), 23 | function(req, res){ 24 | assert.strictEqual(req.form.username, "dandean"); 25 | assert.strictEqual(req.form.password, "12345"); 26 | assert.strictEqual(req.form.isValid, true); 27 | assert.strictEqual(req.form.errors.length, 0); 28 | res.send(JSON.stringify(req.form)); 29 | } 30 | ); 31 | 32 | assert.response(app, 33 | { 34 | url: '/user', 35 | method: 'POST', 36 | body: JSON.stringify({ 37 | username: " dandean \n\n\t", 38 | password: " 12345 " 39 | }), 40 | headers: { 'Content-Type': 'application/json' } 41 | }, 42 | { status: 200 } 43 | ); 44 | }, 45 | 46 | 'express : middleware : merged-data': function() { 47 | app.post( 48 | '/user/:id', 49 | form( 50 | filter("id").toInt(), 51 | filter("stuff").toUpper(), 52 | filter("rad").toUpper() 53 | ), 54 | function(req, res){ 55 | // Validate filtered form data 56 | assert.strictEqual(req.form.id, 5); // from param 57 | assert.equal(req.form.stuff, "THINGS"); // from query param 58 | assert.equal(req.form.rad, "COOL"); // from body 59 | 60 | // Check that originl values are still in place 61 | assert.ok(typeof req.params.id, "string"); 62 | assert.equal(req.query.stuff, "things"); 63 | assert.equal(req.body.rad, "cool"); 64 | 65 | res.send(JSON.stringify(req.form)); 66 | } 67 | ); 68 | 69 | assert.response(app, 70 | { 71 | url: '/user/5?stuff=things&id=overridden', 72 | method: 'POST', 73 | body: JSON.stringify({ 74 | id: "overridden by url param", 75 | stuff: "overridden by query param", 76 | rad: "cool" 77 | }), 78 | headers: { 'Content-Type': 'application/json' } 79 | }, 80 | { status: 200 } 81 | ); 82 | } 83 | 84 | 85 | }; -------------------------------------------------------------------------------- /test/filter.test.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"), 2 | form = require("../index"), 3 | filter = form.filter; 4 | 5 | module.exports = { 6 | 'filter : trim': function() { 7 | var request = { body: { field: "\r\n value \t" }}; 8 | form(filter("field").trim())(request, {}); 9 | assert.equal(request.form.field, "value"); 10 | }, 11 | 12 | 'filter : ltrim': function() { 13 | var request = { body: { field: "\r\n value \t" }}; 14 | form(filter("field").ltrim())(request, {}); 15 | assert.equal(request.form.field, "value \t"); 16 | }, 17 | 18 | 'filter : rtrim': function() { 19 | var request = { body: { field: "\r\n value \t" }}; 20 | form(filter("field").rtrim())(request, {}); 21 | assert.equal(request.form.field, "\r\n value"); 22 | }, 23 | 24 | 'filter : ifNull': function() { 25 | // Replace missing value with "value" 26 | var request = { body: {} }; 27 | form(filter("field").ifNull("value"))(request, {}); 28 | assert.equal(request.form.field, "value"); 29 | 30 | // Replace empty string with value 31 | var request = { body: { field: "" }}; 32 | form(filter("field").ifNull("value"))(request, {}); 33 | assert.equal(request.form.field, "value"); 34 | 35 | // Replace NULL with value 36 | var request = { body: { field: null }}; 37 | form(filter("field").ifNull("value"))(request, {}); 38 | assert.equal(request.form.field, "value"); 39 | 40 | // Replace undefined with value 41 | var request = { body: { field: undefined }}; 42 | form(filter("field").ifNull("value"))(request, {}); 43 | assert.equal(request.form.field, "value"); 44 | 45 | // DO NOT replace false 46 | var request = { body: { field: false }}; 47 | form(filter("field").ifNull("value"))(request, {}); 48 | assert.equal(request.form.field, false); 49 | 50 | // DO NOT replace zero 51 | var request = { body: { field: 0 }}; 52 | form(filter("field").ifNull("value"))(request, {}); 53 | assert.equal(request.form.field, 0); 54 | }, 55 | 56 | 'filter : toFloat': function() { 57 | var request = { body: { field: "50.01" }}; 58 | form(filter("field").toFloat())(request, {}); 59 | assert.ok(typeof request.form.field == "number"); 60 | assert.equal(request.form.field, 50.01); 61 | 62 | var request = { body: { field: "fail" }}; 63 | form(filter("field").toFloat())(request, {}); 64 | assert.ok(typeof request.form.field == "number"); 65 | assert.ok(isNaN(request.form.field)); 66 | }, 67 | 68 | 'filter : toInt': function() { 69 | var request = { body: { field: "50.01" }}; 70 | form(filter("field").toInt())(request, {}); 71 | assert.ok(typeof request.form.field == "number"); 72 | assert.equal(request.form.field, 50); 73 | 74 | var request = { body: { field: "fail" }}; 75 | form(filter("field").toInt())(request, {}); 76 | assert.ok(typeof request.form.field == "number"); 77 | assert.ok(isNaN(request.form.field)); 78 | }, 79 | 80 | 'filter : toBoolean': function() { 81 | // Truthy values 82 | var request = { body: { 83 | field1: true, 84 | field2: "true", 85 | field3: "hi", 86 | field4: new Date(), 87 | field5: 50, 88 | field6: -1, 89 | field7: "3000" 90 | }}; 91 | form( 92 | filter("field1").toBoolean(), 93 | filter("field2").toBoolean(), 94 | filter("field3").toBoolean(), 95 | filter("field4").toBoolean(), 96 | filter("field5").toBoolean(), 97 | filter("field6").toBoolean(), 98 | filter("field7").toBoolean() 99 | )(request, {}); 100 | "1234567".split("").forEach(function(i) { 101 | var name = "field" + i; 102 | assert.strictEqual(typeof request.form[name], "boolean"); 103 | assert.strictEqual(request.form[name], true); 104 | }); 105 | 106 | // Falsy values 107 | var request = { body: { 108 | field1: false, 109 | field2: "false", 110 | field3: null, 111 | field4: undefined, 112 | field5: 0, 113 | field6: "0", 114 | field7: "" 115 | }}; 116 | form( 117 | filter("field1").toBoolean(), 118 | filter("field2").toBoolean(), 119 | filter("field3").toBoolean(), 120 | filter("field4").toBoolean(), 121 | filter("field5").toBoolean(), 122 | filter("field6").toBoolean(), 123 | filter("field7").toBoolean() 124 | )(request, {}); 125 | "1234567".split("").forEach(function(i) { 126 | var name = "field" + i; 127 | assert.strictEqual(typeof request.form[name], "boolean"); 128 | assert.strictEqual(request.form[name], false); 129 | }); 130 | }, 131 | 132 | 'filter : toBooleanStrict': function() { 133 | // Truthy values 134 | var request = { body: { 135 | field1: true, 136 | field2: "true", 137 | field3: 1, 138 | field4: "1" 139 | }}; 140 | form( 141 | filter("field1").toBooleanStrict(), 142 | filter("field2").toBooleanStrict(), 143 | filter("field3").toBooleanStrict(), 144 | filter("field4").toBooleanStrict() 145 | )(request, {}); 146 | "1234".split("").forEach(function(i) { 147 | var name = "field" + i; 148 | assert.strictEqual(typeof request.form[name], "boolean"); 149 | assert.strictEqual(request.form[name], true); 150 | }); 151 | 152 | // Falsy values 153 | var request = { body: { 154 | field1: false, 155 | field2: "false", 156 | field3: null, 157 | field4: undefined, 158 | field5: 0, 159 | field6: "0", 160 | field7: "", 161 | field8: new Date(), 162 | field9: 50, 163 | field0: -1, 164 | fielda: "3000" 165 | }}; 166 | form( 167 | filter("field1").toBooleanStrict(), 168 | filter("field2").toBooleanStrict(), 169 | filter("field3").toBooleanStrict(), 170 | filter("field4").toBooleanStrict(), 171 | filter("field5").toBooleanStrict(), 172 | filter("field6").toBooleanStrict(), 173 | filter("field7").toBooleanStrict(), 174 | filter("field8").toBooleanStrict(), 175 | filter("field9").toBooleanStrict(), 176 | filter("field0").toBooleanStrict(), 177 | filter("fielda").toBooleanStrict() 178 | )(request, {}); 179 | "1234567890a".split("").forEach(function(i) { 180 | var name = "field" + i; 181 | assert.strictEqual(typeof request.form[name], "boolean"); 182 | assert.strictEqual(request.form[name], false); 183 | }); 184 | }, 185 | 186 | 'filter : entityEncode': function() { 187 | // NOTE: single quotes are not encoded 188 | var request = { body: { field: "&\"<>hello!" }}; 189 | form(filter("field").entityEncode())(request, {}); 190 | assert.equal(request.form.field, "&"<>hello!"); 191 | }, 192 | 193 | 'filter : entityDecode': function() { 194 | var request = { body: { field: "&"<>hello!" }}; 195 | form(filter("field").entityDecode())(request, {}); 196 | assert.equal(request.form.field, "&\"<>hello!"); 197 | }, 198 | 199 | 'filter : toUpper': function() { 200 | var request = { body: { field: "hellö!" }}; 201 | form(filter("field").toUpper())(request, {}); 202 | assert.equal(request.form.field, "HELLÖ!"); 203 | }, 204 | 205 | 'filter : toLower': function() { 206 | var request = { body: { field: "HELLÖ!" }}; 207 | form(filter("field").toLower())(request, {}); 208 | assert.equal(request.form.field, "hellö!"); 209 | }, 210 | 211 | 'filter : truncate': function() { 212 | var request = { body: { 213 | field1: "1234567890", 214 | field2: "", 215 | field3: "123", 216 | field4: "123456", 217 | field5: "1234567890" 218 | }}; 219 | form( 220 | filter("field1").truncate(3), // ... 221 | filter("field2").truncate(3), // EMPTY 222 | filter("field3").truncate(3), // 123 223 | filter("field4").truncate(5), // 12... 224 | filter("field5").truncate(7) // 1234... 225 | )(request, {}); 226 | assert.equal(request.form.field1, "..."); 227 | assert.equal(request.form.field2, ""); 228 | assert.equal(request.form.field3, "123"); 229 | assert.equal(request.form.field4, "12..."); 230 | assert.equal(request.form.field5, "1234..."); 231 | }, 232 | 233 | 'filter : custom': function() { 234 | var request = { body: { field: "value!" }}; 235 | form(filter("field").custom(function(value) { 236 | return "!!!"; 237 | }))(request, {}); 238 | assert.equal(request.form.field, "!!!"); 239 | } 240 | 241 | }; -------------------------------------------------------------------------------- /test/form.test.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"), 2 | form = require("../index"), 3 | validate = form.validate; 4 | 5 | module.exports = { 6 | 'form : isValid': function() { 7 | // Failure. 8 | var request = { body: { field: "fail" }}; 9 | form(validate("field").isEmail())(request, {}); 10 | assert.strictEqual(request.form.isValid, false); 11 | 12 | // Success 13 | var request = { body: { field: "me@dandean.com" }}; 14 | form(validate("field").isEmail())(request, {}); 15 | assert.strictEqual(request.form.isValid, true); 16 | 17 | assert["throws"](function() { 18 | request.form.isValid = false; 19 | }); 20 | 21 | assert.strictEqual(request.form.isValid, true); 22 | }, 23 | 24 | 'form : getErrors': function() { 25 | var request = { 26 | body: { 27 | field0: "win", 28 | field1: "fail", 29 | field2: "fail", 30 | field3: "fail" 31 | } 32 | }; 33 | 34 | form( 35 | validate("field0").equals("win"), 36 | validate("field1").isEmail(), 37 | validate("field2").isEmail().isUrl(), 38 | validate("field3").isEmail().isUrl().isIP() 39 | )(request, {}); 40 | 41 | assert.equal(request.form.isValid, false); 42 | assert.equal(request.form.errors.length, 6); 43 | 44 | assert.equal(request.form.getErrors("field0").length, 0); 45 | assert.equal(request.form.getErrors("field1").length, 1); 46 | assert.equal(request.form.getErrors("field2").length, 2); 47 | assert.equal(request.form.getErrors("field3").length, 3); 48 | }, 49 | 50 | 'form : configure : dataSources': function() { 51 | form.configure({ dataSources: 'other' }); 52 | 53 | var request = { other: { field: "me@dandean.com" }}; 54 | form(validate("field").isEmail())(request, {}); 55 | assert.strictEqual(request.form.isValid, true); 56 | assert.equal(request.form.field, "me@dandean.com"); 57 | 58 | form.configure({ dataSources: ['body', "query", "params"] }); 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /test/more.test.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert") 2 | , form = require("../index") 3 | , field = form.field; 4 | 5 | module.exports = { 6 | 7 | "field : arrays": function () { 8 | // Array transformations. 9 | var request = { 10 | body: { 11 | field1: "", 12 | field2: "Hello!", 13 | field3: ["Alpacas?", "Llamas!!?", "Vicunas!", "Guanacos!!!"] 14 | } 15 | }; 16 | form( 17 | field("fieldx").array(), 18 | field("field1").array(), 19 | field("field2").array(), 20 | field("field3").array() 21 | )(request, {}); 22 | assert.strictEqual(Array.isArray(request.form.fieldx), true); 23 | assert.strictEqual(request.form.fieldx.length, 0); 24 | assert.strictEqual(Array.isArray(request.form.field1), true); 25 | assert.strictEqual(request.form.field1.length, 0); 26 | assert.strictEqual(request.form.field2[0], "Hello!"); 27 | assert.strictEqual(request.form.field2.length, 1); 28 | assert.strictEqual(request.form.field3[0], "Alpacas?"); 29 | assert.strictEqual(request.form.field3[1], "Llamas!!?"); 30 | assert.strictEqual(request.form.field3[2], "Vicunas!"); 31 | assert.strictEqual(request.form.field3[3], "Guanacos!!!"); 32 | assert.strictEqual(request.form.field3.length, 4); 33 | 34 | // No array flag! 35 | var request = { body: { field: ["red", "blue"] } }; 36 | form(field("field"))(request, {}); 37 | assert.strictEqual(request.form.field, "red"); 38 | 39 | // Iterate and filter array. 40 | var request = { body: { field: ["david", "stephen", "greg"] } }; 41 | form(field("field").array().toUpper())(request, {}); 42 | assert.strictEqual(request.form.field[0], "DAVID"); 43 | assert.strictEqual(request.form.field[1], "STEPHEN"); 44 | assert.strictEqual(request.form.field[2], "GREG"); 45 | assert.strictEqual(request.form.field.length, 3); 46 | 47 | // Iterate and validate array 48 | var request = { body: { field: [1, 2, "f"] } }; 49 | form(field("field").array().isInt())(request, {}); 50 | assert.equal(request.form.errors.length, 1); 51 | assert.equal(request.form.errors[0], "field is not an integer"); 52 | }, 53 | "field : nesting": function () { 54 | // Nesting with dot notation 55 | var request = { 56 | body: { 57 | field: { 58 | nest: "wow", 59 | child: "4", 60 | gb: { 61 | a: "a", 62 | b: "aaaa", 63 | c: { 64 | fruit: "deeper", 65 | must: { 66 | go: "deeperrrr" 67 | } 68 | } 69 | } 70 | } 71 | 72 | } 73 | }; 74 | form( 75 | field("field.nest").toUpper(), 76 | field("field.child").toUpper(), 77 | field("field.gb.a").toUpper(), 78 | field("field.gb.b").toUpper(), 79 | field("field.gb.c.fruit").toUpper(), 80 | field("field.gb.c.must.go").toUpper() 81 | )(request, {}); 82 | assert.strictEqual(request.form.field.nest, "WOW"); 83 | assert.strictEqual(request.form.field.child, "4"); 84 | assert.strictEqual(request.form.field.gb.a, "A"); 85 | assert.strictEqual(request.form.field.gb.b, "AAAA"); 86 | assert.strictEqual(request.form.field.gb.c.fruit, "DEEPER"); 87 | assert.strictEqual(request.form.field.gb.c.must.go, "DEEPERRRR"); 88 | 89 | // Nesting with square-bracket notation 90 | var request = { 91 | body: { 92 | field: { 93 | nest: "wow", 94 | child: "4", 95 | gb: { 96 | a: "a", 97 | b: "aaaa", 98 | c: { 99 | fruit: "deeper", 100 | must: { 101 | go: "deeperrrr" 102 | } 103 | } 104 | } 105 | } 106 | 107 | } 108 | }; 109 | form( 110 | field("field[nest]").toUpper(), 111 | field("field[child]").toUpper(), 112 | field("field[gb][a]").toUpper(), 113 | field("field[gb][b]").toUpper(), 114 | field("field[gb][c][fruit]").toUpper(), 115 | field("field[gb][c][must][go]").toUpper() 116 | )(request, {}); 117 | assert.strictEqual(request.form.field.nest, "WOW"); 118 | assert.strictEqual(request.form.field.child, "4"); 119 | assert.strictEqual(request.form.field.gb.a, "A"); 120 | assert.strictEqual(request.form.field.gb.b, "AAAA"); 121 | assert.strictEqual(request.form.field.gb.c.fruit, "DEEPER"); 122 | assert.strictEqual(request.form.field.gb.c.must.go, "DEEPERRRR"); 123 | }, 124 | 125 | "field : filter/validate combo ordering": function () { 126 | // Can arrange filter and validate procs in any order. 127 | var request = { 128 | body: { 129 | field1: " whatever ", 130 | field2: " some thing " 131 | } 132 | }; 133 | form( 134 | field("field1").trim().toUpper().maxLength(5), 135 | field("field2").minLength(12).trim() 136 | )(request, {}); 137 | assert.strictEqual(request.form.field1, "WHATEVER"); 138 | assert.strictEqual(request.form.field2, "some thing"); 139 | assert.equal(request.form.errors.length, 1); 140 | assert.equal(request.form.errors[0], "field1 is too long"); 141 | }, 142 | 143 | "field : autoTrim": function () { 144 | // Auto-trim declared fields. 145 | form.configure({ autoTrim: true }); 146 | var request = { body: { field: " whatever " } }; 147 | form(field("field"))(request, {}); 148 | assert.strictEqual(request.form.field, "whatever"); 149 | form.configure({ autoTrim: false }); 150 | }, 151 | 152 | "field : passThrough": function () { 153 | // request.form gets all values from sources. 154 | form.configure({ passThrough: true }); 155 | var request = { 156 | body: { 157 | field1: "fdsa", 158 | field2: "asdf" 159 | } 160 | }; 161 | form(field("field1"))(request, {}); 162 | assert.strictEqual(request.form.field1, "fdsa"); 163 | assert.strictEqual(request.form.field2, "asdf"); 164 | 165 | // request.form only gets declared fields. 166 | form.configure({ passThrough: false }); 167 | var request = { body: { 168 | field1: "fdsa", 169 | field2: "asdf" 170 | } }; 171 | form(field("field1"))(request, {}); 172 | assert.strictEqual(request.form.field1, "fdsa"); 173 | assert.strictEqual(typeof request.form.field2, "undefined"); 174 | }, 175 | 176 | "form : getErrors() gives full map": function() { 177 | var request = { 178 | body: { 179 | field0: "win", 180 | field1: "fail", 181 | field2: "fail", 182 | field3: "fail" 183 | } 184 | }; 185 | form( 186 | field("field0").equals("win"), 187 | field("field1").isEmail(), 188 | field("field2").isEmail().isUrl(), 189 | field("field3").isEmail().isUrl().isIP() 190 | )(request, {}); 191 | assert.equal(request.form.isValid, false); 192 | assert.equal(request.form.errors.length, 6); 193 | assert.equal(typeof request.form.getErrors().field0, "undefined"); 194 | assert.equal(request.form.getErrors().field1.length, 1); 195 | assert.equal(request.form.getErrors().field2.length, 2); 196 | assert.equal(request.form.getErrors().field3.length, 3); 197 | } 198 | 199 | } -------------------------------------------------------------------------------- /test/validate.test.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"), 2 | form = require("../index"), 3 | validate = form.validate; 4 | 5 | module.exports = { 6 | 'validate : isEmail': function() { 7 | // Skip validating empty values 8 | var request = { body: {} }; 9 | form(validate("field").isEmail())(request, {}); 10 | assert.equal(request.form.errors.length, 0); 11 | 12 | // Failure. 13 | var request = { body: { field: "fail" }}; 14 | form(validate("field").isEmail())(request, {}); 15 | assert.equal(request.form.errors.length, 1); 16 | assert.equal(request.form.errors[0], "field is not an email address"); 17 | 18 | // Failure w/ custom message. 19 | var request = { body: { field: "fail" }}; 20 | form(validate("field").isEmail("!!! %s !!!"))(request, {}); 21 | assert.equal(request.form.errors.length, 1); 22 | assert.equal(request.form.errors[0], "!!! field !!!"); 23 | 24 | // Success 25 | var request = { body: { field: "me@dandean.com" }}; 26 | form(validate("field").isEmail())(request, {}); 27 | assert.equal(request.form.errors.length, 0); 28 | }, 29 | 30 | 'validate : isUrl': function() { 31 | // Failure. 32 | var request = { body: { field: "fail" }}; 33 | form(validate("field").isUrl())(request, {}); 34 | assert.equal(request.form.errors.length, 1); 35 | assert.equal(request.form.errors[0], "field is not a URL"); 36 | 37 | // Failure w/ custom message. 38 | var request = { body: { field: "fail" }}; 39 | form(validate("field").isUrl("!!! %s !!!"))(request, {}); 40 | assert.equal(request.form.errors.length, 1); 41 | assert.equal(request.form.errors[0], "!!! field !!!"); 42 | 43 | // Success 44 | var request = { body: { field: "http://www.google.com" }}; 45 | form(validate("field").isUrl())(request, {}); 46 | assert.equal(request.form.errors.length, 0); 47 | }, 48 | 49 | 'validate : isIP': function() { 50 | // Failure. 51 | var request = { body: { field: "fail" }}; 52 | form(validate("field").isIP())(request, {}); 53 | assert.equal(request.form.errors.length, 1); 54 | assert.equal(request.form.errors[0], "field is not an IP address"); 55 | 56 | // Failure w/ custom message. 57 | var request = { body: { field: "fail" }}; 58 | form(validate("field").isIP("!!! %s !!!"))(request, {}); 59 | assert.equal(request.form.errors.length, 1); 60 | assert.equal(request.form.errors[0], "!!! field !!!"); 61 | 62 | // Success 63 | var request = { body: { field: "0.0.0.0" }}; 64 | form(validate("field").isIP())(request, {}); 65 | assert.equal(request.form.errors.length, 0); 66 | }, 67 | 68 | 'validate : isAlpha': function() { 69 | // Failure. 70 | var request = { body: { field: "123456" }}; 71 | form(validate("field").isAlpha())(request, {}); 72 | assert.equal(request.form.errors.length, 1); 73 | assert.equal(request.form.errors[0], "field contains non-letter characters"); 74 | 75 | // Failure w/ custom message. 76 | var request = { body: { field: "123456" }}; 77 | form(validate("field").isAlpha("!!! %s !!!"))(request, {}); 78 | assert.equal(request.form.errors.length, 1); 79 | assert.equal(request.form.errors[0], "!!! field !!!"); 80 | 81 | // Success 82 | var request = { body: { field: "abcde" }}; 83 | form(validate("field").isAlpha())(request, {}); 84 | assert.equal(request.form.errors.length, 0); 85 | }, 86 | 87 | 'validate : isAlphanumeric': function() { 88 | // Failure. 89 | var request = { body: { field: "------" }}; 90 | form(validate("field").isAlphanumeric())(request, {}); 91 | assert.equal(request.form.errors.length, 1); 92 | assert.equal(request.form.errors[0], "field contains non alpha-numeric characters"); 93 | 94 | // Failure w/ custom message. 95 | var request = { body: { field: "------" }}; 96 | form(validate("field").isAlphanumeric("!!! %s !!!"))(request, {}); 97 | assert.equal(request.form.errors.length, 1); 98 | assert.equal(request.form.errors[0], "!!! field !!!"); 99 | 100 | // Success 101 | var request = { body: { field: "abc123" }}; 102 | form(validate("field").isAlphanumeric())(request, {}); 103 | assert.equal(request.form.errors.length, 0); 104 | }, 105 | 106 | 'validate : isNumeric': function() { 107 | // Failure. 108 | var request = { body: { field: "------" }}; 109 | form(validate("field").isNumeric())(request, {}); 110 | assert.equal(request.form.errors.length, 1); 111 | assert.equal(request.form.errors[0], "field is not a number"); 112 | 113 | // Failure w/ custom message. 114 | var request = { body: { field: "------" }}; 115 | form(validate("field").isNumeric("!!! %s !!!"))(request, {}); 116 | assert.equal(request.form.errors.length, 1); 117 | assert.equal(request.form.errors[0], "!!! field !!!"); 118 | 119 | // Success Int 120 | var request = { body: { 121 | integer: "123456", 122 | floating: "123456.45", 123 | negative: "-123456.45", 124 | positive: "+123456.45", 125 | padded: "000045.343" 126 | }}; 127 | form( 128 | validate("integer").isNumeric(), 129 | validate("floating").isNumeric(), 130 | validate("negative").isNumeric(), 131 | validate("positive").isNumeric(), 132 | validate("padded").isNumeric() 133 | )(request, {}); 134 | assert.equal(request.form.errors.length, 0); 135 | }, 136 | 137 | 'validate : isInt': function() { 138 | // Failure. 139 | var request = { body: { field: "------" }}; 140 | form(validate("field").isInt())(request, {}); 141 | assert.equal(request.form.errors.length, 1); 142 | assert.equal(request.form.errors[0], "field is not an integer"); 143 | 144 | // Failure w/ custom message. 145 | var request = { body: { field: "------" }}; 146 | form(validate("field").isInt("!!! %s !!!"))(request, {}); 147 | assert.equal(request.form.errors.length, 1); 148 | assert.equal(request.form.errors[0], "!!! field !!!"); 149 | 150 | // Success 151 | var request = { body: { field: "50" }}; 152 | form(validate("field").isInt())(request, {}); 153 | assert.equal(request.form.errors.length, 0); 154 | }, 155 | 156 | 'validate : isLowercase': function() { 157 | // Failure. 158 | var request = { body: { field: "FAIL" }}; 159 | form(validate("field").isLowercase())(request, {}); 160 | assert.equal(request.form.errors.length, 1); 161 | assert.equal(request.form.errors[0], "field contains uppercase letters"); 162 | 163 | // Failure w/ custom message. 164 | var request = { body: { field: "FAIL" }}; 165 | form(validate("field").isInt("!!! %s !!!"))(request, {}); 166 | assert.equal(request.form.errors.length, 1); 167 | assert.equal(request.form.errors[0], "!!! field !!!"); 168 | 169 | // Success 170 | var request = { body: { field: "win" }}; 171 | form(validate("field").isLowercase())(request, {}); 172 | assert.equal(request.form.errors.length, 0); 173 | }, 174 | 175 | 'validate : isUppercase': function() { 176 | // Failure. 177 | var request = { body: { field: "fail" }}; 178 | form(validate("field").isUppercase())(request, {}); 179 | assert.equal(request.form.errors.length, 1); 180 | assert.equal(request.form.errors[0], "field contains lowercase letters"); 181 | 182 | // Failure w/ custom message. 183 | var request = { body: { field: "fail" }}; 184 | form(validate("field").isUppercase("!!! %s !!!"))(request, {}); 185 | assert.equal(request.form.errors.length, 1); 186 | assert.equal(request.form.errors[0], "!!! field !!!"); 187 | 188 | // Success 189 | var request = { body: { field: "WIN" }}; 190 | form(validate("field").isUppercase())(request, {}); 191 | assert.equal(request.form.errors.length, 0); 192 | }, 193 | 194 | 'validate : isFloat': function() { 195 | // Failure. 196 | var request = { body: { field: "5000" }}; 197 | form(validate("field").isFloat())(request, {}); 198 | assert.equal(request.form.errors.length, 1); 199 | assert.equal(request.form.errors[0], "field is not a decimal"); 200 | 201 | // Failure w/ custom message. 202 | var request = { body: { field: "5000" }}; 203 | form(validate("field").isFloat("!!! %s !!!"))(request, {}); 204 | assert.equal(request.form.errors.length, 1); 205 | assert.equal(request.form.errors[0], "!!! field !!!"); 206 | 207 | // Success 208 | var request = { body: { field: "5000.00" }}; 209 | form(validate("field").isFloat())(request, {}); 210 | assert.equal(request.form.errors.length, 0); 211 | }, 212 | 213 | 'validate : notEmpty': function() { 214 | // Failure. 215 | var request = { body: { field: " \t" }}; 216 | form(validate("field").notEmpty())(request, {}); 217 | assert.equal(request.form.errors.length, 1); 218 | assert.equal(request.form.errors[0], "field has no value or is only whitespace"); 219 | 220 | // Failure w/ custom message. 221 | var request = { body: { field: " \t" }}; 222 | form(validate("field").notEmpty("!!! %s !!!"))(request, {}); 223 | assert.equal(request.form.errors.length, 1); 224 | assert.equal(request.form.errors[0], "!!! field !!!"); 225 | 226 | // Success 227 | var request = { body: { field: "win" }}; 228 | form(validate("field").notEmpty())(request, {}); 229 | assert.equal(request.form.errors.length, 0); 230 | }, 231 | 232 | 'validate : equals': function() { 233 | // Failure. 234 | var request = { body: { field: "value" }}; 235 | form(validate("field").equals("other"))(request, {}); 236 | assert.equal(request.form.errors.length, 1); 237 | assert.equal(request.form.errors[0], "field does not equal other"); 238 | 239 | // Failure w/ custom message. 240 | var request = { body: { field: "value" }}; 241 | form(validate("field").equals("other", "!!! %s !!!"))(request, {}); 242 | assert.equal(request.form.errors.length, 1); 243 | assert.equal(request.form.errors[0], "!!! field !!!"); 244 | 245 | // Success 246 | var request = { body: { field: "value" }}; 247 | form(validate("field").equals("value"))(request, {}); 248 | assert.equal(request.form.errors.length, 0); 249 | 250 | 251 | // Failure 252 | var request = { 253 | body: { 254 | field1: "value1", 255 | field2: "value2" 256 | } 257 | }; 258 | form(validate("field1").equals("field::field2"))(request, {}); 259 | assert.equal(request.form.errors.length, 1); 260 | assert.equal(request.form.errors[0], "field1 does not equal value2"); 261 | 262 | // Success 263 | var request = { 264 | body: { 265 | field1: "value", 266 | field2: "value" 267 | } 268 | }; 269 | form(validate("field1").equals("field::field2"))(request, {}); 270 | assert.equal(request.form.errors.length, 0); 271 | 272 | // Failure with nested values 273 | var request = { 274 | body: { 275 | field1: { deep: "value1"}, 276 | field2: { deeper: "value2"} 277 | } 278 | }; 279 | form(validate("field1.deep").equals("field::field2[deeper]"))(request, {}); 280 | assert.equal(request.form.errors.length, 1); 281 | assert.equal(request.form.errors[0], "field1.deep does not equal value2"); 282 | 283 | // Success with nested values 284 | var request = { 285 | body: { 286 | field1: { deep: "value"}, 287 | field2: { deeper: "value"} 288 | } 289 | }; 290 | form(validate("field1[deep]").equals("field::field2.deeper"))(request, {}); 291 | assert.equal(request.form.errors.length, 0); 292 | }, 293 | 294 | 'validate : contains': function() { 295 | // Failure. 296 | var request = { body: { field: "value" }}; 297 | form(validate("field").contains("other"))(request, {}); 298 | assert.equal(request.form.errors.length, 1); 299 | assert.equal(request.form.errors[0], "field does not contain required characters"); 300 | 301 | // Failure w/ custom message. 302 | var request = { body: { field: "value" }}; 303 | form(validate("field").contains("other", "!!! %s !!!"))(request, {}); 304 | assert.equal(request.form.errors.length, 1); 305 | assert.equal(request.form.errors[0], "!!! field !!!"); 306 | 307 | // Success 308 | var request = { body: { field: "value" }}; 309 | form(validate("field").contains("alu"))(request, {}); 310 | assert.equal(request.form.errors.length, 0); 311 | }, 312 | 313 | 'validate : notContains': function() { 314 | // Failure. 315 | var request = { body: { field: "value" }}; 316 | form(validate("field").notContains("alu"))(request, {}); 317 | assert.equal(request.form.errors.length, 1); 318 | assert.equal(request.form.errors[0], "field contains invalid characters"); 319 | 320 | // Failure w/ custom message. 321 | var request = { body: { field: "value" }}; 322 | form(validate("field").notContains("alu", "!!! %s !!!"))(request, {}); 323 | assert.equal(request.form.errors.length, 1); 324 | assert.equal(request.form.errors[0], "!!! field !!!"); 325 | 326 | // Success 327 | var request = { body: { field: "value" }}; 328 | form(validate("field").notContains("win"))(request, {}); 329 | assert.equal(request.form.errors.length, 0); 330 | }, 331 | 332 | 'validate : regex/is': function() { 333 | // regex(/pattern/) 334 | // regex(/pattern/, "message") 335 | // regex("pattern") 336 | // regex("pattern", "modifiers") 337 | // regex("pattern", "message") 338 | // regex("pattern", "modifiers", "message") 339 | 340 | // Failure: RegExp with default args 341 | var request = { body: { field: "value" }}; 342 | form(validate("field").regex(/^\d+$/))(request, {}); 343 | assert.equal(request.form.errors.length, 1); 344 | assert.equal(request.form.errors[0], "field has invalid characters"); 345 | 346 | // Failure: RegExp with custom message. 347 | var request = { body: { field: "value" }}; 348 | form(validate("field").regex(/^\d+$/, "!!! %s !!!"))(request, {}); 349 | assert.equal(request.form.errors.length, 1); 350 | assert.equal(request.form.errors[0], "!!! field !!!"); 351 | 352 | // Failure: String with default args. 353 | var request = { body: { field: "value" }}; 354 | form(validate("field").regex("^\d+$"))(request, {}); 355 | assert.equal(request.form.errors.length, 1); 356 | assert.equal(request.form.errors[0], "field has invalid characters"); 357 | 358 | // Success: String with modifiers 359 | var request = { body: { field: "value" }}; 360 | form(validate("field").regex("^VALUE$", "i"))(request, {}); 361 | assert.equal(request.form.errors.length, 0); 362 | 363 | // Failure: String with custom message 364 | var request = { body: { field: "value" }}; 365 | form(validate("field").regex("^\d+$", "!!! %s !!!"))(request, {}); 366 | assert.equal(request.form.errors.length, 1); 367 | assert.equal(request.form.errors[0], "!!! field !!!"); 368 | 369 | // Failure: String with modifiers and custom message 370 | var request = { body: { field: "value" }}; 371 | form(validate("field").regex("^\d+$", "i", "!!! %s !!!"))(request, {}); 372 | assert.equal(request.form.errors.length, 1); 373 | assert.equal(request.form.errors[0], "!!! field !!!"); 374 | 375 | 376 | // Success 377 | var request = { body: { field: "value" }}; 378 | form(validate("field").regex(/^value$/))(request, {}); 379 | assert.equal(request.form.errors.length, 0); 380 | }, 381 | 382 | 'validate : notRegex/not': function() { 383 | // notRegex(/pattern/) 384 | // notRegex(/pattern/, "message") 385 | // notRegex("pattern") 386 | // notRegex("pattern", "modifiers") 387 | // notRegex("pattern", "message") 388 | // notRegex("pattern", "modifiers", "message") 389 | 390 | // Failure: RegExp with default args 391 | var request = { body: { field: "value" }}; 392 | form(validate("field").notRegex(/^value$/))(request, {}); 393 | assert.equal(request.form.errors.length, 1); 394 | assert.equal(request.form.errors[0], "field has invalid characters"); 395 | 396 | // Failure: RegExp with custom message. 397 | var request = { body: { field: "value" }}; 398 | form(validate("field").notRegex(/^value$/, "!!! %s !!!"))(request, {}); 399 | assert.equal(request.form.errors.length, 1); 400 | assert.equal(request.form.errors[0], "!!! field !!!"); 401 | 402 | // Failure: String with default args. 403 | var request = { body: { field: "value" }}; 404 | form(validate("field").notRegex("^value$"))(request, {}); 405 | assert.equal(request.form.errors.length, 1); 406 | assert.equal(request.form.errors[0], "field has invalid characters"); 407 | 408 | // Success: String with modifiers 409 | var request = { body: { field: "value" }}; 410 | form(validate("field").notRegex("^win$", "i"))(request, {}); 411 | assert.equal(request.form.errors.length, 0); 412 | 413 | // Failure: String with custom message 414 | var request = { body: { field: "value" }}; 415 | form(validate("field").notRegex("^value$", "!!! %s !!!"))(request, {}); 416 | assert.equal(request.form.errors.length, 1); 417 | assert.equal(request.form.errors[0], "!!! field !!!"); 418 | 419 | // Failure: String with modifiers and custom message 420 | var request = { body: { field: "value" }}; 421 | form(validate("field").notRegex("^value$", "i", "!!! %s !!!"))(request, {}); 422 | assert.equal(request.form.errors.length, 1); 423 | assert.equal(request.form.errors[0], "!!! field !!!"); 424 | 425 | // Success 426 | var request = { body: { field: "value" }}; 427 | form(validate("field").notRegex(/^win$/))(request, {}); 428 | assert.equal(request.form.errors.length, 0); 429 | }, 430 | 431 | 'validation : minLength': function() { 432 | // Failure. 433 | var request = { body: { field: "value" }}; 434 | form(validate("field").minLength(10))(request, {}); 435 | assert.equal(request.form.errors.length, 1); 436 | assert.equal(request.form.errors[0], "field is too short"); 437 | 438 | // Failure w/ custom message. 439 | var request = { body: { field: "value" }}; 440 | form(validate("field").minLength(10, "!!! %s !!!"))(request, {}); 441 | assert.equal(request.form.errors.length, 1); 442 | assert.equal(request.form.errors[0], "!!! field !!!"); 443 | 444 | // Success 445 | var request = { body: { field: "value" }}; 446 | form(validate("field").minLength(1))(request, {}); 447 | assert.equal(request.form.errors.length, 0); 448 | }, 449 | 450 | 'validation : maxLength': function() { 451 | // Failure. 452 | var request = { body: { field: "value" }}; 453 | form(validate("field").maxLength(1))(request, {}); 454 | assert.equal(request.form.errors.length, 1); 455 | assert.equal(request.form.errors[0], "field is too long"); 456 | 457 | // Failure w/ custom message. 458 | var request = { body: { field: "value" }}; 459 | form(validate("field").maxLength(1, "!!! %s !!!"))(request, {}); 460 | assert.equal(request.form.errors.length, 1); 461 | assert.equal(request.form.errors[0], "!!! field !!!"); 462 | 463 | // Success 464 | var request = { body: { field: "value" }}; 465 | form(validate("field").maxLength(5))(request, {}); 466 | assert.equal(request.form.errors.length, 0); 467 | }, 468 | 469 | 'validation : required': function() { 470 | // Failure. 471 | var request = { body: {} }; 472 | form(validate("field").required())(request, {}); 473 | assert.equal(request.form.errors.length, 1); 474 | assert.equal(request.form.errors[0], "field is required"); 475 | 476 | // Failure w/ placeholder value and custom message. 477 | var request = { body: { field: "value" }}; 478 | form(validate("field").required("value", "!!! %s !!!"))(request, {}); 479 | assert.equal(request.form.errors.length, 1); 480 | assert.equal(request.form.errors[0], "!!! field !!!"); 481 | 482 | // Success 483 | var request = { body: { field: "5000.00" }}; 484 | form(validate("field").required())(request, {}); 485 | assert.equal(request.form.errors.length, 0); 486 | 487 | // Non-required fields with no value should not trigger errors 488 | // Success 489 | var request = { body: { 490 | fieldEmpty: "", 491 | fieldUndefined: undefined, 492 | fieldNull: null 493 | }}; 494 | form( 495 | validate("fieldEmpty").is(/whatever/), 496 | validate("fieldUndefined").is(/whatever/), 497 | validate("fieldNull").is(/whatever/), 498 | validate("fieldMissing").is(/whatever/) 499 | )(request, {}); 500 | assert.equal(request.form.errors.length, 0); 501 | }, 502 | 503 | 'validation : custom': function() { 504 | var request; 505 | 506 | // Failure. 507 | request = { body: { field: "value" }}; 508 | form(validate("field").custom(function(value) { 509 | throw new Error(); 510 | }))(request, {}); 511 | assert.equal(request.form.errors.length, 1); 512 | assert.equal(request.form.errors[0], "field is invalid"); 513 | 514 | // Failure w/ custom message. 515 | request = { body: { field: "value" }}; 516 | form(validate("field").custom(function(value) { 517 | throw new Error(); 518 | }, "!!! %s !!!"))(request, {}); 519 | assert.equal(request.form.errors.length, 1); 520 | assert.equal(request.form.errors[0], "!!! field !!!"); 521 | 522 | // Failure w/ custom message from internal error. 523 | request = { body: { field: "value" }}; 524 | form(validate("field").custom(function(value) { 525 | throw new Error("Radical %s"); 526 | }))(request, {}); 527 | assert.equal(request.form.errors.length, 1); 528 | assert.equal(request.form.errors[0], "Radical field"); 529 | 530 | // Success 531 | request = { body: { field: "value" }}; 532 | form(validate("field").custom(function(value) {}))(request, {}); 533 | assert.equal(request.form.errors.length, 0); 534 | 535 | // Pass form data as 2nd argument to custom validators 536 | request = { body: { field1: "value1", field2: "value2" }}; 537 | form(validate("field1").custom(function(value, formData) { 538 | assert.equal("value1", value); 539 | assert.ok(formData); 540 | assert.equal("value1", formData.field1); 541 | assert.equal("value2", formData.field2); 542 | throw new Error("This is a custom error thrown for %s."); 543 | }))(request, {}); 544 | assert.equal(request.form.errors.length, 1); 545 | }, 546 | 547 | "validation : request.form property-pollution": function() { 548 | var request = { body: { }}; 549 | form()(request, {}); 550 | assert.equal(request.form.errors.length, 0); 551 | assert.equal('{}', JSON.stringify(request.form)); 552 | }, 553 | 554 | "validation : complex properties": function() { 555 | var request = { body: { field: { inner: "value", even: { more: { inner: "value" }}}}}; 556 | form( 557 | validate("field[inner]").required().equals("value"), 558 | validate("field[inner]").required().equals("fail"), 559 | validate("field[even][more][inner]").required().equals("value"), 560 | validate("field[even][more][inner]").required().equals("fail") 561 | )(request, {}); 562 | assert.equal(request.form.errors.length, 2); 563 | } 564 | }; 565 | --------------------------------------------------------------------------------