UNPKG

sanidate

Version:

Validation-sanitizing functionality for web frameworks, browsers, and you-name-it

1,090 lines (1,043 loc) 39.6 kB
/** * # Sanidate * * Validation-sanitizing functionality for web frameworks, browsers, and * you-name-it. * * @author Monwara LLC / Branko Vukelic <branko@brankovukeluc.com> * @version 0.1.0 * @license MIT */ void(0); // tells uglfy to not keep docs below /** * ## Overview * * Sanidate (SANItize + valiDATE) is a JavaScript library for validating and * sanitizing user-supplied data. It uses a developer-defined constraint schema * and converts input data into valid and properly formatted data. It can be * used both on server- and client-side, and provides middlewares for * [Express.js](http://expressjs.com/), and * [FragRouter](https://github.com/foxbunny/FragRouter), as well as a jQuery * plugin. * * Sanidate grew out of author's frustration with existing validation * frameworks which tend to produce a lot of cruft, and/or create unreasonably * hard-to-read, verbose code. Data validation and sanitizing is a very common * task (virtually _all_ user input has to be validated and/or sanitized at * some point), repetitive, and outright boring. On top of it, it's not even * the _most important_ part of the business logic, but merely an overture. * * Sanidate tries to reduce validation and sanitizing to what it's supposed to * be, merely a checklist for what the data should look like before it can be * consumed by business logic code. It also tries to reduce validation and * sanitizing into a single pass (called 'sanidation'), so you don't have to do * two mentally very close tasks tasks in two separate passes. Finally, it * systematizes data into a convenient package, so you can access it with ease, * rather than hunt for it in multiple different places. * * The core of Sanidate is its constraints system. Each constraint is a * function which does two things: * * + converts data into intended data _type_ * + checks the validity of the data * * These constraints are named, and a sanidation schema can be contructed using * the names, instead of references to functions. This makes for concise * representation of the sanidation schema (more on that in the following * section). * * Unlike some of the validation frameworks for JavaScript, Sanidate is fully * asynchronous, which means it supports asynchronous operations like database * lookups in its constraint functions. In fact, it ships with two * Mongoose-specific constraints all good to go. * * ## Basic usage * * To sanidate some data, you need two things: * * + The actual data as an object (key-value parirs) * + The sanidation schema (more on that in next section) * * When you have both, simply call the `sanidate.check()` method: * * sanidate.check(data, schema, function(err, data) { * if (err) { return console.log('Error!'); } * // Do something useful with the data, like: * console.log(data); * }); * * ## Sanidation schema * * Sanidation schema is an object that describes your desired output. Since * input is almost always string, Sanidate not only checks the validity of the * strings, but also converts them into desired type and/or format. The * conversion and validation for a single constraint are both controlled by a * single constraint function. * * Let's take a look at a simple sanidation schema and then discuss its * structure in more detail: * * var schema = { * name: 'required', * email: 'email', * age: 'integer', * username: 'required', * profilePicture: [ * ['custom', checkStorage] * ] * }; * * You will first notice that, for most part, schema is simply a key-value * pair, where keys are parameter names, and values are strings or arrays. * * Strings values are very simple: they are names of built-in constraints * (e.g., 'required', 'email', 'numeric'...). * * For specifying multiple constraints, or specifying constraints with * parameters, arrays must be used. Each member of the constraint array is a * single constraint, and it can be either a string or an array. Strings are * the same as string values above: just names of built-in constraints. * Constraint represented by an array has the name of the built-in constraint * as it's first member, and other members are arguments passed to the * constraint function. * * In our example, `profilePicture` parameter has a single constraint called * 'custom', with a single argument, which is a reference to a function caleld * `checkStorage` (which, you can imagine, checks the hard drive for profile * image file and fails validation if one is not found). * * The built-in constraints, such as 'required' or 'email', are all stored in * `sanidate.funcs` object. They are looked up internally when validation takes * place, but are exposed for your convenience, if you ever need to customize * them, or add new constraints yourself. * * One thing to note about the constraints such as 'numeric' is that they will * actually convert the input data into an appropriate type (e.g., 'numeric' * converts to a float, 'integer' converts to an integer, and 'date' converts * to `Date` object). * * Another important note is that all constraints require a value to be * present. Currently, there is no support for optional constraints that are * applied only when data is present. If you wish to apply constraints only if * a value is present, use the 'optional' constraint as the first constraint, * followed by other constraints. * * ## Constraint chaining * * If you use more than one constraint on a parameter, you should keep in mind * that each constraint transforms data before passing to the next one. For * example, if you put a 'match' constraint _after_ the 'date' constraint, this * will yield unexpected results, because the value will have been transformed * into a `Date` object before reaching the 'match' constraint. * * Think of constraint arrays as layers of filters one on top of the other on * which you drip the data. If data is prevented to pass any of the layers, * subsequent layers will not even see it. Any layer always sees data as * filtered by previous layers. This is also important to remember when * handling validation errors, because, for a set of chained constarints, error * will only be registered for the first constraint that reports validations * failure. * * The built-in constraints are constructed in such a way that you usually only * need one of them at a time, so chaining should generally not be required. * For example, 'max' constraint may also serve as either 'numeric' or * 'integer' constraint becuase it will fail if value is not a valid number. * * ## Built-in constraints * * Sanidate ships with a handful of (arguably) very useful constraints. These * are (with arguments in square brackets): * * + required: Simply fails if value is not supplied * + optional: [def] If no value is supplied, return the default value * (`def`), and interrupt execution of further constraints; must be the * first constraint if used with other constraints * + optionalIfPresent: [params, def] Makes parameter optional if parameters * in `params` array are present, otherwise requires it * + match: [pattern] Fails if value does not match the regexp `pattern` * + enum: [allowed] Fails if value does not appear in `allowed` array of * strings * + numeric: Forces conversion to float, and fails when conversion fails * + integer: Forces conversion to integer and fails when conversion fails * + max: [x, integer, equality] Forces conversion to float or integer (if * `integer` flag is set to `true`), and, depending on equality boolean * falg, fails if greater than or greater _and_ equal to `x` * + min: [x, integer, equality] Same as `max`, but fails if less than / less * than or equal to`x` * + date: [resetTime] Forces conversion to Date, and fails if conversion * fails (does not work with any particular date format, so if date * formatting is required, insert the `match` constraint before date * constraint) * + email: Fails if value is not an email * + zip: Fails if value is not a 5-digit number (such as US zip code) * + phone: [digitsOnly] Fails if value does not _contain_ 10 digits * (disregaring any other non-numeric characters), and returns either only * the digits (if `digitsOnly` flag is set to `true`), or formatted phone * number (currently only supports US phone format) * + isTrue: Returns `true` if value is 'true', 'yes', 'on', or '1', otherwise * returns `false`, and never fails * + isNotTrue: Returns `false` if value is 'false', 'no', 'off', or '0', * otherwise returns `true`, and never fails * + strictBoolean: [optional, def] Requires parameter to be strictly boolean, * and fails if it's not, or always passes if `optional` flag is set. * + isDocument: [Model, key] Looks up Mongoose model using either supplied * optional `key`, or parameter name as key name, and fails if no documents * are found; value is converted to returned document * + isNotDocument: [Model, key] Same as 'isDocument', but fails if document * _is_ found, and returns original value on success. * + custom: [func] Uses the `func` function as constraint * + derive: [paramName, func] Uses parameter `paramName` from original * user-supplied data, and applies `func` validation function to its value * * Note that you _can_ use multiple 'custom' constraints for any user-supplied * data. * * ## Sanidation errors * * Sanidation errors are returned as first argument to the callback you pass to * `sanidate.check()`. If there are no errors, you will receive `null` instead. * * If there are any errors, the error object will have two keys. * * The `count` represents an integer count of errors. This number can never be * higher than the number of parameters that were sanidated. * * The `errors` key will contain a object mapping between parameter names and * constraint names. Constraint names in the mapping represent the names of * constraints for which the parameter values failed validation. * * ## Writing custom constraints * * It is possible to write custom constraint functions and use them with the * 'custom' constraint. * * Let's take a look at a simple constraint function that checks the age of a * date field, and makes sure it is not older than 20 days: * * function minAge(v, next) { * var now = Date.now(); * v = new Date(v); * var maxDifference = 20 * 24 * 60 * 60 * 1000; // 20 days in ms * var difference = now - v.getTime(); * next(null, difference < maxDifference ? v : null, 'minAge'); * } * * The function takes two arguments. First argument, `v`, is the current value * of the data, as sanidated by all previous constraints. The second argument * is a callback function which allows Sanidate to continue applying * other constraints. * * What goes on inside the function is pretty self-evident, so we won't go into * details. The important bit is the data we pass on to the callback. Callback * takes three arguments. * * The first argument is an error object. It is currently not important, as it * makes little difference whether you pass an error object. Presence of an * error will be treated as failed validation. This behavior _will_ change in * future, so it's best to always pass the error object if there has been a * non-validation-related error (such as filesystem or database failure). In * later versions, Sanidate will be able to somehow tell you that there has * been an unexpected condition. * * The second argument is sanidated value. It's important to note that only * `null` value is considered a failure. Falsy values like 0, empty string, and * even undefined, are _not_ considered failure conditions. So, if you want to * let Sanidate know that validation has failed, you _must_ pass a null value * as sanidated value. * * Last argument is the constraint name. Because there can be multiple 'custom' * constraints, Sanidate allows constraint functions to return a different * constraint name in the callback. As with all other arguments, `null` is, * again, the special value. If you pass `null` as constraint name, any further * constraint processing will be abandoned, and the value passed to the * callback will be used without questions. That is, for example, what * 'optional' does. It passes on any value that user has provided, and passes * `null` as constraint name to foce Sanidate to use any provided value * (including `null`). * * Custom constraint functions have access to details about the parameter that * is being processed (like the parameter name), the original parameter value, * and the original values of all other parameters that are being sanidated. * This data is stored in `this` as properties: * * + `this.paramName`: parameter name * + `this.originalValue`: original (raw) value of the parameter * + `this.originalData`: all data being sanidated as name-value pairs * * Although the 'derive' constraint can be used effectively for the same * purpose, we will write our own 'custom' constraint that checks email * confirmation, just to demonstrate usage of `originalData` property. * * function emailConfirmation(v, next) { * next(null, v === this.originalData['email'] ? v : null, * 'emailConfirmation'); * } * * We assume that the email was first entered in a field named `email`, and * then confirmed in a field called `emailconfirm`: * * var schema = { * email: 'email', * emailconfirm: [['custom', emailConfrimation]] * }; * * To demonstrate the difference between 'custom' constraint and 'derive' * constraint in this scenario, let's rewrite the above to use 'derive': * * function emailConfirmation(v, o) { * return v === o ? v : null; * } * * var schema = { * email: 'email', * emailconfirm: [['derive', 'email', emailConfrimation]] * }; * * Note that 'emailconfirm' paramter does not have an 'email' constraint. We * don't need to add that, since we know that if 'email' constraint on 'email' * field failed, it must also fail for 'emailconfirm' field if it is identical * to 'email' field. * * Apart from writing function for use with 'custom' constraint, you can also * add your constraint as a named constraint to make it act as one of the * built-in constraints. To do that, siply add your constraint to * `sanidate.funcs` object: * * sanidate.funcs.minAge = function(maxDays) { * return function minAge(v, next) { * var now = Date.now(); * v = new Date(v); * var maxDifference = maxDays * 24 * 60 * 60 * 1000; // 20 days in ms * var difference = now - v.getTime(); * next(null, difference < maxDifference ? v : null, 'minAge'); * }; * } * * As you can see, a built-in constraint is a function that returns functions. * The outer function is called a setup function, which prepares the validation * function. The outer function has access to a `paramObject` (via `this`), * which contains more information * * ## Generic JavaScript example * * var data = { * email: 'test@example.com', * number: '11' * }; * * var schema = { * name: 'required', * email: 'email', * number: 'integer' * }; * * sanidate.check(data, schema, function(err, data) { * if (err) { * console.log('There were ' + err.count + ' errors:', err.errors); * } * // data is: * // {email: 'test@example.com', number: 11} * }); * * ## Express.js example * * var schema = { * name: 'required', * email: 'email', * number: 'integer' * }; * * // Set up middleware using `sanidate.express()` call * app.post('/user', sanidate.express(schema), function(req, res) { * if (req.dataError) { return req.send('Error', 400); } * * // Sanidated data is now available as req.data * * req.send('Success!', 200); * }); * * ## FragRouter example * * frag.addMiddleware(sanidate.frag); * * function myHandler() { * var schema = { * name: 'required', * email: 'email', * number: 'integer' * }; * * // get `data` from somewhere * * this.sanidate(data, schema) * } * * ## jQuery example * * Although Sanidate _is_ an AMD module, it doesn't list jQuery as its * dependency, since it doesn't actually _need_ it. * * If you want to use Sanidate's jQuery plugin, you need to make sure Sanidate * is loaded _after_ jQuery, or configure your AMD loader to load jQuery as * Sanidate's dependency. * * Alternatively, you can run this line after jQuery has been loaded: * * sanidate.jQuery($); * * After you've successfully activated the plugin, you can use it like this: * * var schema = { * name: 'required', * email: 'email', * number: 'integer' * }; * * $('#myForm').sanidate(schema, function(err, data) { * // `err` and `data` are the usual objects * }); * */ (function(root, factory) { if (typeof define === 'function' && define.amd) { define([], factory); } else if (typeof module === 'object' && module.exports) { module.exports = factory(); } else { root.sanidate = factory(); } })(this, function() { var sanidate = {}; var emailRe = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; var phoneRe = /^\(?\d{3}\)? ?\d{3}-?\d{4}$/; function extractDigits(s) { var r = /\d+/g; var ss = ''; while (match = r.exec(s)) { ss += match[0]; } return ss; } function formatPhone(s) { s = extractDigits(s); return '(' + s.slice(0, 3) + ') ' + s.slice(3, 6) + '-' + s.slice(6); } /** * ## sanidate.funcs * * This object contains all of the built-in constraints that can be used by * name in the sanidation schema. Each property is a function which may take * one or more arguments, and which returns a sanidation function. * * Sanidation functions take two arguments, of which first is the value as * sanidized by any previous constraints, and second is a callback which * chains to next sanidizer or finishes the chain. The sanidation function * works more or less the same way as custom functions (see 'Writing custom * constraints' section for more details). * * You can customize any of the built-in constraints by overloading the * function assigned to its key in this object. You can also add new built-in * constraints by adding new keys. */ sanidate.funcs = { /** * ### sanidate.funcs.required() * * Makes value required, and won't pass if there is no value. */ 'required': function() { return function(v, next) { next(null, v ? v : null, 'required'); }; }, /** * ### sanidate.funcs.enum(allowed) * * Fails if value does not appear in `allowed` array. * * @param {Array} allowed Array of allowed strings */ 'enum': function(allowed) { return function(v, next) { next(null, allowed.indexOf(v) > -1 ? v : null, 'enum'); } }, /** * ### sanidate.funcs.match(pattern) * * Makes value match a pattern. * * @param {RegExp} pattern Regexp pattern to match */ 'match': function(pattern) { return function(v, next) { next(null, pattern.exec(v) ? v : null, 'match'); }; }, /** * ### sanidate.funcs.numeric() * * Makes value numeric. */ 'numeric': function() { return function(v, next) { var f = parseFloat(v); next(null, !isNaN(f) ? f : null, 'numeric'); }; }, /** * ### sanidate.funcs.integer() * * Makes value integer. */ 'integer': function() { return function(v, next) { var i = parseInt(v, 10); next(null, !isNaN(i) ? i : null, 'integer'); }; }, /** * ### sanidate.funcs.min(x, integer, equality) * * Makes value be greater than or equal to `x`. If `integer` is `true`, it * will only do integer comparison. * * @param {Number} x The minimum value allowed * @param {Boolean} integer Whether to do integer comparison (default: * false) * @param {Boolean} equality Whether to accept equality (default: false) */ 'min': function(x, integer, equality) { return function(v, next) { var i = integer ? parseInt(v, 10) : parseFloat(v); next(null, (!isNaN(i) && (equality ? i >= x : i > x)) ? i : null, 'min'); }; }, /** * ### sanidate.funcs.max(x, integer, equality) * * Makes value be less than or equal to `x`. If `integer` is `true`, it * will only do integer comparison. * * @param {Number} x The maximum value allowed * @param {Boolean} integer Whether to do integer comparison (default: * false) * @param {Boolean} equality Whether to accept equality (default: false) */ 'max': function(x, integer, equality) { return function(v, next) { var i = integer ? parseInt(v, 10) : parseFloat(v); next(null, (!isNaN(i) && (equality ? i <= x : i < x)) ? i : null, 'min'); }; }, /** * ### sanidate.funcs.date(resetTime) * * Makes value a date. * * @param {Boolean} resetTime Reset time to 0 if specified */ 'date': function(resetTime) { return function(v, next) { var d = new Date(v); var valid = d.toString() !== 'Invalid Date'; if (resetTime && valid) { d = new Date(d.getFullYear(), d.getMonth(), d.getDate()); } next(null, valid ? d : null, 'date'); }; }, /** * ### sanidate.funcs.email() * * Makes value match a canonical email format. */ 'email': function() { return function(v, next) { next(null, emailRe.exec(v) ? v : null, 'email'); }; }, /** * ### sanidate.funcs.zip() * * Makes vlaue match 5-digit number. It returns the original value on * match, not converted to numbers. */ 'zip': function() { return function(v, next) { next(null, /^\d{5}$/.exec(v) ? v : null, 'zip'); }; }, /** * ### sanidate.funcs.isTrue() * * Makes value match any of the following: * + true * + yes * + on * + 1 * * If the value matches, `true` is returned, otherwise `false` is returned. * * This validator never fails. */ 'isTrue': function() { return function(v, next) { next(null, 'true yes 1 on'.split(' ').indexOf(v) > -1, 'isTrue'); }; }, /** * ### sanidate.funcs.isNotFalse() * * Makes value _not_ match any of the following: * + false * + no * + off * + 0 * * If the value matches any of the above, `false` is returned. Otherwise, * `true` is returned. * * This validator never fails. */ 'isNotFalse': function() { return function(v, next) { next(null, 'false no 0 off'.split(' ').indexOf(v) < 0, 'isFalse'); }; }, /** * ### sanidate.func.strictBoolean(optional, def) * * Makes value match any of the strict boolean values. Fails if `optional` * flag is not `true`, otherwise stops further sanidation and returns the * original value or `def` is supplied, and original value is absent. * * If `def` is a function, it will be executed with no arguments, and its * return value will be used as the default value. Function is always * executed after it is determined that default value is needed. * * @param {Boolean} optional Whether this parameter is optional (default: * false) * @param {Any} def Default value when value is missing */ 'strictBoolean': function(optional, def) { return function(v, next) { var isBool = 'true on yes 1 false off no 0'.split(' ').indexOf(v) > -1; var isTrue = 'true on yes 1'.split(' ').indexOf(v) > -1; var defVal = v || (typeof def === 'function' ? def() : def); if (optional) { return next(null, isBool ? isTrue : defVal, defVal ? 'strictBoolean' : null); } next(null, isBool ? isTrue : null, 'strictBoolean'); } }, /** * ### sanidate.funcs.phone(digitsOnly) * * Tests if 10 digits are contained in the value, and returns only the * digits as string (`digitsOnly === true`), or a formatted US phone number * (`digitsOnly === false`). * * @param {Boolean} digitsOnly Optional flag to ask for digits without * formatting. */ 'phone': function(digitsOnly) { return function(v, next) { next(null, phoneRe.exec(v) ? (digitsOnly ? extractDigits(v) : formatPhone(v)) : null, 'phone'); }; }, /** * ### sanidate.funcs.isDocument(Model, [key]) * * Tests if a document that has the key-value pair matching the value of * the parameter exists in a Mongoose/MongoDB database. * * The validator only succeeds if there is a match, and returns the * document as new value. * * Key name will default to parameter name. * * @param {mongoose.Model} Model A Mongoose model object * @param {String} key A database key to use for lookup (optional) */ 'isDocument': function(Model, key) { key = key || this.name; return function(v, next) { if (v == null) { return next(null, null, 'isDocument'); } var criteria = {}; criteria[key] = v; Model.findOne(criteria, function(err, doc) { if (err) { return next(err, null, 'isDocument'); } next(null, doc || null, 'isDocument'); }); }; }, /** * ### sanidate.funcs.isNotDocument(Model, [key]) * * Tests if a document that has the key-value part matching the value of * the parameter _does not_ exist in a Mongoose/MongoDB database. * * The validator fails if there is such a document, otherwise it returns * the value of the parameter intact. * * Key name will default to parameter name. * * @param {mongoose.Model} Model A Mongoose model object * @param {String} key A database key to use for lookup (optional) */ 'isNotDocument': function(Model, key) { key = key || this.name; return function(v, next) { if (v == null) { return next(null, null, 'isNotDocument'); } var criteria = {}; criteria[key] = v; Model.findOne(criteria, function(err, doc) { if (err) { return next(err, null, 'isNotDocument'); } next(null, doc ? null : v, 'isNotDocument'); }); }; }, /** * ### sanidate.funcs.custom(func) * * Run a custom validation function. The custom function must have the same * signature as normal validation functions, and must call the next * callback. * * @param {Function} func Custom validation function */ 'custom': function(func) { var paramObject = this; return function(v, next) { func.call(paramObject, v, next); }; }, /** * ### sanidate.funcs.optional(def) * * Interrupts sanidation immediately if no value is found, but does not * fail validation. * * If `def` is a function, it will be executed with no arguments, and its * return value will be used as the default value. Function is always * executed after it is determined that default value is needed. * * @param {Any} def Optional default value that is returned in case no * value is found. */ 'optional': function(def) { return function(v, next) { next(null, v ? v : (typeof def === 'function' ? def() : def), v ? 'optional' : null); }; }, /** * ### sanidate.funcs.optionalIfPreset(params, def) * * Interrupts sanidation immediately if no value is found, *and* values are * found for parameters specified in `params` array. *All* parameters * listed in `params` array *must* be present to make this field optional. * * If `params` is a single string, it will be treated as an array with * single member. * * Default value of `def` can be used. * * If `def` is a function, it will be executed with no arguments, and its * return value will be used as the default value. Function is always * executed after it is determined that default value is needed. * * @param {Array} params Array of parameter names that make this parameter * optional. * @param {Any} def Value or function to use as default. */ 'optionalIfPresent': function(params, def) { if (typeof params === 'string') { params = [params]; } var originalData = this.originalData; var check = params.every(function(key) { return originalData[key] != null && originalData[key].length; }); return function(v, next) { var newVal = v || (typeof def === 'function' ? def() : def); if (check) { // Behave as if value is optional next(null, v ? v : newVal, v ? 'optionalIfPresent' : null); } else { // Value is required because (some of) other params are not present next(null, v ? v : null, 'optionalIfPresent') } } }, /** * ### sanidate.derive(paramName, func); * * Uses data from another parameter to either derive its own value, or * validate its value. * * The `func` function accepts two arguments. First argument is the * sanidated value of the current parameter. Second paramter is the * original (unsanidated) value of the other parameter. The func must * return a value, which is then used for validation. To fail validation, * you must return `null`. All other values (including `undefined`) will be * considered valid. * * Note that 'derive' only operates on the _original_ value of the other * parameter, and not the sanidated value. * * @param {String} paramName Name of the other parameter to use for * derivation * @param {Function} func Validation function */ 'derive': function(paramName, func) { var o = this.originalData[paramName]; return function(v, next) { next(null, func(v, o), 'derive'); }; } }; /** * ## sanidate.checkParam(paramName, value, constraints, data, cb) * * Runs constraints on a parameter with `paramName` name, `value` value, and * executes a `cb` callback when finished. The callback should expect three * arguments: * * + error object (null if there were no errors) * + final value after running sanidators over the original value * + constraint name if there was a failure of no value was returned * * The constraint name is the name of the sanidator that was run last in case * there is no value returned from it, or it threw an error. * * This method, although exposed, is a private method, and you should not * rely on the stability of its API. * * @param {String} paramName Original parameter name * @param {String} value Value to test against constraints * @param {Array} constraints Array of constraints definitions * @param {Object} data Original data * @param {Function} cb Callback function * @private */ sanidate.checkParam = function(paramName, value, constraints, data, cb) { var paramObject = { name: paramName, originalValue: value, originalData: data }; function runValidators(funcs, val, final) { if (!funcs.length) { return final(null, val); } funcs[0](val, function(err, val, constraintName, interrupt) { if (err) { return final(err, null, constraintName); } if (constraintName === null) { return final(null, val); } if (val === null) { return final(null, null, constraintName); } runValidators(funcs.slice(1), val, final); }); } if (typeof constraints === 'string') { constraints = [constraints]; } constraints = constraints.map(function(constraint) { var cFunc; var cParams; if (typeof constraint === 'string') { cFunc = sanidate.funcs[constraint]; cParams = []; } else { cFunc = sanidate.funcs[constraint[0]]; cParams = constraint.slice(1); } if (typeof cFunc !== 'function') { throw new Error('Constraint ' + constraint + ' is not a function ' + 'for param ' + parameterName); } return cFunc.apply(paramObject, cParams); }); runValidators(constraints, value, cb); }; /** * ## sanidate.preparationRecipes * * Object containing recipes used in `sanidate.prepareSchema`. Exposed to * allow customization. Object maps constraint names with preparation * functions which take two arguments: * + constraint (array whose first member is the constraint name, followed * by parameters) * + module which is used in preparation (see `sanidate.prepareSchema` * documentation) * * Function simply performs replacement, and does not need to explicitly * return a value. See built-in recipes for examples. */ sanidate.preparationRecipes = { optional: function(c, mod) { c[1] = mod[c[1]] || c[1]; }, match: function(c, mod) { c[1] = mod[c[1]]; }, isDocument: function(c, mod) { c[1] = mod[c[1]]; }, isNotDocument: function(c, mod) { c[1] = mod[c[1]]; }, custom: function(c, mod) { c[1] = mod[c[1]]; }, derive: function(c, mod) { c[2] = mod[c[2]]; } }; /** * ## sanidate.prepareSchema(schema, module) * * Takes a schema as string or object, and replaces any string function * references by matching methods from `module` object. * * This method is meant to be used in scenarios where schema is stored as * JSON (and passed either parsed or unparsed), and therefore function * and object references could not be made part of the schema. * * In such cases, any custom functions and objects have to be made available * as a separate module or object whose properties (methods) represent the * missing functions or model objects. Instead of using function references, * we can use strings representing property names, and this method replaces * them with actual function references or model objects. * * This prepares the schema for actual usage with `schema.check` method. * * @param {String/Object} schema Schema to be prepared * @param {Object} module Module with actual functions or model objects * @return {Object} Parsed schema */ sanidate.prepareSchema = function(schema, module) { var recipeKeys = Object.keys(sanidate.preparationRecipes); if (typeof schema === 'string') { schema = JSON.parse(schema); } Object.keys(schema).forEach(function(param) { if (Array.isArray(schema[param])) { schema[param].forEach(function(constraint) { if (Array.isArray(constraint) && recipeKeys.indexOf(constraint[0]) > -1) { sanidate.preparationRecipes[constraint[0]](constraint, module); } }); } }); return schema; }; /** * ## sanidate.check(data, schema, [excludeEmpty], cb) * * Sanidates the data from `data` object using `schema` validation schema, * and calls the `cb` callback. * * @param {Object} data Key-value pair of request parameters to validate * @param {Object} schema Sanidation schema * @param {Boolean} excludeEmpty Optional flag to exclude keys for empty * values * @param {Function} cb Callback function */ sanidate.check = function(data, schema, excludeEmpty, cb) { if (typeof excludeEmpty === 'function') { cb = excludeEmpty; excludeEmpty = false; } var cleanedData = {}; var errors = { count: 0, errors: {} }; var completed = Object.keys(schema).length; Object.keys(schema).forEach(function(paramName) { sanidate.checkParam( paramName, data[paramName], schema[paramName], data, function(err, val, constraintName) { if (err || val === null) { errors.count += 1; errors.errors[paramName] = constraintName; } else { if (!excludeEmpty || (typeof val !== 'undefined' && val !== null)) { cleanedData[paramName] = val; } } completed--; if (!completed) { if (!errors.count) { errors = null; } cb(errors, cleanedData); } }); }); }; /** * ## sanidate.express(schema, excludeEmpty) * * Express.js middleware for automatic sanidation of data prior to request * handling. * * The sanidated data will be stored in `req.data`, and can be accessed * normally, as you would with `req.query` or `req.post`. * * Any validation errors that occur will cause the `dataErrors` property to * appear in `req` object, so you can check for presence of this property * when testing for possible errors. * * @param {Object} schema Sanidation schema * @param {Boolean} excludeEmpty Optional flag to exclude keys for empty */ sanidate.express = function(schema, excludeEmpty) { excludeEmpty = typeof excludeEmpty === 'boolean' ? excludeEmpty : false; return function(req, res, next) { req.sanidateFuncs = sanidate.funcs; req.sanidate = sanidate.check; if (schema) { var data = {}; Object.keys(schema).forEach(function(param) { data[param] = req.param(param); }); sanidate.check(data, schema, excludeEmpty, function(err, data) { req.data = data; err && err.count && (req.dataErrors = err); next(); }); } else { next(); } }; }; /** * ## sanidate.frag(schema) * * FragRouter middleware. Currently, passing schema does absolutely nothing. * * Using this middleware, you can call `sanidate.check` as `this.sanidate()`. * The method signature is the same as for `sanidate.check`. * * @param {Object} schema Sanidation schema (does nothing) */ sanidate.frag = function(schema) { return function(next) { this.sanidateFuncs = sanidate.funcs; this.sanidate = sanidate.check; }; }; sanidate.jQuery = function(jQuery) { jQuery.fn.sanidate = function(schema, cb) { var form = jQuery(this); var data = {}; Object.keys(schema).forEach(function(param) { data[param] = form.find(':input[name=' + param + ']').val(); }); sanidate.check(data, schema, cb); }; }; // Attach the jQuery plugin automatically if jQuery seems present if (typeof jQuery === 'function' && jQuery.fn) { sanidate.jQuery(jQuery); } return sanidate; });