Skip to content

Commit

Permalink
Merge pull request #35 from OldSneerJaw/issue-33-unchanged-values
Browse files Browse the repository at this point in the history
Issue 33: Allow validation to be skipped when an item has a semantically equal value
  • Loading branch information
dkichler authored Jul 6, 2018
2 parents 4f4fb08 + d4709e0 commit 9ead68b
Show file tree
Hide file tree
Showing 8 changed files with 513 additions and 2 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
This project adheres to [Semantic Versioning](http://semver.org/). All notable changes will be documented in this file.

## [Unreleased]
Nothing yet.
### Added
- [#33](https://github.com/OldSneerJaw/couchster/issues/33): Option to ignore item validation errors when value is unchanged

## [1.1.0] - 2018-06-04
### Added
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,7 @@ Validation for all simple and complex data types support the following additiona
* `immutableWhenSetStrict`: As with the `immutableWhenSet` constraint, the item cannot be changed if it already has a value. However, it differs in that specialized string validation types (e.g. `date`, `datetime`, `time`, `timezone`, `uuid`) are not compared semantically; for example, the two `date` values of "2018" and "2018-01-01" are _not_ considered equal because the strings are not strictly equal. Defaults to `false`.
* `mustEqual`: The value of the item must be equal to the specified value. Useful in cases where the item's value should be computed from other properties of the document (e.g. a reference ID that is encoded into the document's ID or a number that is the result of some calculation performed on other properties in the document). For that reason, this constraint is perhaps most useful when specified as a dynamic constraint (e.g. `mustEqual: function(newDoc, oldDoc, value, oldValue) { ... }`) rather than as a static value (e.g. `mustEqual: 'foobar'`). If this constraint is set to `null`, then only values of `null` or missing/`undefined` will be accepted for the corresponding property or element. Differs from `mustEqualStrict` in that it checks for semantic equality of specialized string validation types (e.g. `date`, `datetime`, `time`, `timezone`, `uuid`); for example, the two `datetime` values of "2018-02-12T11:02:00.000-08:00" and "2018-02-12T11:02-0800" are considered equal with this constraint since they represent the same point in time. No constraint by default.
* `mustEqualStrict`: The value of the property or element must be strictly equal to the specified value. Differs from `mustEqual` in that specialized string validation types (e.g. `date`, `datetime`, `time`, `timezone`, `uuid`) are not compared semantically; for example, the two `timezone` values of "Z" and "+00:00" are _not_ considered equal because the strings are not strictly equal. No constraint by default.
* `skipValidationWhenValueUnchanged`: When set to `true`, the property or element is not validated if the document is being replaced and its value is equal to the same property or element value from the previous document revision. Useful if a change that is not backward compatible must be introduced to a property/element validator and existing values from documents that are already stored in the database should be preserved as they are. Defaults to `false`.
* `customValidation`: A function that accepts as parameters (1) the new document, (2) the old document that is being replaced/deleted (if any), (3) an object that contains metadata about the current item to validate, (4) a stack of the items (e.g. object properties, array elements, hashtable element values) that have gone through validation, where the last/top element contains metadata for the direct parent of the item currently being validated and the first/bottom element is metadata for the root (i.e. the document itself), (5) the CouchDB [user context](http://docs.couchdb.org/en/latest/json-structure.html#userctx-object) of the authenticated user (or `null` if the request is not authenticated), and (6) the CouchDB [security object](http://docs.couchdb.org/en/latest/json-structure.html#security-object) for the database. In cases where the document is in the process of being deleted, the first parameter's `_deleted` property will be `true` and, if the document does not yet exist or it was deleted, the second parameter will be `null`. Generally, custom validation should not throw exceptions; it's recommended to return an array/list of error descriptions so the validation function can compile a list of all validation errors that were encountered once full validation is complete. A return value of `null`, `undefined` or an empty array indicate there were no validation errors. An example:

```
Expand Down
4 changes: 3 additions & 1 deletion src/validation/document-definitions-validator.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ describe('Document definitions validator:', () => {
unrecognizedConstraint: true, // Invalid property constraint
propertyValidators: {
_validName: {
type: 'boolean'
type: 'boolean',
skipValidationWhenValueUnchanged: 1 // Must be a boolean
},
dateProperty: {
type: 'date',
Expand Down Expand Up @@ -233,6 +234,7 @@ describe('Document definitions validator:', () => {
'myDoc1.propertyValidators.timezoneProperty.maximumValueExclusive: \"maximumValueExclusive\" conflict with forbidden peer \"mustEqual\"',
'myDoc1.propertyValidators._invalidName: "_invalidName" is not allowed',
'myDoc1.propertyValidators.nestedObject.unrecognizedConstraint: "unrecognizedConstraint" is not allowed',
'myDoc1.propertyValidators.nestedObject.propertyValidators._validName.skipValidationWhenValueUnchanged: \"skipValidationWhenValueUnchanged\" must be a boolean',
'myDoc1.propertyValidators.nestedObject.propertyValidators.dateProperty.immutableWhenSet: \"immutableWhenSet\" conflict with forbidden peer \"immutable\"',
'myDoc1.propertyValidators.nestedObject.propertyValidators.dateProperty.immutable: \"immutable\" conflict with forbidden peer \"immutableWhenSet\"',
'myDoc1.propertyValidators.nestedObject.propertyValidators.dateProperty.maximumValue: "maximumValue" with value "2018-01-31T17:31:27.283-08:00" fails to match the required pattern: /^([+-]\\d{6}|\\d{4})(-(0[1-9]|1[0-2])(-(0[1-9]|[12]\\d|3[01]))?)?$/',
Expand Down
1 change: 1 addition & 0 deletions src/validation/property-validator-schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ function universalConstraintSchemas(typeEqualitySchema) {
immutableWhenSetStrict: dynamicConstraintSchema(joi.boolean()),
mustEqual: dynamicConstraintSchema(mustEqualConstraintSchema(typeEqualitySchema)),
mustEqualStrict: dynamicConstraintSchema(mustEqualConstraintSchema(typeEqualitySchema)),
skipValidationWhenValueUnchanged: dynamicConstraintSchema(joi.boolean()),
customValidation: joi.func().maxArity(6) // Function parameters: doc, oldDoc, currentItemEntry, validationItemStack, userContext, securityInfo
};
}
Expand Down
1 change: 1 addition & 0 deletions templates/validation-function/comparison-module.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
function comparisonModule(utils, buildItemPath, timeModule) {
return {
checkItemEquality: checkItemEquality,
validateMinValueInclusiveConstraint: validateMinValueInclusiveConstraint,
validateMaxValueInclusiveConstraint: validateMaxValueInclusiveConstraint,
validateMinValueExclusiveConstraint: validateMinValueExclusiveConstraint,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,14 @@ function documentPropertiesValidationModule(utils, simpleTypeFilter, typeIdValid
var itemValue = currentItemEntry.itemValue;
var validatorType = resolveItemConstraint(validator.type);

if (!utils.isDocumentMissingOrDeleted(oldDoc) &&
resolveItemConstraint(validator.skipValidationWhenValueUnchanged) &&
comparisonModule.checkItemEquality(itemValue, currentItemEntry.oldItemValue, validatorType)) {
// No need to perform further validation since the validator is configured to skip validation when the current
// value and old value are semantically equal to each other
return;
}

if (validator.customValidation) {
performCustomValidation(validator);
}
Expand Down
114 changes: 114 additions & 0 deletions test/resources/skip-validation-when-value-unchanged-doc-definitions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
function() {
return {
staticSkipValidationWhenValueUnchangedDoc: {
typeFilter: simpleTypeFilter,
authorizedRoles: { write: 'write' },
propertyValidators: {
integerProp: {
type: 'integer',
skipValidationWhenValueUnchanged: true,
minimumValue: 0
},
floatProp: {
type: 'float',
skipValidationWhenValueUnchanged: true,
maximumValue: 0
},
stringProp: {
type: 'string',
skipValidationWhenValueUnchanged: true,
minimumLength: 4
},
booleanProp: {
type: 'boolean',
skipValidationWhenValueUnchanged: true,
customValidation: function(doc, oldDoc, currentItemEntry) {
if (isValueNullOrUndefined(currentItemEntry.itemValue) || currentItemEntry.itemValue) {
return [ ];
} else {
return [ currentItemEntry.itemName + ' must be true' ];
}
}
},
dateProp: {
type: 'date',
skipValidationWhenValueUnchanged: true,
maximumValue: '1953-01-14'
},
datetimeProp: {
type: 'datetime',
skipValidationWhenValueUnchanged: true,
minimumValue: '2018-06-13T23:33+00:00',
maximumValue: '2018-06-13T23:33Z'
},
timeProp: {
type: 'time',
skipValidationWhenValueUnchanged: true,
minimumValueExclusive: '17:45:53.911'
},
timezoneProp: {
type: 'timezone',
skipValidationWhenValueUnchanged: true,
maximumValueExclusive: '+15:30'
},
enumProp: {
type: 'enum',
predefinedValues: [ 1, 2, 3 ],
skipValidationWhenValueUnchanged: true
},
uuidProp: {
type: 'uuid',
skipValidationWhenValueUnchanged: true,
maximumValueExclusive: '10000000-0000-0000-0000-000000000000'
},
attachmentReferenceProp: {
type: 'attachmentReference',
skipValidationWhenValueUnchanged: true,
regexPattern: /^[a-z]+\.txt$/
},
arrayProp: {
type: 'array',
skipValidationWhenValueUnchanged: true,
maximumLength: 3
},
objectProp: {
type: 'object',
skipValidationWhenValueUnchanged: true,
propertyValidators: {
nestedProp: {
type: 'string'
}
}
},
hashtableProp: {
type: 'hashtable',
skipValidationWhenValueUnchanged: true,
hashtableValuesValidator: {
type: 'integer'
}
}
}
},
dynamicSkipValidationWhenValueUnchangedDoc: {
typeFilter: simpleTypeFilter,
authorizedRoles: { write: 'write' },
propertyValidators: {
allowValidationSkip: {
type: 'boolean'
},
minimumUuidValue: {
type: 'uuid'
},
uuidProp: {
type: 'uuid',
skipValidationWhenValueUnchanged: function(doc, oldDoc, value, oldValue) {
return !isDocumentMissingOrDeleted(oldDoc) ? oldDoc.allowValidationSkip : doc.allowValidationSkip;
},
minimumValue: function(doc, oldDoc, value, oldValue) {
return doc.minimumUuidValue;
}
}
}
}
};
}
Loading

0 comments on commit 9ead68b

Please sign in to comment.