From d20f35e8760ac9c06c24e3ebfbe296269ebdf12c Mon Sep 17 00:00:00 2001 From: Joel Date: Sat, 7 Jul 2018 17:01:57 -0700 Subject: [PATCH] Issue #324: Support for any JSON data type A property or element with the `any` validation type can have a value from any of the standard JSON data types: number, string, boolean, array or object. As noted in the related issue, universal validation type constraints are supported by the `any` type, but type-specific constraints (e.g. `string`'s `mustBeTrimmed` constraint or `object`'s `allowUnknownProperties` constraint) are not. --- CHANGELOG.md | 1 + README.md | 1 + .../document-definitions-validator.spec.js | 11 +- src/validation/property-validator-schema.js | 5 + .../document-properties-validation-module.js | 3 + test/any-type.spec.js | 142 ++++++++++++++++++ test/resources/any-type-doc-definitions.js | 27 ++++ 7 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 test/any-type.spec.js create mode 100644 test/resources/any-type-doc-definitions.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 4375d647..00a4f88f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). All notable c ## [Unreleased] ### 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 ## [2.5.0] - 2018-05-30 ### Added diff --git a/README.md b/README.md index 3b8ba2e5..8bb6087b 100644 --- a/README.md +++ b/README.md @@ -473,6 +473,7 @@ 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 diff --git a/src/validation/document-definitions-validator.spec.js b/src/validation/document-definitions-validator.spec.js index 6bc33189..a5a8e0bf 100644 --- a/src/validation/document-definitions-validator.spec.js +++ b/src/validation/document-definitions-validator.spec.js @@ -222,6 +222,12 @@ describe('Document definitions validator:', () => { type: 'object', mustEqual: (a, b, c, d) => d, propertyValidators: { } // Must specify at least one property validator + }, + anyProperty: { + type: 'any', + minimumValue: 32, // Not supported by the "any" type + mustNotBeEmpty: false, // Not supported by the "any" type + regexPattern: /^foo$/ // Not supported by the "any" type } } } @@ -310,7 +316,10 @@ describe('Document definitions validator:', () => { 'myDoc1.propertyValidators.nestedObject.propertyValidators.arrayProperty.arrayElementsValidator.propertyValidators.invalidMustEqualConstraintProperty.mustEqual: \"mustEqual\" must be an object', 'myDoc1.propertyValidators.nestedObject.propertyValidators.arrayProperty.arrayElementsValidator.propertyValidators.emptyPropertyValidatorsProperty.propertyValidators: \"propertyValidators\" must have at least 1 children', 'myDoc1.propertyValidators.nestedObject.propertyValidators.arrayProperty.arrayElementsValidator.propertyValidators.noTypeProperty.type: "type" is required', - 'myDoc1.propertyValidators.nestedObject.propertyValidators.unrecognizedTypeProperty.type: "type" must be one of [array, attachmentReference, boolean, date, datetime, enum, float, hashtable, integer, object, string, time, timezone, uuid]', + '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.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)$/', ]); }); diff --git a/src/validation/property-validator-schema.js b/src/validation/property-validator-schema.js index b2beb795..ec6ec725 100644 --- a/src/validation/property-validator-schema.js +++ b/src/validation/property-validator-schema.js @@ -30,6 +30,7 @@ const timeOnlySchema = joi.string().regex(/^((([01]\d|2[0-3])(:[0-5]\d)(:[0-5]\d const timezoneSchema = joi.string().regex(/^(Z|([+-])([01]\d|2[0-3]):([0-5]\d))$/); const typeEqualitySchemas = { + any: joi.any(), string: joi.string(), integer: integerSchema, float: joi.number(), @@ -51,6 +52,9 @@ const validPropertyTypes = Object.keys(typeEqualitySchemas).sort(); const schema = joi.object().keys({ type: dynamicConstraintSchema(joi.string().only(validPropertyTypes)).required() }) + .when( + joi.object().unknown().keys({ type: 'any' }), + { then: makeTypeConstraintsSchema('any') }) .when( joi.object().unknown().keys({ type: 'string' }), { then: makeTypeConstraintsSchema('string') }) @@ -107,6 +111,7 @@ module.exports = exports = schema; // references between the complex types (e.g. "array", "object", "hashtable") and the main "propertyValidators" schema function typeSpecificConstraintSchemas() { return { + any: { }, string: { mustNotBeEmpty: dynamicConstraintSchema(joi.boolean()), mustBeTrimmed: dynamicConstraintSchema(joi.boolean()), diff --git a/templates/sync-function/document-properties-validation-module.js b/templates/sync-function/document-properties-validation-module.js index 945d8a7c..d6dc22e4 100644 --- a/templates/sync-function/document-properties-validation-module.js +++ b/templates/sync-function/document-properties-validation-module.js @@ -179,6 +179,9 @@ function documentPropertiesValidationModule(utils, simpleTypeFilter, typeIdValid } 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'); diff --git a/test/any-type.spec.js b/test/any-type.spec.js new file mode 100644 index 00000000..ff3d9928 --- /dev/null +++ b/test/any-type.spec.js @@ -0,0 +1,142 @@ +const testFixtureMaker = require('../src/testing/test-fixture-maker'); +const errorFormatter = require('../src/testing/validation-error-formatter'); + +describe('Any validation type:', () => { + const testFixture = testFixtureMaker.initFromSyncFunction('build/sync-functions/test-any-type-sync-function.js'); + + afterEach(() => { + testFixture.resetTestEnvironment(); + }); + + describe('for array elements', () => { + it('allows string, number, boolean, array and object values in an array', () => { + const doc = { + _id: 'my-doc', + type: 'anyTypeDoc', + arrayProp: [ + 'a-string', + -117.8, + true, + [ 'foo', 'bar' ], + { baz: 'qux' } + ] + }; + + testFixture.verifyDocumentCreated(doc); + }); + + it('respects universal constraints (e.g. "required")', () => { + const doc = { + _id: 'my-doc', + type: 'anyTypeDoc', + arrayProp: [ + '', + 0, + null + ] + }; + + testFixture.verifyDocumentNotCreated(doc, 'anyTypeDoc', errorFormatter.requiredValueViolation('arrayProp[2]')); + }); + }); + + describe('for hashtable elements', () => { + it('allows string, number, boolean, array and object values in a hashtable', () => { + const doc = { + _id: 'my-doc', + type: 'anyTypeDoc', + hashtableProp: { + 1: 'another-string', + 2: 13, + 3: false, + 4: [ 0, 1, 2 ], + 5: { } + } + }; + + testFixture.verifyDocumentCreated(doc); + }); + + it('respects universal constraints (e.g. "immutableWhenSet")', () => { + const oldDoc = { + _id: 'my-doc', + type: 'anyTypeDoc', + hashtableProp: { + 1: 1.9, + 2: true, + 3: 'one-more-string', + 4: null // Can be changed since it doesn't have a value yet + } + }; + + const doc = { + _id: 'my-doc', + type: 'anyTypeDoc', + hashtableProp: { + 1: 85, // Changed + 2: true, + 3: 'one-more-string', + 4: [ ] + } + }; + + testFixture.verifyDocumentNotReplaced( + doc, + oldDoc, + 'anyTypeDoc', + errorFormatter.immutableItemViolation('hashtableProp[1]')); + }); + }); + + describe('for object properties', () => { + it('allows a string value', () => { + const doc = { + _id: 'my-doc', + type: 'anyTypeDoc', + anyProp: 'a-string' + }; + + testFixture.verifyDocumentCreated(doc); + }); + + it('allows a numeric value', () => { + const doc = { + _id: 'my-doc', + type: 'anyTypeDoc', + anyProp: -115.8 + }; + + testFixture.verifyDocumentCreated(doc); + }); + + it('allows a boolean value', () => { + const doc = { + _id: 'my-doc', + type: 'anyTypeDoc', + anyProp: false + }; + + testFixture.verifyDocumentCreated(doc); + }); + + it('allows an array value', () => { + const doc = { + _id: 'my-doc', + type: 'anyTypeDoc', + anyProp: [ 'foo', 'bar' ] + }; + + testFixture.verifyDocumentCreated(doc); + }); + + it('allows an object value', () => { + const doc = { + _id: 'my-doc', + type: 'anyTypeDoc', + anyProp: { foo: 'bar' } + }; + + testFixture.verifyDocumentCreated(doc); + }); + }); +}); diff --git a/test/resources/any-type-doc-definitions.js b/test/resources/any-type-doc-definitions.js new file mode 100644 index 00000000..fff8d80a --- /dev/null +++ b/test/resources/any-type-doc-definitions.js @@ -0,0 +1,27 @@ +function() { + return { + anyTypeDoc: { + typeFilter: simpleTypeFilter, + channels: { write: 'write' }, + propertyValidators: { + arrayProp: { + type: 'array', + arrayElementsValidator: { + type: 'any', + required: true + } + }, + hashtableProp: { + type: 'hashtable', + hashtableValuesValidator: { + type: 'any', + immutableWhenSet: true + } + }, + anyProp: { + type: 'any' + } + } + } + }; +}