From 969a1c03e2176b469151eebbb5102e228f9bb36a Mon Sep 17 00:00:00 2001 From: Joel Date: Mon, 22 Jan 2018 17:32:23 -0800 Subject: [PATCH] Issue #176: Add UUID validation type A data type represented by strings that are formatted as universally unique identifiers. --- CHANGELOG.md | 1 + README.md | 1 + samples/fragment-notification.js | 5 ++ ...ocument-definition-properties-validator.js | 2 + src/validation-error-formatter.js | 9 +++ templates/sync-function-validation-module.js | 11 +++ test/resources/uuid-doc-definitions.js | 11 +++ test/sample-notification.spec.js | 13 +++- test/uuid.spec.js | 68 +++++++++++++++++++ 9 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 test/resources/uuid-doc-definitions.js create mode 100644 test/uuid.spec.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b3fea7b..9d3e4d5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - [#108](https://github.com/Kashoo/synctos/issues/108): Finer grained control over whether null and missing values are accepted - [#127](https://github.com/Kashoo/synctos/issues/127): Immutable constraints that treat null and missing values as different - [#128](https://github.com/Kashoo/synctos/issues/128): Equality constraint that treats null and missing values as different +- [#176](https://github.com/Kashoo/synctos/issues/176): UUID data validation type ### Changed - [#118](https://github.com/Kashoo/synctos/issues/118): Embed indent.js as a static dependency diff --git a/README.md b/README.md index 4ca3de1b..36a5b605 100644 --- a/README.md +++ b/README.md @@ -347,6 +347,7 @@ Validation for simple data types (e.g. integers, floating point numbers, strings * `maximumValueExclusive`: Reject dates that are greater than or equal to this. May be either an ISO 8601 date string without time and time zone components OR a JavaScript `Date` object. No restriction by default. * `enum`: The value must be one of the specified predefined string and/or integer values. Additional parameters: * `predefinedValues`: A list of strings and/or integers that are to be accepted. If this parameter is omitted from an `enum` property's configuration, that property will not accept a value of any kind. For example: `[ 1, 2, 3, 'a', 'b', 'c' ]` +* `uuid`: The value must be a string representation of a [universally unique identifier](https://en.wikipedia.org/wiki/Universally_unique_identifier) (UUID). A UUID may contain either uppercase or lowercase letters so that, for example, both "1511fba4-e039-42cc-9ac2-9f2fa29eecfc" and "DFF421EA-0AB2-45C9-989C-12C76E7282B8" are valid. * `attachmentReference`: The value is the name of one of the document's file attachments. Note that, because the addition of an attachment is often a separate Sync Gateway API operation from the creation/replacement of the associated document, this validation type is only applied if the attachment is actually present in the document. However, since the sync function is run twice in such situations (i.e. once when the _document_ is created/replaced and once when the _attachment_ is created/replaced), the validation will be performed eventually. The top-level `allowAttachments` property should be `true` so that documents of this type can actually store attachments. Additional parameters: * `supportedExtensions`: An array of case-insensitive file extensions that are allowed for the attachment's filename (e.g. "txt", "jpg", "pdf"). Takes precedence over the document-wide `supportedExtensions` constraint for the referenced attachment. No restriction by default. * `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. diff --git a/samples/fragment-notification.js b/samples/fragment-notification.js index b502dcf8..68c850d3 100644 --- a/samples/fragment-notification.js +++ b/samples/fragment-notification.js @@ -28,6 +28,11 @@ } ], propertyValidators: { + eventId: { + type: 'uuid', + required: true, + immutable: true + }, sender: { // Which Kashoo app/service generated the notification type: 'string', diff --git a/src/document-definition-properties-validator.js b/src/document-definition-properties-validator.js index 41b709f4..7f06e137 100644 --- a/src/document-definition-properties-validator.js +++ b/src/document-definition-properties-validator.js @@ -54,6 +54,8 @@ function validate(docDefinition, docPropertyValidatorDefinitions) { break; case 'enum': break; + case 'uuid': + break; case 'attachmentReference': break; case 'array': diff --git a/src/validation-error-formatter.js b/src/validation-error-formatter.js index 73d49043..79e81a81 100644 --- a/src/validation-error-formatter.js +++ b/src/validation-error-formatter.js @@ -359,6 +359,15 @@ exports.unsupportedProperty = function(propertyPath) { return 'property "' + propertyPath + '" is not supported'; }; +/** + * Formats a message for the error that occurs when the format for a UUID is invalid. + * + * @param {string} itemPath The full path of the property or element in which the error occurs (e.g. "objectProp.arrayProp[10].uuidProp") + */ +exports.uuidFormatInvalid = function(propertyPath) { + return 'item "' + propertyPath + '" is not a valid UUID'; +}; + function getTypeDescription(type) { switch (type) { case 'array': diff --git a/templates/sync-function-validation-module.js b/templates/sync-function-validation-module.js index 6eb0cea0..15f5a7cc 100644 --- a/templates/sync-function-validation-module.js +++ b/templates/sync-function-validation-module.js @@ -18,6 +18,12 @@ function() { return regex.test(value); } + function isUuid(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); + } + // A regular expression that matches one of the given file extensions function buildSupportedExtensionsRegex(extensions) { // Note that this regex uses double quotes rather than single quotes as a workaround to https://github.com/Kashoo/synctos/issues/116 @@ -297,6 +303,11 @@ function() { validationErrors.push('item "' + buildItemPath(itemStack) + '" must be one of the predefined values: ' + enumPredefinedValues.toString()); } break; + case 'uuid': + if (!isUuid(itemValue)) { + validationErrors.push('item "' + buildItemPath(itemStack) + '" is not a valid UUID'); + } + break; case 'object': var childPropertyValidators = resolveValidationConstraint(validator.propertyValidators); if (typeof itemValue !== 'object' || itemValue instanceof Array) { diff --git a/test/resources/uuid-doc-definitions.js b/test/resources/uuid-doc-definitions.js new file mode 100644 index 00000000..62acee8f --- /dev/null +++ b/test/resources/uuid-doc-definitions.js @@ -0,0 +1,11 @@ +{ + myDocType: { + typeFilter: simpleTypeFilter, + channels: { write: 'write' }, + propertyValidators: { + uuidProp: { + type: 'uuid' + } + } + } +} diff --git a/test/sample-notification.spec.js b/test/sample-notification.spec.js index afad4b2a..af2ff077 100644 --- a/test/sample-notification.spec.js +++ b/test/sample-notification.spec.js @@ -62,6 +62,7 @@ describe('Sample business notification doc definition', function() { it('successfully creates a valid notification document', function() { var doc = { _id: 'biz.63.notification.5', + eventId: '082979cf-6990-44a6-bb62-9b9517c3052b', sender: 'test-service', type: 'invoice-payments', subject: 'pay up!', @@ -77,6 +78,7 @@ describe('Sample business notification doc definition', function() { it('cannot create a notification document when the properties are invalid', function() { var doc = { _id: 'biz.13.notification.5', + eventId: 'not-a-uuid', type: true , subject: '', // missing sender, empty subject 'whatsthis?': 'something I dont recognize!', // unrecognized property @@ -98,13 +100,15 @@ describe('Sample business notification doc definition', function() { errorFormatter.requiredValueViolation('actions[0].label'), errorFormatter.requiredValueViolation('actions[1]'), errorFormatter.unsupportedProperty('whatsthis?'), - errorFormatter.datetimeFormatInvalid('firstReadAt') + errorFormatter.datetimeFormatInvalid('firstReadAt'), + errorFormatter.uuidFormatInvalid('eventId') ]); }); it('successfully replaces a valid notification document', function() { var doc = { _id: 'biz.7.notification.3', + eventId: '1d856cd8-a0db-473c-9ea0-20b3113e2571', type: 'invoice-payments', sender: 'test-service', subject: 'a different subject', @@ -116,6 +120,7 @@ describe('Sample business notification doc definition', function() { }; var oldDoc = { _id: 'biz.7.notification.3', + eventId: '1d856cd8-a0db-473c-9ea0-20b3113e2571', type: 'invoice-payments', sender: 'test-service', subject: 'a different subject', @@ -131,6 +136,7 @@ describe('Sample business notification doc definition', function() { it('cannot replace a notification document when the properties are invalid', function() { var doc = { _id: 'biz.10.notification.3', + eventId: '692d8c84-8ff2-358-b806-edbf4c3c5813', sender: '', // missing type, empty sender message: '', // missing subject, empty message createdAt: '2016-04-29T17:13:43.666Z', // changed createdAt @@ -139,6 +145,7 @@ describe('Sample business notification doc definition', function() { }; var oldDoc = { // valid oldDoc _id: 'biz.10.notification.3', + eventId: '692d8c84-8ff2-358-b806-edbf4c3c5813', type: 'invoice-payments', sender: 'test-service', subject: 'a different subject', @@ -165,13 +172,15 @@ describe('Sample business notification doc definition', function() { errorFormatter.immutableItemViolation('actions'), errorFormatter.requiredValueViolation('actions[0].url'), errorFormatter.mustNotBeEmptyViolation('actions[0].label'), - errorFormatter.immutableItemViolation('firstReadAt') + errorFormatter.immutableItemViolation('firstReadAt'), + errorFormatter.uuidFormatInvalid('eventId') ]); }); it('successfully deletes a valid notification document', function() { var oldDoc = { _id: 'biz.71.notification.5', + eventId: '56be8a52-f050-4d72-b4cb-c4f6eb2ca3ed', type: 'invoice-payments', sender: 'test-service', subject: 'pay up!', diff --git a/test/uuid.spec.js b/test/uuid.spec.js new file mode 100644 index 00000000..3249ee3d --- /dev/null +++ b/test/uuid.spec.js @@ -0,0 +1,68 @@ +var testHelper = require('../src/test-helper.js'); +var errorFormatter = testHelper.validationErrorFormatter; + +describe('UUID validation type', function() { + beforeEach(function() { + testHelper.initSyncFunction('build/sync-functions/test-uuid-sync-function.js'); + }); + + it('allows a valid UUID with lowercase letters', function() { + var doc = { + _id: 'my-doc', + type: 'myDocType', + uuidProp: '1511fba4-e039-42cc-9ac2-9f2fa29eecfc' + }; + + testHelper.verifyDocumentCreated(doc); + }); + + it('allows a valid UUID with uppercase letters', function() { + var doc = { + _id: 'my-doc', + type: 'myDocType', + uuidProp: 'DFF421EA-0AB2-45C9-989C-12C76E7282B8' + }; + + testHelper.verifyDocumentCreated(doc); + }); + + it('rejects a UUID with invalid characters', function() { + var doc = { + _id: 'my-doc', + type: 'myDocType', + uuidProp: 'g78d516e-cb95-4ef7-b593-2ee7ce375738' + }; + + testHelper.verifyDocumentNotCreated(doc, 'myDocType', [ errorFormatter.uuidFormatInvalid('uuidProp') ]); + }); + + it('rejects a UUID without hyphens', function() { + var doc = { + _id: 'my-doc', + type: 'myDocType', + uuidProp: '1511fba4e03942cc9ac29f2fa29eecfc' + }; + + testHelper.verifyDocumentNotCreated(doc, 'myDocType', [ errorFormatter.uuidFormatInvalid('uuidProp') ]); + }); + + it('rejects a UUID with too many characters', function() { + var doc = { + _id: 'my-doc', + type: 'myDocType', + uuidProp: '1511fba4-e039-42cc-9ac2-9f2fa29eecfc3' + }; + + testHelper.verifyDocumentNotCreated(doc, 'myDocType', [ errorFormatter.uuidFormatInvalid('uuidProp') ]); + }); + + it('rejects a UUID with too few characters', function() { + var doc = { + _id: 'my-doc', + type: 'myDocType', + uuidProp: '1511fba4-e03-42cc-9ac2-9f2fa29eecfc' + }; + + testHelper.verifyDocumentNotCreated(doc, 'myDocType', [ errorFormatter.uuidFormatInvalid('uuidProp') ]); + }); +});