Skip to content

Commit b32bccc

Browse files
authored
Merge pull request #328 from Kashoo/86-conditional-type
Issue 86: Implementation of conditional validation type
2 parents 63d7835 + 82577f7 commit b32bccc

13 files changed

+798
-34
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). All notable c
55
### Added
66
- [#323](https://github.com/Kashoo/synctos/issues/323): Option to ignore item validation errors when value is unchanged
77
- [#324](https://github.com/Kashoo/synctos/issues/324): Validation type that accepts any type of value
8+
- [#86](https://github.com/Kashoo/synctos/issues/86): Conditional validation type
89

910
## [2.5.0] - 2018-05-30
1011
### Added

README.md

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ For validation of documents in Apache CouchDB, see the [couchster](https://githu
2828
- [Content validation](#content-validation)
2929
- [Simple type validation](#simple-type-validation)
3030
- [Complex type validation](#complex-type-validation)
31-
- [Universal constraint validation](#universal-constraint-validation)
31+
- [Multi-type validation](#multi-type-validation)
32+
- [Universal validation constraints](#universal-validation-constraints)
3233
- [Predefined validators](#predefined-validators)
3334
- [Dynamic constraint validation](#dynamic-constraint-validation)
3435
- [Definition file](#definition-file)
@@ -473,7 +474,6 @@ Validation for simple data types (e.g. integers, floating point numbers, strings
473474
* `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.
474475
* `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.
475476
* `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.
476-
* `any`: The value may be any JSON data type: number, string, boolean, array or object. No additional parameters.
477477

478478
##### Complex type validation
479479

@@ -543,7 +543,64 @@ myHash1: {
543543
}
544544
```
545545

546-
##### Universal constraint validation
546+
##### Multi-type validation
547+
548+
These validation types support more than a single data type:
549+
550+
* `any`: The value may be any JSON data type: number, string, boolean, array or object. No additional parameters.
551+
* `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:
552+
* `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:
553+
554+
```javascript
555+
entries: {
556+
type: 'hashtable',
557+
hashtableValuesValidator: {
558+
type: 'object',
559+
required: true,
560+
propertyValidators: {
561+
entryType: {
562+
type: 'enum',
563+
required: true,
564+
predefinedValues: [ 'name', 'codes' ]
565+
},
566+
entryValue: {
567+
type: 'conditional',
568+
required: true,
569+
validationCandidates: [
570+
{
571+
condition: function(doc, oldDoc, currentItemEntry, validationItemStack) {
572+
var parentEntry = validationItemStack[validationItemStack.length - 1];
573+
574+
return parentEntry.itemValue.entryType === 'name';
575+
},
576+
validator: {
577+
type: 'string',
578+
mustNotBeEmpty: true
579+
}
580+
},
581+
{
582+
condition: function(doc, oldDoc, currentItemEntry, validationItemStack) {
583+
var parentEntry = validationItemStack[validationItemStack.length - 1];
584+
585+
return parentEntry.itemValue.entryType === 'codes';
586+
},
587+
validator: {
588+
type: 'array',
589+
arrayElementsValidator: {
590+
type: 'integer',
591+
required: true,
592+
minimumValue: 1
593+
}
594+
}
595+
}
596+
]
597+
}
598+
}
599+
}
600+
}
601+
```
602+
603+
##### Universal validation constraints
547604

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

@@ -558,7 +615,7 @@ Validation for all simple and complex data types support the following additiona
558615
* `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.
559616
* `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`.
560617
* `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`.
561-
* `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:
618+
* `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:
562619

563620
```
564621
propertyValidators: {

samples/fragment-notification.js

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -101,22 +101,42 @@
101101
type: 'array',
102102
immutable: true,
103103
arrayElementsValidator: {
104-
type: 'object',
104+
type: 'conditional',
105105
required: true,
106-
propertyValidators: {
107-
url: {
108-
// The URL of the action
109-
type: 'string',
110-
required: true,
111-
mustNotBeEmpty: true
106+
validationCandidates: [
107+
{
108+
condition: function(doc, oldDoc, currentItemEntry, validationItemStack) {
109+
return typeof currentItemEntry.itemValue === 'object';
110+
},
111+
validator: {
112+
type: 'object',
113+
propertyValidators: {
114+
url: {
115+
// The URL of the action
116+
type: 'string',
117+
required: true,
118+
mustNotBeEmpty: true
119+
},
120+
label: {
121+
// A plain text label for the action
122+
type: 'string',
123+
required: true,
124+
mustNotBeEmpty: true
125+
}
126+
}
127+
}
112128
},
113-
label: {
114-
// A plain text label for the action
115-
type: 'string',
116-
required: true,
117-
mustNotBeEmpty: true
129+
{
130+
condition: function(doc, oldDoc, currentItemEntry, validationItemStack) {
131+
return typeof currentItemEntry.itemValue === 'string';
132+
},
133+
validator: {
134+
// The URL of the action
135+
type: 'string',
136+
mustNotBeEmpty: true
137+
}
118138
}
119-
}
139+
]
120140
}
121141
}
122142
}

samples/fragment-notifications-config.js

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,36 @@
2222
// The list of notification transports that are enabled for the notification type
2323
type: 'array',
2424
arrayElementsValidator: {
25-
type: 'object',
25+
type: 'conditional',
2626
required: true,
27-
propertyValidators: {
28-
transportId: {
29-
// The ID of the notification transport
30-
type: 'string',
31-
required: true,
32-
mustNotBeEmpty: true
27+
validationCandidates: [
28+
{
29+
condition: function(doc, oldDoc, currentItemEntry, validationItemStack) {
30+
return typeof currentItemEntry.itemValue === 'object';
31+
},
32+
validator: {
33+
type: 'object',
34+
propertyValidators: {
35+
transportId: {
36+
// The ID of the notification transport
37+
type: 'string',
38+
required: true,
39+
mustNotBeEmpty: true
40+
}
41+
}
42+
}
43+
},
44+
{
45+
condition: function(doc, oldDoc, currentItemEntry, validationItemStack) {
46+
return typeof currentItemEntry.itemValue === 'string';
47+
},
48+
validator: {
49+
// The ID of the notification transport
50+
type: 'string',
51+
mustNotBeEmpty: true
52+
}
3353
}
34-
}
54+
]
3555
}
3656
}
3757
}

src/testing/validation-error-formatter.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,15 @@ exports.unsupportedProperty = (propertyPath) => `property "${propertyPath}" is n
369369
*/
370370
exports.uuidFormatInvalid = (itemPath) => `item "${itemPath}" must be ${getTypeDescription('uuid')}`;
371371

372+
/**
373+
* Formats a message for the error that occurs when a value does not satisfy any of the candidate validators for
374+
* conditional validation.
375+
*
376+
* @param {string} itemPath The full path of the property or element in which the error occurs (e.g. "hashtableProp[my-key]")
377+
*/
378+
exports.validationConditionsViolation =
379+
(itemPath) => `item "${itemPath}" does not satisfy any candidate validation conditions`;
380+
372381
function getTypeDescription(type) {
373382
switch (type) {
374383
case 'array':

src/testing/validation-error-formatter.spec.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,11 @@ describe('Validation error formatter', () => {
239239
expect(errorFormatter.uuidFormatInvalid(fakeItemPath)).to.equal(`item "${fakeItemPath}" must be a UUID string`);
240240
});
241241

242+
it('produces validation conditions violation messages', () => {
243+
expect(errorFormatter.validationConditionsViolation(fakeItemPath))
244+
.to.equal(`item "${fakeItemPath}" does not satisfy any candidate validation conditions`);
245+
});
246+
242247
describe('type constraint violations', () => {
243248
it('formats messages for general types', () => {
244249
const typeDescriptions = {

src/validation/document-definitions-validator.spec.js

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,32 @@ describe('Document definitions validator:', () => {
9797
accessAssignments: (a, b, extraParam) => extraParam, // Too many parameters
9898
customActions: { }, // Must have at least one property
9999
propertyValidators: {
100+
conditionalTypeProperty: {
101+
type: 'conditional',
102+
immutableWhenSetStrict: true,
103+
minimumValue: -15, // Unsupported constraint for this validation type
104+
validationCandidates: [
105+
{
106+
condition: (a, b, c, d, extra) => extra, // Too many parameters and must have a "validator" property
107+
foobar: 'baz' // Unsupported property
108+
},
109+
{
110+
condition: true, // Must be a function
111+
validator: {
112+
type: 'float',
113+
maximumLength: 3, // Unsupported constraint for this validation type
114+
mustEqual: (a, b, c, d) => d
115+
}
116+
},
117+
{
118+
condition: () => true,
119+
validator: {
120+
type: 'object',
121+
allowUnknownProperties: 0 // Must be a boolean
122+
}
123+
}
124+
]
125+
},
100126
timeProperty: {
101127
type: 'time',
102128
immutable: 1, // Must be a boolean
@@ -261,6 +287,13 @@ describe('Document definitions validator:', () => {
261287
'myDoc1.attachmentConstraints.filenameRegexPattern: \"filenameRegexPattern\" must be an instance of \"RegExp\"',
262288
'myDoc1.accessAssignments: \"accessAssignments\" must have an arity lesser or equal to 2',
263289
'myDoc1.customActions: \"customActions\" must have at least 1 children',
290+
'myDoc1.propertyValidators.conditionalTypeProperty.minimumValue: \"minimumValue\" is not allowed',
291+
'myDoc1.propertyValidators.conditionalTypeProperty.validationCandidates.0.condition: \"condition\" must have an arity lesser or equal to 4',
292+
'myDoc1.propertyValidators.conditionalTypeProperty.validationCandidates.0.foobar: \"foobar\" is not allowed',
293+
'myDoc1.propertyValidators.conditionalTypeProperty.validationCandidates.0.validator: \"validator\" is required',
294+
'myDoc1.propertyValidators.conditionalTypeProperty.validationCandidates.1.condition: \"condition\" must be a Function',
295+
'myDoc1.propertyValidators.conditionalTypeProperty.validationCandidates.1.validator.maximumLength: \"maximumLength\" is not allowed',
296+
'myDoc1.propertyValidators.conditionalTypeProperty.validationCandidates.2.validator.allowUnknownProperties: \"allowUnknownProperties\" must be a boolean',
264297
'myDoc1.propertyValidators.timeProperty.immutable: \"immutable\" must be a boolean',
265298
'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})?)?))$/',
266299
'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})?)?))$/',
@@ -319,7 +352,7 @@ describe('Document definitions validator:', () => {
319352
'myDoc1.propertyValidators.nestedObject.propertyValidators.arrayProperty.arrayElementsValidator.propertyValidators.anyProperty.minimumValue: \"minimumValue\" is not allowed',
320353
'myDoc1.propertyValidators.nestedObject.propertyValidators.arrayProperty.arrayElementsValidator.propertyValidators.anyProperty.mustNotBeEmpty: \"mustNotBeEmpty\" is not allowed',
321354
'myDoc1.propertyValidators.nestedObject.propertyValidators.arrayProperty.arrayElementsValidator.propertyValidators.anyProperty.regexPattern: \"regexPattern\" is not allowed',
322-
'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]',
355+
'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]',
323356
'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)$/',
324357
]);
325358
});

0 commit comments

Comments
 (0)