UNPKG

synctos

Version:

The Syncmaker. A tool to build comprehensive sync functions for Couchbase Sync Gateway.

535 lines (458 loc) 24.8 kB
function documentPropertiesValidationModule(utils, simpleTypeFilter, typeIdValidator) { var timeModule = importSyncFunctionFragment('time-module.js')(utils); var comparisonModule = importSyncFunctionFragment('comparison-module.js')(utils, buildItemPath, timeModule); return { validateProperties: function(doc, oldDoc, docDefinition) { var attachmentsValidationModule = importSyncFunctionFragment('attachments-validation-module.js')(utils, buildItemPath, resolveItemConstraint); var validationErrors = [ ]; var itemStack = [ { itemValue: doc, oldItemValue: oldDoc, itemName: null } ]; var resolvedPropertyValidators = utils.resolveDocumentConstraint(docDefinition.propertyValidators); // Ensure that, if the document type uses the simple type filter, it supports the "type" property if (docDefinition.typeFilter === simpleTypeFilter && utils.isValueNullOrUndefined(resolvedPropertyValidators.type)) { resolvedPropertyValidators.type = typeIdValidator; } // Execute each of the document's property validators while ignoring internal document properties at the root level validateObjectProperties( resolvedPropertyValidators, utils.resolveDocumentConstraint(docDefinition.allowUnknownProperties), true); if (doc._attachments) { storeOptionalValidationErrors(attachmentsValidationModule.validateAttachments(doc, docDefinition)); } return validationErrors; // The following functions are nested within this function so they can share access to the doc, oldDoc and // validationErrors params and the attachmentReferenceValidators and itemStack variables function validateObjectProperties(propertyValidators, allowUnknownProperties, ignoreInternalProperties) { var currentItemEntry = itemStack[itemStack.length - 1]; var objectValue = currentItemEntry.itemValue; var oldObjectValue = currentItemEntry.oldItemValue; var supportedProperties = [ ]; for (var propertyValidatorName in propertyValidators) { var validator = propertyValidators[propertyValidatorName]; if (utils.isValueNullOrUndefined(validator) || utils.isValueNullOrUndefined(resolveItemConstraint(validator.type))) { // Skip over non-validator fields/properties continue; } var propertyValue = objectValue[propertyValidatorName]; var oldPropertyValue; if (!utils.isValueNullOrUndefined(oldObjectValue)) { oldPropertyValue = oldObjectValue[propertyValidatorName]; } supportedProperties.push(propertyValidatorName); itemStack.push({ itemValue: propertyValue, oldItemValue: oldPropertyValue, itemName: propertyValidatorName }); validateItemValue(validator); itemStack.pop(); } // Verify there are no unsupported properties in the object if (!allowUnknownProperties) { for (var propertyName in objectValue) { if (ignoreInternalProperties && propertyName.indexOf('_') === 0) { // These properties are special cases that should always be allowed - generally only applied at the root // level of the document continue; } if (supportedProperties.indexOf(propertyName) < 0) { var objectPath = buildItemPath(itemStack); var fullPropertyPath = objectPath ? objectPath + '.' + propertyName : propertyName; validationErrors.push('property "' + fullPropertyPath + '" is not supported'); } } } } function validateItemValue(validator) { var currentItemEntry = itemStack[itemStack.length - 1]; var itemValue = currentItemEntry.itemValue; var validatorType = resolveItemConstraint(validator.type); if (validatorType === 'conditional') { return performConditionalValidation(validator); } else if (shouldSkipItemValidation(validator, validatorType)) { return; } if (validator.customValidation) { performCustomValidation(validator); } if (!utils.isDocumentMissingOrDeleted(oldDoc)) { if (resolveItemConstraint(validator.immutable)) { storeOptionalValidationErrors(comparisonModule.validateImmutable(itemStack, false, validatorType)); } if (resolveItemConstraint(validator.immutableStrict)) { // Omitting validator type forces it to perform strict equality comparisons for specialized string types // (e.g. "date", "datetime", "time", "timezone", "uuid") storeOptionalValidationErrors(comparisonModule.validateImmutable(itemStack, false)); } if (resolveItemConstraint(validator.immutableWhenSet)) { storeOptionalValidationErrors(comparisonModule.validateImmutable(itemStack, true, validatorType)); } if (resolveItemConstraint(validator.immutableWhenSetStrict)) { // Omitting validator type forces it to perform strict equality comparisons for specialized string types // (e.g. "date", "datetime", "time", "timezone", "uuid") storeOptionalValidationErrors(comparisonModule.validateImmutable(itemStack, true)); } } var expectedEqualValue = resolveItemConstraint(validator.mustEqual); if (expectedEqualValue !== void 0) { storeOptionalValidationErrors(comparisonModule.validateEquality(itemStack, expectedEqualValue, validatorType)); } var expectedStrictEqualValue = resolveItemConstraint(validator.mustEqualStrict); if (expectedStrictEqualValue !== void 0) { // Omitting validator type forces it to perform strict equality comparisons for specialized string types // (e.g. "date", "datetime", "time", "timezone", "uuid") storeOptionalValidationErrors(comparisonModule.validateEquality(itemStack, expectedStrictEqualValue)); } if (!utils.isValueNullOrUndefined(itemValue)) { if (resolveItemConstraint(validator.mustNotBeEmpty) && itemValue.length < 1) { validationErrors.push('item "' + buildItemPath(itemStack) + '" must not be empty'); } var minimumValue = resolveItemConstraint(validator.minimumValue); if (!utils.isValueNullOrUndefined(minimumValue)) { storeOptionalValidationErrors( comparisonModule.validateMinValueInclusiveConstraint(itemStack, minimumValue, validatorType)); } var minimumValueExclusive = resolveItemConstraint(validator.minimumValueExclusive); if (!utils.isValueNullOrUndefined(minimumValueExclusive)) { storeOptionalValidationErrors(comparisonModule.validateMinValueExclusiveConstraint( itemStack, minimumValueExclusive, validatorType)); } var maximumValue = resolveItemConstraint(validator.maximumValue); if (!utils.isValueNullOrUndefined(maximumValue)) { storeOptionalValidationErrors( comparisonModule.validateMaxValueInclusiveConstraint(itemStack, maximumValue, validatorType)); } var maximumValueExclusive = resolveItemConstraint(validator.maximumValueExclusive); if (!utils.isValueNullOrUndefined(maximumValueExclusive)) { storeOptionalValidationErrors(comparisonModule.validateMaxValueExclusiveConstraint( itemStack, maximumValueExclusive, validatorType)); } var minimumLength = resolveItemConstraint(validator.minimumLength); if (!utils.isValueNullOrUndefined(minimumLength) && itemValue.length < minimumLength) { validationErrors.push('length of item "' + buildItemPath(itemStack) + '" must not be less than ' + minimumLength); } var maximumLength = resolveItemConstraint(validator.maximumLength); if (!utils.isValueNullOrUndefined(maximumLength) && itemValue.length > maximumLength) { validationErrors.push('length of item "' + buildItemPath(itemStack) + '" must not be greater than ' + maximumLength); } switch (validatorType) { case 'any': // Any type of value is allowed - no further validation required break; case 'string': if (typeof itemValue !== 'string') { validationErrors.push('item "' + buildItemPath(itemStack) + '" must be a string'); } else { validateString(validator); } break; case 'integer': if (!utils.isValueAnInteger(itemValue)) { validationErrors.push('item "' + buildItemPath(itemStack) + '" must be an integer'); } break; case 'float': if (typeof itemValue !== 'number') { validationErrors.push('item "' + buildItemPath(itemStack) + '" must be a floating point or integer number'); } break; case 'boolean': if (typeof itemValue !== 'boolean') { validationErrors.push('item "' + buildItemPath(itemStack) + '" must be a boolean'); } break; case 'datetime': if (typeof itemValue !== 'string' || !timeModule.isIso8601DateTimeString(itemValue)) { validationErrors.push('item "' + buildItemPath(itemStack) + '" must be an ECMAScript simplified ISO 8601 date string with optional time and time zone components'); } break; case 'date': if (typeof itemValue !== 'string' || !timeModule.isIso8601DateString(itemValue)) { validationErrors.push('item "' + buildItemPath(itemStack) + '" must be an ECMAScript simplified ISO 8601 date string with no time or time zone components'); } break; case 'time': if (typeof itemValue !== 'string' || !timeModule.isIso8601TimeString(itemValue)) { validationErrors.push('item "' + buildItemPath(itemStack) + '" must be an ECMAScript simplified ISO 8601 time string with no date or time zone components'); } break; case 'timezone': if (typeof itemValue !== 'string' || !timeModule.isIso8601TimeZoneString(itemValue)) { validationErrors.push('item "' + buildItemPath(itemStack) + '" must be an ECMAScript simplified ISO 8601 time zone string'); } break; case 'enum': var enumPredefinedValues = resolveItemConstraint(validator.predefinedValues); if (!Array.isArray(enumPredefinedValues)) { validationErrors.push('item "' + buildItemPath(itemStack) + '" belongs to an enum that has no predefined values'); } else if (enumPredefinedValues.indexOf(itemValue) < 0) { validationErrors.push('item "' + buildItemPath(itemStack) + '" must be one of the predefined values: ' + enumPredefinedValues.join(',')); } break; case 'uuid': if (!isValueAUuid(itemValue)) { validationErrors.push('item "' + buildItemPath(itemStack) + '" must be a UUID string'); } break; case 'object': var childPropertyValidators = resolveItemConstraint(validator.propertyValidators); if (typeof itemValue !== 'object' || Array.isArray(itemValue)) { validationErrors.push('item "' + buildItemPath(itemStack) + '" must be an object'); } else if (childPropertyValidators) { validateObjectProperties(childPropertyValidators, resolveItemConstraint(validator.allowUnknownProperties)); } break; case 'array': validateArray(resolveItemConstraint(validator.arrayElementsValidator)); break; case 'hashtable': validateHashtable(validator); break; case 'attachmentReference': storeOptionalValidationErrors(attachmentsValidationModule.validateAttachmentReference(doc, validator, itemStack)); break; default: // This is not a document validation error; the item validator is configured incorrectly and must be fixed throw new Error('No data type defined for validator of item "' + buildItemPath(itemStack) + '"'); } } else if (resolveItemConstraint(validator.required)) { // The item has no value (either it's null or undefined), but the validator indicates it is required validationErrors.push('item "' + buildItemPath(itemStack) + '" must not be null or missing'); } else if (resolveItemConstraint(validator.mustNotBeMissing)) { // The item is missing (i.e. it's undefined), but the validator indicates it must not be validationErrors.push('item "' + buildItemPath(itemStack) + '" must not be missing'); } else if (resolveItemConstraint(validator.mustNotBeNull)) { // The item is null, but the validator indicates it must not be validationErrors.push('item "' + buildItemPath(itemStack) + '" must not be null'); } } function storeOptionalValidationErrors(errorMessages) { if (typeof errorMessages === 'string') { validationErrors.push(errorMessages); } else if (Array.isArray(errorMessages)) { for (var errorMessageIndex = 0; errorMessageIndex < errorMessages.length; errorMessageIndex++) { validationErrors.push(errorMessages[errorMessageIndex]); } } } function validateString(validator) { var currentItemEntry = itemStack[itemStack.length - 1]; var itemValue = currentItemEntry.itemValue; var regexPattern = resolveItemConstraint(validator.regexPattern); if (regexPattern && !regexPattern.test(itemValue)) { validationErrors.push('item "' + buildItemPath(itemStack) + '" must conform to expected format ' + regexPattern); } var mustBeTrimmed = resolveItemConstraint(validator.mustBeTrimmed); if (mustBeTrimmed && isStringUntrimmed(itemValue)) { validationErrors.push('item "' + buildItemPath(itemStack) + '" must not have any leading or trailing whitespace'); } var mustEqualIgnoreCase = resolveItemConstraint(validator.mustEqualIgnoreCase); if (!utils.isValueNullOrUndefined(mustEqualIgnoreCase) && (itemValue.toLowerCase() !== mustEqualIgnoreCase.toLowerCase())) { validationErrors.push('value of item "' + buildItemPath(itemStack) + '" must equal (case insensitive) "' + mustEqualIgnoreCase + '"'); } } function validateArray(elementValidator) { var currentItemEntry = itemStack[itemStack.length - 1]; var itemValue = currentItemEntry.itemValue; var oldItemValue = currentItemEntry.oldItemValue; if (!Array.isArray(itemValue)) { validationErrors.push('item "' + buildItemPath(itemStack) + '" must be an array'); } else if (elementValidator) { // Validate each element in the array for (var elementIndex = 0; elementIndex < itemValue.length; elementIndex++) { var elementName = '[' + elementIndex + ']'; var elementValue = itemValue[elementIndex]; var oldElementValue = (!utils.isValueNullOrUndefined(oldItemValue) && elementIndex < oldItemValue.length) ? oldItemValue[elementIndex] : null; itemStack.push({ itemName: elementName, itemValue: elementValue, oldItemValue: oldElementValue }); validateItemValue(elementValidator); itemStack.pop(); } } } function validateHashtable(validator) { var keyValidator = resolveItemConstraint(validator.hashtableKeysValidator); var valueValidator = resolveItemConstraint(validator.hashtableValuesValidator); var currentItemEntry = itemStack[itemStack.length - 1]; var itemValue = currentItemEntry.itemValue; var oldItemValue = currentItemEntry.oldItemValue; var hashtablePath = buildItemPath(itemStack); if (typeof itemValue !== 'object' || Array.isArray(itemValue)) { validationErrors.push('item "' + buildItemPath(itemStack) + '" must be an object/hashtable'); } else { var size = 0; for (var elementKey in itemValue) { size++; var elementValue = itemValue[elementKey]; var elementName = '[' + elementKey + ']'; if (keyValidator) { var fullKeyPath = hashtablePath ? hashtablePath + elementName : elementName; if (typeof elementKey !== 'string') { validationErrors.push('hashtable key "' + fullKeyPath + '" is not a string'); } else { if (resolveItemConstraint(keyValidator.mustNotBeEmpty) && elementKey.length < 1) { validationErrors.push('hashtable "' + buildItemPath(itemStack) + '" must not have an empty key'); } var regexPattern = resolveItemConstraint(keyValidator.regexPattern); if (regexPattern && !regexPattern.test(elementKey)) { validationErrors.push('hashtable key "' + fullKeyPath + '" must conform to expected format ' + regexPattern); } } } if (valueValidator) { var oldElementValue; if (!utils.isValueNullOrUndefined(oldItemValue)) { oldElementValue = oldItemValue[elementKey]; } itemStack.push({ itemName: elementName, itemValue: elementValue, oldItemValue: oldElementValue }); validateItemValue(valueValidator); itemStack.pop(); } } var maximumSize = resolveItemConstraint(validator.maximumSize); if (!utils.isValueNullOrUndefined(maximumSize) && size > maximumSize) { validationErrors.push('hashtable "' + hashtablePath + '" must not be larger than ' + maximumSize + ' elements'); } var minimumSize = resolveItemConstraint(validator.minimumSize); if (!utils.isValueNullOrUndefined(minimumSize) && size < minimumSize) { validationErrors.push('hashtable "' + hashtablePath + '" must not be smaller than ' + minimumSize + ' elements'); } } } function performConditionalValidation(validator) { var currentItemEntry = itemStack[itemStack.length - 1]; // Copy all but the last element so that the item's parent is at the top of the stack for condition functions var conditionalValidationItemStack = itemStack.slice(0, -1); var resolvedOldDoc = utils.resolveOldDoc(oldDoc); var validationCandidates = resolveItemConstraint(validator.validationCandidates) || [ ]; for (var candidateIndex = 0; candidateIndex < validationCandidates.length; candidateIndex++) { var candidate = validationCandidates[candidateIndex]; if (typeof candidate.condition === 'function' && candidate.condition(doc, resolvedOldDoc, currentItemEntry, conditionalValidationItemStack)) { // Create a new validator that merges the universal constraints from the conditional validator and the more // specific constraints from the candidate validator var combinedValidator = assignProperties({ }, [ validator, candidate.validator ], [ 'validationCandidates' ]); return validateItemValue(combinedValidator); } } // If we got here, then none of the candidate validator conditions were satisfied if (shouldSkipItemValidation(validator, resolveItemConstraint('conditional'))) { return; } else if (utils.isValueNullOrUndefined(currentItemEntry.itemValue)) { // Ensure that a null/missing value does not violate any of the universal constraints specified by the // conditional validator (e.g. required, immutable) var nullValidator = assignProperties({ }, [ validator, { type: 'any' } ], [ 'validationCandidates' ]); validateItemValue(nullValidator); } else { validationErrors.push('item "' + buildItemPath(itemStack) + '" does not satisfy any candidate validation conditions'); } } function performCustomValidation(validator) { var currentItemEntry = itemStack[itemStack.length - 1]; // Copy all but the last/top element so that the item's parent is at the top of the stack for the custom validation function var customValidationItemStack = itemStack.slice(0, -1); var customValidationErrors = validator.customValidation(doc, oldDoc, currentItemEntry, customValidationItemStack); if (Array.isArray(customValidationErrors)) { for (var errorIndex = 0; errorIndex < customValidationErrors.length; errorIndex++) { validationErrors.push(customValidationErrors[errorIndex]); } } } function resolveItemConstraint(constraintDefinition) { if (typeof constraintDefinition === 'function') { var currentItemEntry = itemStack[itemStack.length - 1]; return constraintDefinition( doc, utils.resolveOldDoc(oldDoc), currentItemEntry.itemValue, currentItemEntry.oldItemValue); } else { return constraintDefinition; } } function shouldSkipItemValidation(validator, validatorType) { var currentItemEntry = itemStack[itemStack.length - 1]; var itemValue = currentItemEntry.itemValue; var oldItemValue = currentItemEntry.oldItemValue; if (utils.isDocumentMissingOrDeleted(oldDoc)) { // Can't skip validation when creating a new document return false; } else if (utils.isDocumentMissingOrDeleted(doc)) { // No need to validate when deleting an existing document return true; } else if (resolveItemConstraint(validator.skipValidationWhenValueUnchanged) && comparisonModule.checkItemEquality(itemValue, oldItemValue, validatorType)) { // Old and new values are semantically equal and the item is allowed to skip validation return true; } else if (resolveItemConstraint(validator.skipValidationWhenValueUnchangedStrict) && comparisonModule.checkItemEquality(itemValue, oldItemValue)) { // Old and new values are strictly equal and the item is allowed skip validation return true; } else { return false; } } } }; function isValueAUuid(value) { var regex = /^[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}$/; return regex.test(value); } function isStringUntrimmed(value) { if (utils.isValueNullOrUndefined(value)) { return false; } else { return value !== value.trim(); } } // Constructs the fully qualified path of the item at the top of the given stack function buildItemPath(itemStack) { var nameComponents = [ ]; for (var i = 0; i < itemStack.length; i++) { var itemName = itemStack[i].itemName; if (!itemName) { // Skip null or empty names (e.g. the first element is typically the root of the document, which has no name) continue; } else if (nameComponents.length < 1 || itemName.indexOf('[') === 0) { nameComponents.push(itemName); } else { nameComponents.push('.' + itemName); } } return nameComponents.join(''); } function assignProperties(target, sources, skipPropertyNames) { var actualSkipPropertyNames = skipPropertyNames || [ ]; for (var sourceIndex = 0; sourceIndex < sources.length; sourceIndex++) { var source = sources[sourceIndex]; for (var propertyName in source) { if (source.hasOwnProperty(propertyName) && actualSkipPropertyNames.indexOf(propertyName) < 0) { target[propertyName] = source[propertyName]; } } } return target; } }