UNPKG

express-form

Version:

Form validation and data filtering for Express

369 lines (311 loc) 10.4 kB
var validator = require("validator") , FilterPrototype = validator.Filter.prototype , ValidatorPrototype = validator.Validator.prototype , externalFilter = new validator.Filter() , externalValidator = new validator.Validator() , object = require("object-additions").object , utils = require("./utils"); function Field(property, label) { var stack = [] , isArray = false , fieldLabel = label || property; this.name = property; this.__required = false; this.add = function(func) { stack.push(func); return this; }; this.array = function() { isArray = true; return this; }; this.run = function (source, form, options) { var self = this , errors = [] , value = utils.getProp(property, form) || utils.getProp(property, source); if (options.autoTrim) { stack.unshift(function (value) { if (object.isString(value)) { return FilterPrototype.trim.apply(externalFilter.sanitize(value)); } return value; }); } function runStack(foo) { stack.forEach(function (proc) { var result = proc(foo, source); // Pass source for "equals" proc. if (result.valid) return; if (result.error) { // If this field is not required and it doesn't have a value, ignore error. if (!utils.hasValue(value) && !self.__required) return; return errors.push(result.error.replace("%s", fieldLabel)); } foo = result; }); return foo; } if (isArray) { if (!utils.hasValue(value)) value = []; if (!Array.isArray(value)) value = [value]; value = value.map(runStack); } else { if (Array.isArray(value)) value = value[0]; value = runStack(value); } utils.setProp(property, form, value); if (errors.length) return errors; }; } // ARRAY METHODS Field.prototype.array = function () { return this.array(); }; Field.prototype.arrLength = function (from, to) { return this.add(function (arr) { if (value.length < from) { return { error: message || e.message || "%s is too short" }; } if (value.length > to) { return { error: message || e.message || "%s is too long" }; } return { valid: true }; }); } // HYBRID METHODS Field.prototype.custom = function(func, message) { return this.add(function (value, source) { try { var result = func(value, source); } catch (e) { return { error: message || e.message || "%s is invalid" }; } // Functions that return values are filters. if (result != null) return result; return { valid: true }; }); }; // FILTER METHODS Object.keys(FilterPrototype).forEach(function (name) { if (name.match(/^ifNull$/)) return; Field.prototype[name] = function () { var args = arguments; return this.add(function (value) { var a = FilterPrototype[name].apply(externalFilter.sanitize(value), args); return a; }); }; }); Field.prototype.ifNull = function (replacement) { return this.add(function (value) { if (object.isUndefined(value) || null === value || '' === value) { return replacement; } return value; }); }; Field.prototype.toUpper = Field.prototype.toUpperCase = function () { return this.add(function (value) { return value.toString().toUpperCase(); }); }; Field.prototype.toLower = Field.prototype.toLowerCase = function () { return this.add(function (value) { return value.toString().toLowerCase(); }); }; Field.prototype.truncate = function (length) { return this.add(function (value) { value = value.toString(); if (value.length <= length) { return value; } if (length <= 3) return "..."; if (value.length > length - 3) { return value.substr(0,length - 3) + "..."; } return value; }); }; Field.prototype.customFilter = function (func) { return this.add(func); }; // VALIDATE METHODS var MESSAGES = { isDate: "%s is not a date", isEmail: "%s is not an email address", isUrl: "%s is not a URL", isIP: "%s is not an IP address", isAlpha: "%s contains non-letter characters", isAlphanumeric: "%s contains non alpha-numeric characters", isNumeric: "%s is not numeric", isLowercase: "%s contains uppercase letters", isUppercase: "%s contains lowercase letters", isInt: "%s is not an integer", notEmpty: "%s has no value or is only whitespace" }; Object.keys(ValidatorPrototype).forEach(function (name) { if (name.match(/^(contains|notContains|equals|check|validate|assert|error|len|isNumeric|isDecimal|isFloat|regex|notRegex|is|not|notNull|isNull)$/)) { return; } Field.prototype[name] = function (message) { var args = arguments; message = message || MESSAGES[name]; return this.add(function(value) { try { ValidatorPrototype[name].apply(externalValidator.check(value, message), args); } catch (e) { return { error: e.message || e.toString() }; } return { valid: true }; }); }; }); Field.prototype.contains = function (test, message) { return this.add(function(value) { try { ValidatorPrototype.contains.call(externalValidator.check(value, message), test); } catch (e) { return { error: message || "%s does not contain required characters" }; } return { valid: true }; }); }; Field.prototype.notContains = function (test, message) { return this.add(function (value) { try { ValidatorPrototype.notContains.call(externalValidator.check(value, message), test); } catch (e) { return { error: message || "%s contains invalid characters" }; } return { valid: true }; }); }; Field.prototype.equals = function (other, message) { if (object.isString(other) && other.match(/^field::/)) { this.__required = true; } return this.add(function (value, source) { // If other is a field token (field::fieldname), grab the value of fieldname // and use that as the OTHER value. var test = other; if (object.isString(other) && other.match(/^field::/)) { test = utils.getProp(other.replace(/^field::/, ""), source); } if (value != test) { return { error: message || "%s does not equal " + String(test) }; } return { valid: true }; }); }; // node-validator's numeric validator seems unintuitive. All numeric values should be valid, not just int. Field.prototype.isNumeric = function (message) { return this.add(function (value) { if (object.isNumber(value) || (object.isString(value) && value.match(/^[-+]?[0-9]*\.?[0-9]+$/))) { } else { return { error: message || "%s is not a number" }; } return { valid: true }; }); }; // node-validator's decimal/float validator incorrectly thinks Ints are valid. Field.prototype.isFloat = Field.prototype.isDecimal = function (message) { return this.add(function (value) { if ((object.isNumber(value) && value % 1 == 0) || (object.isString(value) && value.match(/^[-+]?[0-9]*\.[0-9]+$/))) { } else { return { error: message || "%s is not a decimal" }; } return { valid: true }; }); }; Field.prototype.regex = Field.prototype.is = function (pattern, modifiers, message) { // regex(/pattern/) // regex(/pattern/, "message") // regex("pattern") // regex("pattern", "modifiers") // regex("pattern", "message") // regex("pattern", "modifiers", "message") if (pattern instanceof RegExp) { if (object.isString(modifiers) && modifiers.match(/^[gimy]+$/)) { throw new Error("Invalid arguments: `modifiers` can only be passed in if `pattern` is a string."); } message = modifiers; modifiers = undefined; } else if (object.isString(pattern)) { if (arguments.length == 2 && !modifiers.match(/^[gimy]+$/)) { // 2nd arg doesn't look like modifier flags, it's the message (might also be undefined) message = modifiers; modifiers = undefined; } pattern = new RegExp(pattern, modifiers); } return this.add(function (value) { if (pattern.test(value) === false) { return { error: message || "%s has invalid characters" }; } return { valid: true }; }); }; Field.prototype.notRegex = Field.prototype.not = function(pattern, modifiers, message) { // notRegex(/pattern/) // notRegex(/pattern/, "message") // notRegex("pattern") // notRegex("pattern", "modifiers") // notRegex("pattern", "message") // notRegex("pattern", "modifiers", "message") if (pattern instanceof RegExp) { if (object.isString(modifiers) && modifiers.match(/^[gimy]+$/)) { throw new Error("Invalid arguments: `modifiers` can only be passed in if `pattern` is a string."); } message = modifiers; modifiers = undefined; } else if (object.isString(pattern)) { if (arguments.length == 2 && !modifiers.match(/^[gimy]+$/)) { // 2nd arg doesn't look like modifier flags, it's the message (might also be undefined) message = modifiers; modifiers = undefined; } pattern = new RegExp(pattern, modifiers); } return this.add(function(value) { if (pattern.test(value) === true) { return { error: message || "%s has invalid characters" }; } return { valid: true }; }); }; Field.prototype.required = function (placeholderValue, message) { this.__required = true; return this.add(function (value) { if (!utils.hasValue(value) || value == placeholderValue) { return { error: message || "%s is required" }; } return { valid: true }; }); }; Field.prototype.minLength = function (length, message) { return this.add(function(value) { if (value.toString().length < length) { return { error: message || "%s is too short" }; } return { valid: true }; }); }; Field.prototype.maxLength = function (length, message) { return this.add(function(value) { if (value.toString().length > length) { return { error: message || "%s is too long" }; } return { valid: true }; }); }; Field.prototype.customValidator = function(func, message) { return this.add(function(value, source) { try { func(value, source); } catch (e) { return { error: message || e.message || "%s is invalid" }; } return { valid: true }; }); }; module.exports = Field;