UNPKG

x2node-validators

Version:
877 lines (717 loc) 20.4 kB
'use strict'; /** * Standard validation error messages. * * @protected * @type {Object.<string,Object.<string,string>>} */ exports.VALIDATION_ERROR_MESSAGES = { '*': { 'en-US': 'Invalid.' }, 'missing': { 'en-US': 'Missing value.' }, 'invalidType': { 'en-US': 'Missing or invalid type.' }, 'notEmpty': { 'en-US': 'Expected to be empty.' }, 'invalidValueType': { 'en-US': 'Invalid value type ${actual}, expected ${expected}.' }, 'invalidValue': { 'en-US': 'Invalid value.' }, 'invalidFormat': { 'en-US': 'Invalid format.' }, 'invalidNumber': { 'en-US': 'Not a number.' }, 'invalidInteger': { 'en-US': 'Not an integer.' }, 'invalidDatetime': { 'en-US': 'Invalid datetime.' }, 'invalidRefTarget': { 'en-US': 'Invalid reference target ${actual}, expected ${expected}.' }, 'invalidRefTargetPoly': { 'en-US': 'Invalid reference target ${actual},' + ' expected one of: ${expected}.' }, 'invalidRefTargetIdNumber': { 'en-US': 'Invalid target record id value ${value}, expected a number.' }, 'invalidPattern': { 'en-US': 'Does not match the pattern.' }, 'notArray': { 'en-US': 'Not an array.' }, 'duplicates': { 'en-US': 'Contains duplicates.' }, 'tooLong': { 'en-US': 'Too long.' }, 'tooShort': { 'en-US': 'Too short.' }, 'tooLarge': { 'en-US': 'Too large.' }, 'tooSmall': { 'en-US': 'Too small.' }, 'outOfRange': { 'en-US': 'Out of range.' }, 'invalidEmail': { 'en-US': 'Invalid e-mail address.' }, 'invalidDate': { 'en-US': 'Invalid date.' }, 'invalidTime': { 'en-US': 'Invalid time.' }, 'invalidTimeGranularity': { 'en-US': 'Must be aligned to ${granularity} minutes.' }, 'invalidWeekday': { 'en-US': 'Invalid weekday.' }, 'invalidCCNumber': { 'en-US': 'Invalid credit card number.' }, 'invalidBankRoutingNumber': { 'en-US': 'Invalid routing number.' }, 'invalidUSState': { 'en-US': 'Invalid state.' }, 'invalidUSZip': { 'en-US': 'Invalid ZIP code.' }, 'invalidUSPhone': { 'en-US': 'Invalid phone number.' }, 'invalidRangeDef': { 'en-US': 'Must be greater than ${rangeLoName}.' }, 'missingWhen': { 'en-US': 'Required with ${prop}.' }, 'missingWhenNot': { 'en-US': 'Required when ${prop} is empty.' }, 'missingWhenValue': { 'en-US': 'Requried when ${prop} is ${value}.' }, 'missingWhenNotValue': { 'en-US': 'Required when ${prop} is not ${value}.' }, 'missingWhenPattern': { 'en-US': 'Required when ${prop} has the provided value.' }, 'missingWhenNotPattern': { 'en-US': 'Required when ${prop} has the provided value.' }, 'notEmptyWhen': { 'en-US': 'Expected to be empty with ${prop}.' }, 'notEmptyWhenNot': { 'en-US': 'Expected to be empty when ${prop} is empty.' }, 'notEmptyWhenValue': { 'en-US': 'Expected to be empty when ${prop} is ${value}.' }, 'notEmptyWhenNotValue': { 'en-US': 'Expected to be empty when ${prop} is not ${value}.' }, 'notEmptyWhenPattern': { 'en-US': 'Expected to be empty when ${prop} has the provided value.' }, 'notEmptyWhenNotPattern': { 'en-US': 'Expected to be empty when ${prop} has the provided value.' } }; /** * Tell if the specified property value can be considered empty. * * @private * @param {module:x2node-pointers~RecordElementPointer} ptr Property pointer. * @param {module:x2node-records~PropertyDescriptor} propDesc Property * descriptor. * @param {*} value Property value to test. * @returns {boolean} <code>true</code> if empty. */ function isEmpty(ptr, propDesc, value) { if ((value === undefined) || (value === null)) return true; if (propDesc.isArray() && !ptr.collectionElement && Array.isArray(value)) { if (value.length === 0) return true; } else if (propDesc.isMap() && !ptr.collectionElement && ((typeof value) === 'object')) { if (Object.keys(value).length === 0) return true; } else if ((propDesc.isScalar() || ptr.collectionElement) && (propDesc.scalarValueType === 'string') && ((typeof value) === 'string')) { if (value.length === 0) return true; } return false; } /** * Tell if the specified context container has specified property and it * optionally matches the specified test value. * * @private * @param {module:x2node-validators~ValidationContext} ctx Current validation * context. * @param {string} propName Sibling property name. * @param {(*|RegExp)} [testValue] Test value. * @param {boolean} whenErrors What to return when <code>propName</code> has * validation errors. * @returns {boolean} <code>true</code> if has matching sibling. */ function hasMatchingSibling(ctx, propName, testValue, whenErrors) { const containerDesc = ctx.currentPropDesc.container; const propDesc = containerDesc.getPropertyDesc(propName); let propPtr; if (containerDesc.parentContainer && containerDesc.parentContainer.isPolymorphObject()) { const pathParts = containerDesc.nestedPath.split('.'); propPtr = ctx.currentPointer.parent.createChildPointer( `${pathParts[pathParts.length - 2]}:${propName}`); } else { propPtr = ctx.currentPointer.parent.createChildPointer(propName); } if (ctx.hasErrorsFor(propPtr)) return whenErrors; const container = ctx.containersChain[ctx.containersChain.length - 1]; const propValue = container[propName]; if (testValue !== undefined) { if (((testValue instanceof RegExp) && testValue.test(propValue)) || (propValue === testValue)) return true; } else { if (!isEmpty(propPtr, propDesc, propValue)) return true; } return false; } /** * Used to store dependency validators errors on the validation context. * * @private * @constant {Symbol} */ const DEPS_ERRORS = Symbol('DEPS_ERRORS'); /** * Add dependecy validator error to the context if no dependency validator errors * already added for the current property. * * @private * @param {module:x2node-validators~ValidationContext} ctx Current validation * context. * @param {string} type Validator type. * @param {string} propName Dependency property name. * @param {(*|RegExp)} testValue Dependecy property test value. */ function addDepsError(ctx, type, propName, testValue) { let depsErrors = ctx[DEPS_ERRORS]; if (!depsErrors) depsErrors = ctx[DEPS_ERRORS] = {}; const curPtrStr = ctx.currentPointer.toString(); if (depsErrors[curPtrStr]) return; depsErrors[curPtrStr] = true; const hasTestValue = (testValue !== undefined); const testValueIsPattern = (testValue instanceof RegExp); ctx.addError( ( hasTestValue ? ( testValueIsPattern ? `{${type}Pattern}` : `{${type}Value}` ) : `{${type}}` ), { prop: propName, [testValueIsPattern ? 'pattern' : 'value']: testValue } ); } /** * Standard validator/normalizer definitions. * * @protected * @type {Object.<string,module:x2node-validators.validator>} */ exports.VALIDATOR_DEFS = { 'required': function(_, ctx, value) { if (isEmpty(ctx.currentPointer, ctx.currentPropDesc, value)) ctx.addError('{missing}'); return value; }, 'empty': function(_, ctx, value) { if (!isEmpty(ctx.currentPointer, ctx.currentPropDesc, value)) ctx.addError('{notEmpty}'); return value; }, 'string': function(_, ctx, value) { if ((value === undefined) || (value === null)) return value; const actual = (typeof value); if (actual !== 'string') ctx.addError('{invalidValueType}', { expected: 'string', actual: actual }); return value; }, 'number': function(_, ctx, value) { if ((value === undefined) || (value === null)) return value; const actual = (typeof value); if (actual !== 'number') ctx.addError('{invalidValueType}', { expected: 'number', actual: actual }); else if (!Number.isFinite(value)) ctx.addError('{invalidNumber}', { value: value }); return value; }, 'boolean': function(_, ctx, value) { if ((value === undefined) || (value === null)) return value; const actual = (typeof value); if (actual !== 'boolean') ctx.addError('{invalidValueType}', { expected: 'boolean', actual: actual }); return value; }, 'datetime': function(_, ctx, value) { if ((value === undefined) || (value === null)) return value; const actual = (typeof value); if (actual !== 'string') { ctx.addError('{invalidValueType}', { expected: 'string', actual: actual }); } else if ( !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/.test(value)) { ctx.addError('{invalidFormat}'); } else { const dateVal = Date.parse(value); if (Number.isNaN(dateVal)) ctx.addError('{invalidDatetime}', { value: value }); else return (new Date(dateVal)).toISOString(); } return value; }, 'ref': function(params, ctx, value) { if ((value === undefined) || (value === null)) return value; const actual = (typeof value); if (actual !== 'string') { ctx.addError('{invalidValueType}', { expected: 'string', actual: actual }); } else { const hashInd = value.indexOf('#'); if ((hashInd <= 0) || (hashInd === value.length - 1)) { ctx.addError('{invalidFormat}'); } else { const refTarget = value.substring(0, hashInd); if (!params.some(rtn => (rtn === refTarget))) { if (params.length > 1) { ctx.addError('{invalidRefTargetPoly}', { actual: refTarget, expected: params.join(', ') }); } else { ctx.addError('{invalidRefTarget}', { actual: refTarget, expected: params[0] }); } } else { const targetRecordTypeDesc = ctx.recordTypes.getRecordTypeDesc(refTarget); const idPropDesc = targetRecordTypeDesc.getPropertyDesc( targetRecordTypeDesc.idPropertyName); if (idPropDesc.scalarValueType === 'number') { const targetIdString = value.substring(hashInd + 1); const targetId = Number(targetIdString); if (!Number.isFinite(targetId)) ctx.addError('{invalidRefTargetIdNumber}', { value: targetIdString }); else return refTarget + '#' + String(targetId); } } } } return value; }, 'object': function(_, ctx, value) { if ((value === undefined) || (value === null)) return value; const actual = (typeof value); if (actual !== 'object') ctx.addError('{invalidValueType}', { expected: 'object', actual: actual }); return value; }, 'array': function(_, ctx, value) { if ((value === undefined) || (value === null)) return value; if (!Array.isArray(value)) ctx.addError('{notArray}'); return value; }, 'noDupes': function(_, ctx, value) { if (Array.isArray(value)) { TOP: for (let i = 0, len = value.length; i < len - 1; i++) { const el = value[i]; for (let j = i + 1; j < len; j++) { if (value[j] === el) { ctx.addError('{duplicates}'); break TOP; } } } } return value; }, 'integer': function(_, ctx, value) { if (((typeof value) === 'number') && !Number.isInteger(value)) ctx.addError('{invalidInteger}'); return value; }, 'precision': function(params, _, value) { if (((typeof value) === 'number') && Number.isFinite(value)) { const p = Math.pow(10, params[0]); return Math.round(value * p) / p; } return value; }, 'dropEmptyString': function(_, __, value) { if (((typeof value) === 'string') && (value.length === 0)) return undefined; return value; }, 'trim': function(_, __, value) { if ((typeof value) === 'string') return value.replace(/^\s+|\s+$/g, ''); return value; }, 'pattern': function(params, ctx, value) { const pattern = params[0]; const re = ( pattern instanceof RegExp ? pattern : new RegExp(pattern)); if (((typeof value) === 'string') && !re.test(value)) ctx.addError('{invalidPattern}', { pattern: re }); return value; }, 'maxLength': function(params, ctx, value) { if ((Array.isArray(value) || ((typeof value) === 'string')) && (value.length > params[0])) ctx.addError('{tooLong}', { max: params[0] }); return value; }, 'minLength': function(params, ctx, value) { if ((Array.isArray(value) || ((typeof value) === 'string')) && (value.length < params[0])) ctx.addError('{tooShort}', { min: params[0] }); return value; }, 'max': function(params, ctx, value) { if ((value === undefined) || (value === null)) return value; const maxVal = params[0]; if (((typeof value) === (typeof maxVal)) && (value > maxVal)) ctx.addError('{tooLarge}', { max: maxVal }); return value; }, 'min': function(params, ctx, value) { if ((value === undefined) || (value === null)) return value; const minVal = params[0]; if (((typeof value) === (typeof minVal)) && (value < minVal)) ctx.addError('{tooSmall}', { min: minVal }); return value; }, 'range': function(params, ctx, value) { if ((value === undefined) || (value === null)) return value; const minVal = params[0]; const maxVal = params[1]; if (((typeof value) === (typeof minVal)) && ((typeof value) === (typeof maxVal)) && ((value < minVal) || (value > maxVal))) ctx.addError('{outOfRange}', { min: minVal, max: maxVal }); return value; }, 'oneOf': function(params, ctx, value) { if ((value === undefined) || (value === null)) return value; const validVals = (Array.isArray(params[0]) ? params[0] : params); if (!validVals.some(validVal => (value === validVal))) ctx.addError('{invalidValue}'); return value; }, 'lowercase': function(_, __, value) { if ((typeof value) === 'string') return value.toLowerCase(); return value; }, 'uppercase': function(_, __, value) { if ((typeof value) === 'string') return value.toUpperCase(); return value; }, 'email': function(_, ctx, value) { const re = new RegExp( '^[a-z0-9._%+\'-]+' + '@[a-z0-9][a-z0-9-]{0,63}(?:\\.[a-z0-9][a-z0-9-]{0,63})+$', 'i'); if (((typeof value) === 'string') && !re.test(value)) ctx.addError('{invalidEmail}'); return value; }, 'date': function(_, ctx, value) { if (((typeof value) === 'string') && !/^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])$/.test(value)) ctx.addError('{invalidDate}'); return value; }, 'time': function(params, ctx, value) { const param1 = params && params[0]; const param2 = params && params[1]; const granularity = Number.isInteger(param1) && param1; const options = ( ((typeof param1 === 'string') && param1) || ((typeof param2 === 'string') && param2) ); const re = ( options && /\ballow24\b/.test(options) ? /^24:00|([0-1][0-9]|2[0-3]):[0-5][0-9]$/ : /^([0-1][0-9]|2[0-3]):[0-5][0-9]$/ ); if ((typeof value) === 'string') { if (!re.test(value)) { ctx.addError('{invalidTime}'); } else if (granularity) { if (Number(value.substring(3)) % granularity !== 0) ctx.addError('{invalidTimeGranularity}', { granularity: granularity }); } } return value; }, 'timeToSecond': function(_, ctx, value) { if (((typeof value) === 'string') && !/^([0-1][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]$/.test(value)) ctx.addError('{invalidTime}'); return value; }, 'ccNumber': function(_, ctx, value) { if ((typeof value) === 'string') { if (!/^\d{13,16}$/.test(value)) { ctx.addError('{invalidCCNumber}'); } else { let sum = 0, even = false; for (let i = value.length - 1; i >= 0; i--) { let digit = Number(value.charAt(i)); if (even) digit *= 2; if (digit > 9) digit = digit / 10 + digit % 10; sum += digit; even = !even; } if (sum % 10 !== 0) ctx.addError('{invalidCCNumber}'); } } return value; }, 'bankRoutingNumber': function(_, ctx, value) { if ((typeof value) === 'string') { if (!/^\d{9}$/.test(value)) { ctx.addError('{invalidBankRoutingNumber}'); } else { const sum = ( 7 * ( Number(value.charAt(0)) + Number(value.charAt(3)) + Number(value.charAt(6)) ) + 3 * ( Number(value.charAt(1)) + Number(value.charAt(4)) + Number(value.charAt(7)) ) + 9 * ( Number(value.charAt(2)) + Number(value.charAt(5))) ); if (sum % 10 !== Number(value.charAt(8))) ctx.addError('{invalidBankRoutingNumber}'); } } return value; }, 'weekday2': function(_, ctx, value) { const re = new RegExp('^(MO|TU|WE|TH|FR|SA|SU)$', 'i'); if ((typeof value) === 'string') { if (re.test(value)) value = value.toUpperCase(); else ctx.addError('{invalidWeekday}'); } return value; }, 'weekday3': function(_, ctx, value) { const re = new RegExp('^(MON|TUE|WED|THU|FRI|SAT|SUN)$', 'i'); if ((typeof value) === 'string') { if (re.test(value)) value = value.toUpperCase(); else ctx.addError('{invalidWeekday}'); } return value; }, 'loc_US:state2': function(_, ctx, value) { const re = new RegExp( '^(AL|AK|AS|AZ|AR|CA|CO|CT|DE|DC|FL|GA|GU|HI|ID|IL|IN|IA|KS|KY|LA' + '|ME|MD|MH|MA|MI|FM|MN|MS|MO|MT|NE|NV|NH|NJ|NM|NY|NC|ND|MP|OH' + '|OK|OR|PW|PA|PR|RI|SC|SD|TN|TX|UT|VT|VA|VI|WA|WV|WI|WY)$', 'i'); if ((typeof value) === 'string') { if (re.test(value)) value = value.toUpperCase(); else ctx.addError('{invalidUSState}'); } return value; }, 'loc_US:zip5': function(_, ctx, value) { if (((typeof value) === 'string') && !/^\d{5}$/.test(value)) ctx.addError('{invalidUSZip}'); return value; }, 'loc_US:phone10': function(_, ctx, value) { if ((typeof value) === 'string') { const normValue = value.replace(/[\s()-]/g, ''); if (/^\d{10}$/.test(normValue)) value = normValue; else ctx.addError('{invalidUSPhone}'); } return value; }, 'rangeDef': function(params, ctx, value) { if ((value === undefined) || (value === null) || ((typeof value) !== 'object')) return value; const propLo = params[0]; const propHi = params[1]; const curPtr = ctx.currentPointer.toString(); let propLoPtr, propHiPtr; const containerDesc = ( ctx.currentPropDesc ? ctx.currentPropDesc.nestedProperties : ctx.recordTypeDesc ); if (containerDesc.isPolymorphObject()) { if (ctx.hasErrorsFor(`${curPtr}/${containerDesc.typePropertyName}`)) return value; const subtype = value[containerDesc.typePropertyName]; propLoPtr = `${curPtr}/${subtype}:${propLo}`; propHiPtr = `${curPtr}/${subtype}:${propHi}`; } else { propLoPtr = `${curPtr}/${propLo}`; propHiPtr = `${curPtr}/${propHi}`; } if (ctx.hasErrorsFor(propLoPtr) || ctx.hasErrorsFor(propHiPtr)) return value; const propLoVal = value[propLo]; const propHiVal = value[propHi]; if ((propLoVal === undefined) || (propLoVal === null) || (propHiVal === undefined) || (propHiVal === null)) return value; const nonZero = /\bnonZero\b/.test(params[2]); if ((nonZero && (propLoVal >= propHiVal)) || (!nonZero && (propLoVal > propHiVal))) { const propLoTitle = ctx.getElementTitle(propLoPtr); ctx.addErrorFor(propHiPtr, '{invalidRangeDef}', { rangeLoName: propLoTitle, rangeLoNameCaps: propLoTitle.charAt(0).toUpperCase() + propLoTitle.substring(1) }); } return value; }, 'requiredIf': function(params, ctx, value) { if (isEmpty(ctx.currentPointer, ctx.currentPropDesc, value)) { const propName = params[0]; const testValue = params[1]; if (hasMatchingSibling(ctx, propName, testValue, false)) addDepsError(ctx, 'missingWhen', propName, testValue); } return value; }, 'requiredUnless': function(params, ctx, value) { if (isEmpty(ctx.currentPointer, ctx.currentPropDesc, value)) { const propName = params[0]; const testValue = params[1]; if (!hasMatchingSibling(ctx, propName, testValue, true)) addDepsError(ctx, 'missingWhenNot', propName, testValue); } return value; }, 'emptyIf': function(params, ctx, value) { if (!isEmpty(ctx.currentPointer, ctx.currentPropDesc, value)) { const propName = params[0]; const testValue = params[1]; if (hasMatchingSibling(ctx, propName, testValue, false)) addDepsError(ctx, 'notEmptyWhen', propName, testValue); } return value; }, 'emptyUnless': function(params, ctx, value) { if (!isEmpty(ctx.currentPointer, ctx.currentPropDesc, value)) { const propName = params[0]; const testValue = params[1]; if (!hasMatchingSibling(ctx, propName, testValue, true)) addDepsError(ctx, 'notEmptyWhenNot', propName, testValue); } return value; } };