UNPKG

mustom-validator

Version:

Lightweight yet powerful, highly extensible validation/sanitization library

931 lines (750 loc) 30.9 kB
// MUSTOM, More Than Custom, https://mustom.com // Copyright © Ryu Woosik. All rights reserved. const { errorHandler } = require('../util/error-handler') const { dataTypeChecker } = require('../util/data-type-checker') const dataType = { any: function () { return this }, null: function () { this.criterion = 'null' if (this.dataType === 'undefined') { return this } if (this.dataType !== 'null') { errorHandler(this, 'ValidationError', `The value {{input}} should be null.`) } return this }, undefined: function () { this.criterion = 'undefined' if (this.dataType === 'undefined') { return this } errorHandler(this, 'ValidationError', `The value {{input}} should be undefined.`) return this }, nan: function () { this.criterion = 'nan' if (this.dataType === 'undefined') { return this } if (!this.dataType !== 'nan') { errorHandler(this, 'ValidationError', `The value {{input}} should be NaN.`) } return this }, map: function () { this.criterion = 'map' if (this.dataType === 'undefined') { return this } if (this.dataType !== 'map') { errorHandler(this, 'ValidationError', `The value {{input}} should be a map.`) } return this }, set: function () { this.criterion = 'set' if (this.dataType === 'undefined') { return this } if (this.dataType !== 'set') { errorHandler(this, 'ValidationError', `The value {{input}} should be a set.`) } return this }, bigInt: function () { this.criterion = 'bigint' if (this.dataType === 'undefined') { return this } if (this.dataType !== 'bigint') { errorHandler(this, 'ValidationError', `The value {{input}} should be a bigint.`) } return this }, function: function () { this.criterion = 'function' if (this.dataType === 'undefined') { return this } if (this.dataType !== 'function') { errorHandler(this, 'ValidationError', `The value {{input}} should be a function.`) } return this }, symbol: function () { this.criterion = 'symbol' if (this.dataType === 'undefined') { return this } if (this.dataType !== 'symbol') { errorHandler(this, 'ValidationError', `The value {{input}} should be a symbol.`) } return this }, regexp: function () { this.criterion = 'regexp' if (this.dataType === 'undefined') { return this } if (this.dataType !== 'regexp') { errorHandler(this, 'ValidationError', `The value {{input}} should be a regexp.`) } return this }, /** * Checks if the input is an object. * @example * validator.single({ key: 'value' }).object() // Passes * validator.single([1, 2, 3]).object() // Throws an error */ object: function () { this.criterion = 'object' if (this.dataType === 'undefined') { return this } if (this.dataType !== 'object') { errorHandler(this, 'ValidationError', `The value {{input}} should be an object.`) } return this }, /** * Checks if the input is an array of objects. * @example * validator.single([{ key: 'value' }]).arrayOfObject() // Passes * validator.single([1, 2, 3]).arrayOfObject() // Throws an error */ arrayOfObject: function () { this.criterion = 'arrayOfObject' if (this.dataType === 'undefined') { return this } if (this.dataType !== 'array') { errorHandler(this, 'ValidationError', `The value {{input}} should be an array of objects.`) } for (const item of this.input) { const itemType = dataTypeChecker(item) if (itemType !== 'object') { errorHandler(this, 'ValidationError', `The value {{input}} should be an array of objects.`) } } return this }, /** * Validate the value is an array. * @example * validator.single([1, 2, 3]).array() // Passes * validator.single('hello').array() // Throws an error */ array: function () { this.criterion = 'array' if (this.dataType === 'undefined') { return this } if (this.dataType !== 'array') { errorHandler(this, 'ValidationError', `The value {{input}} should be an array.`) } return this }, /** * Validate the value is a string. If the value is null, undefined or empty string, it will be ignored. * @example * validator.single('hello').string() // Passes * validator.single(1).string() // Throws an error * validator.single(true).string() // Throws an error */ string: function () { this.criterion = 'string' if (this.dataType === 'undefined') { return this } if (this.dataType !== 'string') { errorHandler(this, 'ValidationError', `The value {{input}} should be a string.`) } return this }, /** * Validate the value is a boolean. If the value is null, undefined or empty string, it will be ignored. * @example * validator.single(true).boolean() // Passes * validator.single(1).boolean() // Throws an error * validator.single('true').boolean() // Throws an error */ boolean: function () { this.criterion = 'boolean' if (this.dataType === 'undefined') { return this } if (this.dataType !== 'boolean') { errorHandler(this, 'ValidationError', `The value {{input}} should be a boolean.`) } return this }, /** * Validate the value is a number. If the value is null, undefined or empty string, it will be ignored. * @example * validator.single(1).number() // Passes * validator.single(1.1).number() // Passes * validator.single('1').number() // Throws an error * validator.single('mustom').number() // Throws an error */ number: function () { this.criterion = 'number' if (this.dataType === 'undefined') { return this } if (this.dataType !== 'number') { errorHandler( this, 'ValidationError', `The value {{input}} should be a number.` ) } return this }, /** * Validate the value is a non-negative number. It can be a fraction or zero. * Allowed : 0, 1, 2, 3, 1.1, 2.2, 3.3, ... * @example * validator.single(0).nonNegativeNumber() // Passes * validator.single(1).nonNegativeNumber() // Passes * validator.single(1.1).nonNegativeNumber() // Passes * validator.single(-1).nonNegativeNumber() // Throws an error */ nonNegativeNumber: function () { this.criterion = 'nonNegativeNumber' if (this.dataType === 'undefined') { return this } if (this.dataType !== 'number' || this.input < 0) { errorHandler( this, 'ValidationError', `The value {{input}} should be a non-negative number.` ) } return this }, /** * Validate the value is a positive number which is not negative and not zero. It can be a fraction. * Allowed : 1, 2, 3, 1.1, 2.2, 3.3, ... * @example * validator.single(1).positiveNumber() // Passes * validator.single(1.1).positiveNumber() // Passes * validator.single(-1).positiveNumber() // Throws an error * validator.single(0).positiveNumber() // Throws an error */ positiveNumber: function () { this.criterion = 'positiveNumber' if (this.dataType === 'undefined') { return this } if (this.dataType !== 'number' || this.input <= 0) { errorHandler( this, 'ValidationError', `The value {{input}} should be a positive number.` ) } return this }, /** * Validate the value is a natural number which is not negative and not a fraction, and not zero. * Allowed : 1, 2, 3, 4, 5, ... * @example * validator.single(1).naturalNumber() // Passes * validator.single(1.1).naturalNumber() // Throws an error * validator.single(-1).naturalNumber() // Throws an error * validator.single(0).naturalNumber() // Throws an error */ naturalNumber: function () { this.criterion = 'naturalNumber' if (this.dataType === 'undefined') { return this } if (this.input % 1 !== 0 || this.input <= 0 || this.dataType !== 'number') { errorHandler( this, 'ValidationError', `The value {{input}} should be a natural number.` ) } return this }, /** * Validate the value is a whole number which is not negative and not a fraction. * Allowed : 0, 1, 2, 3, 4, 5, ... * @example * validator.single(0).wholeNumber() // Passes * validator.single(1).wholeNumber() // Passes * validator.single(1.1).wholeNumber() // Throws an error * validator.single(-1).wholeNumber() // Throws an error */ wholeNumber: function () { this.criterion = 'wholeNumber' if (this.dataType === 'undefined') { return this } if (this.input % 1 !== 0 || !this.input < 0 || this.dataType !== 'number') { errorHandler( this, 'ValidationError', `The value {{input}} should be a whole number.` ) } return this }, /** * Validate the value is an integer. It can be a negative number, zero or positive number. * Allowed : -3, -2, -1, 0, 1, 2, 3, ... * @example * validator.single(0).integer() // Passes * validator.single(1).integer() // Passes * validator.single(-1).integer() // Passes * validator.single(1.1).integer() // Throws an error */ integer: function () { this.criterion = 'integer' if (this.dataType === 'undefined') { return this } if (this.input % 1 !== 0 || this.dataType !== 'number') { errorHandler( this, 'ValidationError', `The value {{input}} should be an integer.` ) } return this }, /** * Validate the value is a negative integer which is not a fraction, and less than zero. * Allowed : -3, -2, -1 * @example * validator.single(-1).negativeInteger() // Passes * validator.single(-1.1).negativeInteger() // Throws an error * validator.single(1).negativeInteger() // Throws an error * validator.single(0).negativeInteger() // Throws an error */ negativeInteger: function () { this.criterion = 'negativeInteger' if (this.dataType === 'undefined') { return this } if (this.input % 1 !== 0 || !this.input >= 0 || this.dataType !== 'number') { errorHandler( this, 'ValidationError', `The value {{input}} should be a negative integer.` ) } return this }, /** * Validate the value is a valid email format. * @example * validator.single('mustom@email.com').email() // Passes * validator.single('mustom').email() // Throws an error */ email: function () { this.criterion = 'email' if (this.dataType === 'undefined') { return this } const regex = /^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i const isPassed = regex.test(this.input) if (!isPassed) { errorHandler(this, 'ValidationError', `The value {{input}} should be a correct email format.`) } return this }, /** * Validate the value is a URL format. It should be start with http:// or https://. * @example * validator.single('http://mustom.com').url() // Passes * validator.single('https://mustom.com').url() // Passes * validator.single('mustom.com').url() // Throws an error * validator.single('ftp://mustom.com').url() // Throws an error */ url: function () { this.criterion = 'url' if (this.dataType === 'undefined') { return this } const regex = /^http[s]?:\/\// const isPassed = regex.test(this.input) if (!isPassed) { errorHandler(this, 'ValidationError', `The value {{input}} should be a valid URL format.`) } return this }, /** * Validate the value is a IP format. It should be IPv4 or IPv6. * @example * validator.single('192.168.0.1').ip() // Passes * validator.single('2001:0db8:85a3:0000:0000:8a2e:0370:7334').ip() // Passes * validator.single('::ffff:192.168.0.1').ip() // Passes * validator.single('mustom.com').ip() // Throws an error */ ip: function () { this.criterion = 'ip' if (this.dataType === 'undefined') { return this } const regex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/ const isPassed = regex.test(this.input) if (!isPassed) { errorHandler(this, 'ValidationError', `The value {{input}} should be a valid IP format.`) } return this }, /** * Validate the value is a code format that is used in Mustom. * It should be start with alphabet and contain only number, alphabet, underscore, and hyphen. * Max length is 50 characters. * @example * validator.single('mustom-123').code() // Passes * validator.single('mustom@123').code() // Throws an error * validator.single('123-mustom').code() // Throws an error */ code: function () { this.criterion = 'code' if (this.dataType === 'undefined') { return this } if (this.input.length < 50) { errorHandler( this, 'ValidationError', `The value {{input}} should be less than 50 characters.` ) } const regex = /^[A-Za-z][A-Za-z0-9_-]*$/ const isPassed = regex.test(this.input) if (!isPassed) { errorHandler( this, 'ValidationError', `The value {{input}} should be start with alphabet, and should be contain only number, alphabet, underscore, and hyphen.` ) } return this }, /** * Validate the value is a path format that is used in Mustom. * It should be contain only number, alphabet, underscore, and hyphen. * Max length is 50 characters. * @example * validator.single('mustom-123').path() // Passes * validator.single('mustom@123').path() // Throws an error * validator.single('mustom/123').path() // Throws an error */ path: function () { this.criterion = 'path' if (this.dataType === 'undefined') { return this } const regex = /^[A-Za-z0-9_-]*$/ const isPassed = regex.test(this.input) if (!isPassed) { errorHandler( this, 'ValidationError', `The value {{input}} should be contain only number, alphabet, underscore, and hyphen.` ) } return this }, /** * Validate the value is an injection safe string. * It should be contain only number, alphabet, underscore, dot, 골뱅이 and hyphen. * It used for admin username. * @example * validator.single('mustom-123').code() // Passes * validator.single('mustom@123').code() // Passes * validator.single('mus/tom').code() // Throws an error */ injectionSafeString: function () { this.criterion = 'injectionSafeString' if (this.dataType === 'undefined') { return this } const regex = /^[A-Za-z0-9_.@-]*$/ const isPassed = regex.test(this.input) if (!isPassed) { errorHandler( this, 'ValidationError', `The value {{input}} should be contain only number, alphabet, underscore, dot, at sign, and hyphen.` ) } return this }, /** * Validate the value is an alphabet. * It should be contain only alphabet characters (A-Z, a-z). * @example * validator.single('mustom').alphabet() // Passes * validator.single('MUSTOM').alphabet() // Passes * validator.single('mustom123').alphabet() // Throws an error * validator.single('mustom@').alphabet() // Throws an error */ alphabet: function () { this.criterion = 'alphabet' if (this.dataType === 'undefined') { return this } const regex = /^[A-Za-z]*$/ const isPassed = regex.test(this.input) if (!isPassed) { errorHandler(this, 'ValidationError', `The value {{input}} should be an alphabet.`) } return this }, /** * Validate the value is a capital letter. * It should be contain only capital letters (A-Z). * @example * validator.single('MUSTOM').uppercase() // Passes * validator.single('mustom').uppercase() // Throws an error * validator.single('MUSTOM123').uppercase() // Throws an error * validator.single('MUSTOM@').uppercase() // Throws an error */ uppercase: function () { this.criterion = 'uppercase' if (this.dataType === 'undefined') { return this } const regex = /^[A-Z]*$/ const isPassed = regex.test(this.input) if (!isPassed) { errorHandler(this, 'ValidationError', `The value {{input}} should be a capital letter.`) } return this }, /** * Validate the value is a lowercase letter. * It should be contain only lowercase letters (a-z). * @example * validator.single('mustom').lowercase() // Passes * validator.single('MUSTOM').lowercase() // Throws an error * validator.single('mustom123').lowercase() // Throws an error * validator.single('mustom@').lowercase() // Throws an error */ lowercase: function () { this.criterion = 'lowercase' if (this.dataType === 'undefined') { return this } const regex = /^[a-z]*$/ const isPassed = regex.test(this.input) if (!isPassed) { errorHandler(this, 'ValidationError', `The value {{input}} should be a lowercase.`) } return this }, /** * Validate the value is an alphanumeric string. * It should be contain only number and alphabet characters (A-Z, a-z, 0-9). * @example * validator.single('mustom').alphaNumeric() // Passes * validator.single('MUSTOM123').alphaNumeric() // Passes * validator.single('mustom@123').alphaNumeric() // Throws an error * validator.single('mustom-123').alphaNumeric() // Throws an error */ alphaNumeric: function () { this.criterion = 'alphaNumeric' if (this.dataType === 'undefined') { return this } const regex = /^[A-Za-z0-9]*$/ const isPassed = regex.test(this.input) if (!isPassed) { errorHandler( this, 'ValidationError', `The value {{input}} should be contain only number and alphabet.` ) } return this }, /** * Validate the value is a valid password format. (Used in Mustom Admin) * It should be minimum eight characters, maximum twenty characters, at least one letter, one number and one special character. * If the value is null, undefined or empty string, it will be ignored. * @example * validator.single('Password1!').password() // Passes * validator.single('Pass1!').password() // Throws an error * validator.single('Password!').password() // Throws an error * validator.single('Password1').password() // Throws an error */ password: function () { this.criterion = 'password' if (this.dataType === 'undefined') { return this } const regex = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,20}$/ const isPassed = regex.test(this.input) if (!isPassed) { errorHandler(this, 'ValidationError', `The value {{input}} should be a valid password format.`) } return this }, /** * Validate the value is an image file format. * It should be end with one of the following extensions: jpg, jpeg, png, gif, bmp, tiff, tif, svg, webp. * If the value is null, undefined or empty string, it will be ignored. * @example * validator.single('image.jpg').imageFile() // Passes * validator.single('image.png').imageFile() // Passes * validator.single('image.txt').imageFile() // Throws an error * validator.single('image').imageFile() // Throws an error */ imageFile: function () { this.criterion = 'imageFile' if (this.dataType === 'undefined') { return this } const regex = /[^\s]+(.*?).(jpg|jpeg|png|gif|bmp|tiff|tif|svg|webp)$/ if (this.dataType === 'array') { for (const item of this.input) { const valueToLowerCase = item.toLowerCase() if (!regex.test(valueToLowerCase)) { errorHandler(this, 'ValidationError', `The value {{input}} should be a valid image file format.`) } } return this } const valueToLowerCase = this.input.toLowerCase() const isPassed = regex.test(valueToLowerCase) if (!isPassed) { errorHandler(this, 'ValidationError', `The value {{input}} should be a valid image file format.`) } return this }, /** * Validate the value is a valid date time format. * It can be in various formats including common date/time format and ISO 8601 formats. * @example * validator.single('2023-01-05 09:05:02').dateTime() // Passes * validator.single('2023-01-05T09:05:02Z').dateTime() // Passes * validator.single('2023-01-05').dateTime() // Throws an error * validator.single('09:05:02').dateTime() // Throws an error */ dateTime: function () { this.criterion = 'dateTime' if (this.dataType === 'undefined') { return this } if (typeof this.input !== 'string') { errorHandler(this, 'ValidationError', `The value {{input}} should be a string for datetime validation.`) return this } // Define multiple valid datetime patterns FIRST const validPatterns = [ // Common date/time format (YYYY-MM-DD HH:mm:ss) /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/, // ISO 8601 formats /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/, // YYYY-MM-DDTHH:mm:ss /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{1,3}$/, // YYYY-MM-DDTHH:mm:ss.s/ss/sss /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/, // YYYY-MM-DDTHH:mm:ssZ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{1,3}Z$/, // YYYY-MM-DDTHH:mm:ss.s/ss/sssZ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}$/, // YYYY-MM-DDTHH:mm:ss+HH:mm /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{1,3}[+-]\d{2}:\d{2}$/, // YYYY-MM-DDTHH:mm:ss.s/ss/sss+HH:mm // Additional common formats /^\d{4}-\d{1,2}-\d{1,2} \d{1,2}:\d{2}:\d{2}$/, // Single digit month/day/hour (e.g. 2023-1-5 9:05:02) /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{1,3}$/, // With milliseconds space separator (e.g. 2023-01-05 09:05:02.123) /^\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}$/, // Forward slashes (e.g. 2023/01/05 09:05:02) /^\d{2}\/\d{2}\/\d{4} \d{2}:\d{2}:\d{2}$/, // MM/DD/YYYY format (e.g. 01/05/2023 09:05:02) /^\d{2}-\d{2}-\d{4} \d{2}:\d{2}:\d{2}$/ // DD-MM-YYYY format (e.g. 05-01-2023 09:05:02) ] const isValidFormat = validPatterns.some(pattern => pattern.test(this.input)) if (!isValidFormat) { errorHandler(this, 'ValidationError', `The value {{input}} should be a valid date time format.`) return this } // Validate if it creates a valid date (less restrictive but catches logical errors) const date = new Date(this.input) if (isNaN(date.getTime())) { errorHandler(this, 'ValidationError', `The value {{input}} should be a valid date time.`) return this } // Additional logical validation if strictDateValidation option is enabled if (this.option.strictDateValidation) { const isoMatch = this.input.match(/^(\d{4})-(\d{1,2})-(\d{1,2})/) if (isoMatch) { const year = parseInt(isoMatch[1]) const month = parseInt(isoMatch[2]) const day = parseInt(isoMatch[3]) // Basic range validation if (month < 1 || month > 12) { errorHandler(this, 'ValidationError', `The month '${month}' should be between 1 and 12.`) return this } if (day < 1 || day > 31) { errorHandler(this, 'ValidationError', `The day '${day}' should be between 1 and 31.`) return this } // Check if the constructed date matches input (prevents Feb 30, etc.) const testDate = new Date(year, month - 1, day) if (testDate.getFullYear() !== year || testDate.getMonth() !== month - 1 || testDate.getDate() !== day) { errorHandler(this, 'ValidationError', `The date '${year}-${month}-${day}' is not a valid date.`) return this } } } return this }, /** * Validate the value is a valid ISO 8601 date format (YYYY-MM-DD). * @example * validator.single('2023-01-05').dateOnly() // Passes * validator.single('2023-1-5').dateOnly() // Throws an error * validator.single('2023/01/05').dateOnly() // Throws an error * validator.single('05-01-2023').dateOnly() // Throws an error */ dateOnly: function () { this.criterion = 'dateOnly' if (this.dataType === 'undefined') { return this } const regex = /^\d{4}-\d{2}-\d{2}$/ const isPassed = regex.test(this.input) if (!isPassed) { errorHandler(this, 'ValidationError', `The value {{input}} should be a valid date format.`) } // Additional logical validation if strictDateValidation option is enabled if (this.option.strictDateValidation) { const match = this.input.match(/^(\d{4})-(\d{2})-(\d{2})$/) if (match) { const year = parseInt(match[1]) const month = parseInt(match[2]) const day = parseInt(match[3]) // Basic range validation if (month < 1 || month > 12) { errorHandler(this, 'ValidationError', `The month '${month}' should be between 1 and 12.`) return this } if (day < 1 || day > 31) { errorHandler(this, 'ValidationError', `The day '${day}' should be between 1 and 31.`) return this } // Check if the constructed date matches input (prevents Feb 30, etc.) const testDate = new Date(year, month - 1, day) if (testDate.getFullYear() !== year || testDate.getMonth() !== month - 1 || testDate.getDate() !== day) { errorHandler(this, 'ValidationError', `The date {{input}} is not a valid date.`) return this } } } return this } } module.exports = dataType