Skip to content

Commit

Permalink
Merge pull request #328 from Kashoo/86-conditional-type
Browse files Browse the repository at this point in the history
Issue 86: Implementation of conditional validation type
  • Loading branch information
dkichler authored Aug 16, 2018
2 parents 63d7835 + 82577f7 commit b32bccc
Show file tree
Hide file tree
Showing 13 changed files with 798 additions and 34 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). All notable c
### Added
- [#323](https://github.com/Kashoo/synctos/issues/323): Option to ignore item validation errors when value is unchanged
- [#324](https://github.com/Kashoo/synctos/issues/324): Validation type that accepts any type of value
- [#86](https://github.com/Kashoo/synctos/issues/86): Conditional validation type

## [2.5.0] - 2018-05-30
### Added
Expand Down
65 changes: 61 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ For validation of documents in Apache CouchDB, see the [couchster](https://githu
- [Content validation](#content-validation)
- [Simple type validation](#simple-type-validation)
- [Complex type validation](#complex-type-validation)
- [Universal constraint validation](#universal-constraint-validation)
- [Multi-type validation](#multi-type-validation)
- [Universal validation constraints](#universal-validation-constraints)
- [Predefined validators](#predefined-validators)
- [Dynamic constraint validation](#dynamic-constraint-validation)
- [Definition file](#definition-file)
Expand Down Expand Up @@ -473,7 +474,6 @@ Validation for simple data types (e.g. integers, floating point numbers, strings
* `supportedContentTypes`: An array of content/MIME types that are allowed for the attachment's contents (e.g. "image/png", "text/html", "application/xml"). Takes precedence over the document-wide `supportedContentTypes` constraint for the referenced attachment. No restriction by default.
* `maximumSize`: The maximum file size, in bytes, of the attachment. May not be greater than 20MB (20,971,520 bytes), as Couchbase Server/Sync Gateway sets that as the hard limit per document or attachment. Takes precedence over the document-wide `maximumIndividualSize` constraint for the referenced attachment. Unlimited by default.
* `regexPattern`: A regular expression pattern that must be satisfied by the value. Takes precedence over the document-wide `attachmentConstraints.filenameRegexPattern` constraint for the referenced attachment. No restriction by default.
* `any`: The value may be any JSON data type: number, string, boolean, array or object. No additional parameters.

##### Complex type validation

Expand Down Expand Up @@ -543,7 +543,64 @@ myHash1: {
}
```

##### Universal constraint validation
##### Multi-type validation

These validation types support more than a single data type:

* `any`: The value may be any JSON data type: number, string, boolean, array or object. No additional parameters.
* `conditional`: The value must match any one of some number of candidate validators. Each validator is accompanied by a condition that determines whether that validator should be applied to the value. Additional parameters:
* `validationCandidates`: A list of candidates to act as the property or element's validator if their conditions are satisfied. Each condition is defined as a function that returns a boolean and accepts as parameters (1) the new document, (2) the old document that is being replaced/deleted (if any; it will be `null` if it has been deleted or does not exist), (3) an object that contains metadata about the current item to validate and (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). Conditions are tested in the order they are defined; if two or more candidates' conditions would evaluate to `true`, only the first candidate's validator will be applied to the property or element value. When a matching validation candidate declares the same constraint as the containing `conditional` validator, the candidate validator's constraint takes precedence. An example:

```javascript
entries: {
type: 'hashtable',
hashtableValuesValidator: {
type: 'object',
required: true,
propertyValidators: {
entryType: {
type: 'enum',
required: true,
predefinedValues: [ 'name', 'codes' ]
},
entryValue: {
type: 'conditional',
required: true,
validationCandidates: [
{
condition: function(doc, oldDoc, currentItemEntry, validationItemStack) {
var parentEntry = validationItemStack[validationItemStack.length - 1];

return parentEntry.itemValue.entryType === 'name';
},
validator: {
type: 'string',
mustNotBeEmpty: true
}
},
{
condition: function(doc, oldDoc, currentItemEntry, validationItemStack) {
var parentEntry = validationItemStack[validationItemStack.length - 1];

return parentEntry.itemValue.entryType === 'codes';
},
validator: {
type: 'array',
arrayElementsValidator: {
type: 'integer',
required: true,
minimumValue: 1
}
}
}
]
}
}
}
}
```

##### Universal validation constraints

Validation for all simple and complex data types support the following additional parameters:

Expand All @@ -558,7 +615,7 @@ Validation for all simple and complex data types support the following additiona
* `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 _semantically_ 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. Differs from `skipValidationWhenValueUnchangedStrict` in that it checks for semantic equality of specialized string validation types (e.g. `date`, `datetime`, `time`, `timezone`, `uuid`); for example, the two `date` values of "2018" and "2018-01-01" are considered equal with this constraint since they represent the same date. Defaults to `false`.
* `skipValidationWhenValueUnchangedStrict`: When set to `true`, the property or element is not validated if the document is being replaced and its value is _strictly_ 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. Differs from `skipValidationWhenValueUnchanged` in that specialized string validation types (e.g. `date`, `datetime`, `time`, `timezone`, `uuid`) are not compared semantically; for example, the two `datetime` values of "2018-06-23T14:30:00.000Z" and "2018-06-23T14:30+00:00" are _not_ considered equal because the strings are not strictly equal. 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 and (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). In cases where the document is in the process of being deleted, the first parameter's `_deleted` property will be `true`, so be sure to account for such cases. If the document does not yet exist, the second parameter will be `null`. And, in some cases where the document previously existed (i.e. it was deleted), the second parameter _may_ be non-null and its `_deleted` property will be `true`. Generally, custom validation should not throw exceptions; it's recommended to return an array/list of error descriptions so the sync 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:
* `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 and (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). If the document does not yet exist, the second parameter will be `null`. And, in some cases where the document previously existed (i.e. it was deleted), the second parameter _may_ be non-null and its `_deleted` property will be `true`. Generally, custom validation should not throw exceptions; it's recommended to return an array/list of error descriptions so the sync 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:

```
propertyValidators: {
Expand Down
46 changes: 33 additions & 13 deletions samples/fragment-notification.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,22 +101,42 @@
type: 'array',
immutable: true,
arrayElementsValidator: {
type: 'object',
type: 'conditional',
required: true,
propertyValidators: {
url: {
// The URL of the action
type: 'string',
required: true,
mustNotBeEmpty: true
validationCandidates: [
{
condition: function(doc, oldDoc, currentItemEntry, validationItemStack) {
return typeof currentItemEntry.itemValue === 'object';
},
validator: {
type: 'object',
propertyValidators: {
url: {
// The URL of the action
type: 'string',
required: true,
mustNotBeEmpty: true
},
label: {
// A plain text label for the action
type: 'string',
required: true,
mustNotBeEmpty: true
}
}
}
},
label: {
// A plain text label for the action
type: 'string',
required: true,
mustNotBeEmpty: true
{
condition: function(doc, oldDoc, currentItemEntry, validationItemStack) {
return typeof currentItemEntry.itemValue === 'string';
},
validator: {
// The URL of the action
type: 'string',
mustNotBeEmpty: true
}
}
}
]
}
}
}
Expand Down
36 changes: 28 additions & 8 deletions samples/fragment-notifications-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,36 @@
// The list of notification transports that are enabled for the notification type
type: 'array',
arrayElementsValidator: {
type: 'object',
type: 'conditional',
required: true,
propertyValidators: {
transportId: {
// The ID of the notification transport
type: 'string',
required: true,
mustNotBeEmpty: true
validationCandidates: [
{
condition: function(doc, oldDoc, currentItemEntry, validationItemStack) {
return typeof currentItemEntry.itemValue === 'object';
},
validator: {
type: 'object',
propertyValidators: {
transportId: {
// The ID of the notification transport
type: 'string',
required: true,
mustNotBeEmpty: true
}
}
}
},
{
condition: function(doc, oldDoc, currentItemEntry, validationItemStack) {
return typeof currentItemEntry.itemValue === 'string';
},
validator: {
// The ID of the notification transport
type: 'string',
mustNotBeEmpty: true
}
}
}
]
}
}
}
Expand Down
9 changes: 9 additions & 0 deletions src/testing/validation-error-formatter.js
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,15 @@ exports.unsupportedProperty = (propertyPath) => `property "${propertyPath}" is n
*/
exports.uuidFormatInvalid = (itemPath) => `item "${itemPath}" must be ${getTypeDescription('uuid')}`;

/**
* Formats a message for the error that occurs when a value does not satisfy any of the candidate validators for
* conditional validation.
*
* @param {string} itemPath The full path of the property or element in which the error occurs (e.g. "hashtableProp[my-key]")
*/
exports.validationConditionsViolation =
(itemPath) => `item "${itemPath}" does not satisfy any candidate validation conditions`;

function getTypeDescription(type) {
switch (type) {
case 'array':
Expand Down
5 changes: 5 additions & 0 deletions src/testing/validation-error-formatter.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,11 @@ describe('Validation error formatter', () => {
expect(errorFormatter.uuidFormatInvalid(fakeItemPath)).to.equal(`item "${fakeItemPath}" must be a UUID string`);
});

it('produces validation conditions violation messages', () => {
expect(errorFormatter.validationConditionsViolation(fakeItemPath))
.to.equal(`item "${fakeItemPath}" does not satisfy any candidate validation conditions`);
});

describe('type constraint violations', () => {
it('formats messages for general types', () => {
const typeDescriptions = {
Expand Down
35 changes: 34 additions & 1 deletion src/validation/document-definitions-validator.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,32 @@ describe('Document definitions validator:', () => {
accessAssignments: (a, b, extraParam) => extraParam, // Too many parameters
customActions: { }, // Must have at least one property
propertyValidators: {
conditionalTypeProperty: {
type: 'conditional',
immutableWhenSetStrict: true,
minimumValue: -15, // Unsupported constraint for this validation type
validationCandidates: [
{
condition: (a, b, c, d, extra) => extra, // Too many parameters and must have a "validator" property
foobar: 'baz' // Unsupported property
},
{
condition: true, // Must be a function
validator: {
type: 'float',
maximumLength: 3, // Unsupported constraint for this validation type
mustEqual: (a, b, c, d) => d
}
},
{
condition: () => true,
validator: {
type: 'object',
allowUnknownProperties: 0 // Must be a boolean
}
}
]
},
timeProperty: {
type: 'time',
immutable: 1, // Must be a boolean
Expand Down Expand Up @@ -261,6 +287,13 @@ describe('Document definitions validator:', () => {
'myDoc1.attachmentConstraints.filenameRegexPattern: \"filenameRegexPattern\" must be an instance of \"RegExp\"',
'myDoc1.accessAssignments: \"accessAssignments\" must have an arity lesser or equal to 2',
'myDoc1.customActions: \"customActions\" must have at least 1 children',
'myDoc1.propertyValidators.conditionalTypeProperty.minimumValue: \"minimumValue\" is not allowed',
'myDoc1.propertyValidators.conditionalTypeProperty.validationCandidates.0.condition: \"condition\" must have an arity lesser or equal to 4',
'myDoc1.propertyValidators.conditionalTypeProperty.validationCandidates.0.foobar: \"foobar\" is not allowed',
'myDoc1.propertyValidators.conditionalTypeProperty.validationCandidates.0.validator: \"validator\" is required',
'myDoc1.propertyValidators.conditionalTypeProperty.validationCandidates.1.condition: \"condition\" must be a Function',
'myDoc1.propertyValidators.conditionalTypeProperty.validationCandidates.1.validator.maximumLength: \"maximumLength\" is not allowed',
'myDoc1.propertyValidators.conditionalTypeProperty.validationCandidates.2.validator.allowUnknownProperties: \"allowUnknownProperties\" must be a boolean',
'myDoc1.propertyValidators.timeProperty.immutable: \"immutable\" must be a boolean',
'myDoc1.propertyValidators.timeProperty.minimumValue: \"minimumValue\" with value \"15\" fails to match the required pattern: /^((([01]\\d|2[0-3])(:[0-5]\\d)(:[0-5]\\d(\\.\\d{1,3})?)?)|(24:00(:00(\\.0{1,3})?)?))$/',
'myDoc1.propertyValidators.timeProperty.maximumValue: \"maximumValue\" with value \"23:49:52.1234\" fails to match the required pattern: /^((([01]\\d|2[0-3])(:[0-5]\\d)(:[0-5]\\d(\\.\\d{1,3})?)?)|(24:00(:00(\\.0{1,3})?)?))$/',
Expand Down Expand Up @@ -319,7 +352,7 @@ describe('Document definitions validator:', () => {
'myDoc1.propertyValidators.nestedObject.propertyValidators.arrayProperty.arrayElementsValidator.propertyValidators.anyProperty.minimumValue: \"minimumValue\" is not allowed',
'myDoc1.propertyValidators.nestedObject.propertyValidators.arrayProperty.arrayElementsValidator.propertyValidators.anyProperty.mustNotBeEmpty: \"mustNotBeEmpty\" is not allowed',
'myDoc1.propertyValidators.nestedObject.propertyValidators.arrayProperty.arrayElementsValidator.propertyValidators.anyProperty.regexPattern: \"regexPattern\" is not allowed',
'myDoc1.propertyValidators.nestedObject.propertyValidators.unrecognizedTypeProperty.type: \"type\" must be one of [any, array, attachmentReference, boolean, date, datetime, enum, float, hashtable, integer, object, string, time, timezone, uuid]',
'myDoc1.propertyValidators.nestedObject.propertyValidators.unrecognizedTypeProperty.type: \"type\" must be one of [any, array, attachmentReference, boolean, conditional, date, datetime, enum, float, hashtable, integer, object, string, time, timezone, uuid]',
'myDoc1.expiry: \"expiry\" with value \"20180415T1357-0700\" fails to match the required pattern: /^\\d{4}-(((0[13578]|1[02])-(0[1-9]|[12]\\d|3[01]))|((0[469]|11)-(0[1-9]|[12]\\d|30))|(02-(0[1-9]|[12]\\d)))T([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(Z|[+-]([01]\\d|2[0-3]):[0-5]\\d)$/',
]);
});
Expand Down
Loading

0 comments on commit b32bccc

Please sign in to comment.