Skip to content

Commit

Permalink
Merge pull request #92 from Kashoo/issue-90-attachment-constraints
Browse files Browse the repository at this point in the history
Issue 90: Additional attachment constraints
  • Loading branch information
dkichler authored Mar 21, 2017
2 parents d928783 + fe26c5e commit 3393c30
Show file tree
Hide file tree
Showing 8 changed files with 449 additions and 93 deletions.
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,9 @@ An example of a mix of channel and role access assignments:
* `maximumAttachmentCount`: (optional) The maximum number of attachments that may be assigned to a single document of this type. Unlimited by default.
* `maximumIndividualSize`: (optional) The maximum file size, in bytes, allowed for any single attachment assigned to a document of this type. 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. Unlimited by default.
* `maximumTotalSize`: (optional) The maximum total size, in bytes, of _all_ attachments assigned to a single document of this type. In other words, when the sizes of all of a document's attachments are added together, it must not exceed this value. Unlimited by default.
* `supportedExtensions`: (optional) An array of case-insensitive file extensions that are allowed for an attachment's filename (e.g. "txt", "jpg", "pdf"). No restriction by default.
* `supportedContentTypes`: (optional) An array of content/MIME types that are allowed for an attachment's contents (e.g. "image/png", "text/html", "application/xml"). No restriction by default.
* `requireAttachmentReferences`: (optional) Whether every one of a document's attachments must have a corresponding `attachmentReference`-type property referencing it. Defaults to `false`.
* `allowUnknownProperties`: (optional) Whether to allow the existence of properties that are not explicitly declared in the document type definition. Not applied recursively to objects that are nested within documents of this type. Defaults to `false`.
* `immutable`: (optional) The document cannot be replaced or deleted after it is created. Note that, even if attachments are allowed for this document type (see the `allowAttachments` parameter for more info), it will not be possible to create, modify or delete attachments in a document that already exists, which means that they must be created inline in the document's `_attachments` property when the document is first created. Defaults to `false`.
* `cannotReplace`: (optional) As with the `immutable` constraint, the document cannot be replaced after it is created. However, this constraint does not prevent the document from being deleted. Note that, even if attachments are allowed for this document type (see the `allowAttachments` parameter for more info), it will not be possible to create, modify or delete attachments in a document that already exists, which means that they must be created inline in the document's `_attachments` property when the document is first created. Defaults to `false`.
Expand Down Expand Up @@ -309,9 +312,9 @@ Validation for simple data types:
* `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.
* `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"). 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"). 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-level `maximumIndividualSize` constraint. Unlimited by default.
* `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-level `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-level `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-level `maximumIndividualSize` constraint for the referenced attachment. Unlimited by default.

Validation for complex data types, which allow for nesting of child properties and elements:

Expand Down
51 changes: 43 additions & 8 deletions etc/sync-function-validation-module.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ function() {
return regex.test(value);
}

function buildSupportedExtensionsRegex(extensions) {
return new RegExp('\\.(' + extensions.join('|') + ')$', 'i');
}

// Constructs the fully qualified path of the item at the top of the given stack
function buildItemPath(itemStack) {
var nameComponents = [ ];
Expand All @@ -43,7 +47,7 @@ function() {

validateDocImmutability(doc, oldDoc, docDefinition, validationErrors);

// Only validate the document's contents if it's being created or replaced. But there's no need if it's being deleted.
// Only validate the document's contents if it's being created or replaced. There's no need if it's being deleted.
if (!doc._deleted) {
validateDocContents(
doc,
Expand Down Expand Up @@ -486,7 +490,7 @@ function() {
attachmentReferenceValidators[itemValue] = validator;

if (validator.supportedExtensions) {
var extRegex = new RegExp('\\.(' + validator.supportedExtensions.join('|') + ')$', 'i');
var extRegex = buildSupportedExtensionsRegex(validator.supportedExtensions);
if (!extRegex.test(itemValue)) {
validationErrors.push('attachment reference "' + buildItemPath(itemStack) + '" must have a supported file extension (' + validator.supportedExtensions.join(',') + ')');
}
Expand All @@ -512,26 +516,57 @@ function() {
}

function validateAttachments() {
var maximumIndividualAttachmentSize =
docDefinition.attachmentConstraints ? docDefinition.attachmentConstraints.maximumIndividualSize : null;
var maximumTotalAttachmentSize = docDefinition.attachmentConstraints ? docDefinition.attachmentConstraints.maximumTotalSize : null;
var maximumAttachmentCount = docDefinition.attachmentConstraints ? docDefinition.attachmentConstraints.maximumAttachmentCount : null;
var attachmentConstraints = docDefinition.attachmentConstraints;

var maximumAttachmentCount = attachmentConstraints ? attachmentConstraints.maximumAttachmentCount : null;
var maximumIndividualAttachmentSize = attachmentConstraints ? attachmentConstraints.maximumIndividualSize : null;
var maximumTotalAttachmentSize = attachmentConstraints ? attachmentConstraints.maximumTotalSize : null;

var supportedExtensions = attachmentConstraints ? attachmentConstraints.supportedExtensions : null;
var supportedExtensionsRegex = supportedExtensions ? buildSupportedExtensionsRegex(supportedExtensions) : null;

var supportedContentTypes = attachmentConstraints ? attachmentConstraints.supportedContentTypes : null;

var requireAttachmentReferences = attachmentConstraints ? attachmentConstraints.requireAttachmentReferences : false;

var totalSize = 0;
var attachmentCount = 0;
for (var attachmentName in doc._attachments) {
attachmentCount++;

var attachmentSize = doc._attachments[attachmentName].length;
var attachment = doc._attachments[attachmentName];

var attachmentSize = attachment.length;
totalSize += attachmentSize;

var attachmentRefValidator = attachmentReferenceValidators[attachmentName];

if (requireAttachmentReferences && isValueNullOrUndefined(attachmentRefValidator)) {
validationErrors.push('attachment ' + attachmentName + ' must have a corresponding attachment reference property');
}

if (isInteger(maximumIndividualAttachmentSize) && attachmentSize > maximumIndividualAttachmentSize) {
// If this attachment is owned by an attachment reference property, that property's size constraint (if any) takes precedence
var attachmentRefValidator = attachmentReferenceValidators[attachmentName];
if (isValueNullOrUndefined(attachmentRefValidator) || !isInteger(attachmentRefValidator.maximumSize)) {
validationErrors.push('attachment ' + attachmentName + ' must not exceed ' + maximumIndividualAttachmentSize + ' bytes');
}
}

if (supportedExtensionsRegex && !supportedExtensionsRegex.test(attachmentName)) {
// If this attachment is owned by an attachment reference property, that property's extensions constraint (if any) takes
// precedence
if (isValueNullOrUndefined(attachmentRefValidator) || isValueNullOrUndefined(attachmentRefValidator.supportedExtensions)) {
validationErrors.push('attachment "' + attachmentName + '" must have a supported file extension (' + supportedExtensions.join(',') + ')');
}
}

if (supportedContentTypes && supportedContentTypes.indexOf(attachment.content_type) < 0) {
// If this attachment is owned by an attachment reference property, that property's content types constraint (if any) takes
// precedence
if (isValueNullOrUndefined(attachmentRefValidator) || isValueNullOrUndefined(attachmentRefValidator.supportedContentTypes)) {
validationErrors.push('attachment "' + attachmentName + '" must have a supported content type (' + supportedContentTypes.join(',') + ')');
}
}
}

if (isInteger(maximumTotalAttachmentSize) && totalSize > maximumTotalAttachmentSize) {
Expand Down
58 changes: 54 additions & 4 deletions etc/validation-error-message-formatter.js
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,16 @@ exports.regexPatternItemViolation = function(itemPath, expectedRegex) {
return 'item "' + itemPath + '" must conform to expected format ' + expectedRegex;
};

/**
* Formats a message for the error that occurs when a file attachment violates the constraint that all of that document type's file
* attachments must have a corresponding attachment reference property.
*
* @param {string} attachmentName The name of the attachment in question
*/
exports.requireAttachmentReferencesViolation = function(attachmentName) {
return 'attachment ' + attachmentName + ' must have a corresponding attachment reference property';
};

/**
* Formats a message for the error that occurs when a required property or element value is null or undefined.
*
Expand All @@ -235,31 +245,71 @@ exports.requiredValueViolation = function(itemPath) {
};

/**
* Formats a message for the error that occurs when a file attachment is not one of the supported content types.
* Formats a message for the error that occurs when a file attachment reference is not one of the supported content types.
*
* @param {string} itemPath The full path of the property or element in which the error occurs (e.g. "objectProp.attachmentRefProp")
* @param {string[]} expectedContentTypes An array of content types that are expected (e.g. [ 'image/png', 'image/gif', 'image/jpeg' ]).
* Element order must match that set in the validator in the document definition.
*/
exports.supportedContentTypesAttachmentViolation = function(itemPath, expectedContentTypes) {
exports.supportedContentTypesAttachmentReferenceViolation = function(itemPath, expectedContentTypes) {
var contentTypesString = expectedContentTypes.join(',');

return 'attachment reference "' + itemPath + '" must have a supported content type (' + contentTypesString + ')';
};

/**
* Formats a message for the error that occurs when a file attachment does not have one of the supported file extensions.
* DEPRECATED. Use supportedContentTypesAttachmentReferenceViolation instead.
*/
exports.supportedContentTypesAttachmentViolation = function(itemPath, expectedContentTypes) {
return exports.supportedContentTypesAttachmentReferenceViolation(itemPath, expectedContentTypes);
};

/**
* Formats a message for the error that occurs when a file attachment is not one of the supported content types.
*
* @param {string} attachmentName The name of the attachment in question
* @param {string[]} expectedContentTypes An array of content types that are expected (e.g. [ 'image/png', 'image/gif', 'image/jpeg' ]).
* Element order must match that set in the validator in the document definition.
*/
exports.supportedContentTypesRawAttachmentViolation = function(attachmentName, expectedContentTypes) {
var contentTypesString = expectedContentTypes.join(',');

return 'attachment "' + attachmentName + '" must have a supported content type (' + contentTypesString + ')';
};

/**
* Formats a message for the error that occurs when a file attachment reference does not have one of the supported file extensions.
*
* @param {string} itemPath The full path of the property or element in which the error occurs (e.g. "arrayProp[0].attachmentRefProp")
* @param {string[]} expectedFileExtensions An array of file extensions that are expected (e.g. [ 'png', 'gif', 'jpg', 'jpeg' ]).
* Element order must match that set in the validator in the document definition.
*/
exports.supportedExtensionsAttachmentViolation = function(itemPath, expectedFileExtensions) {
exports.supportedExtensionsAttachmentReferenceViolation = function(itemPath, expectedFileExtensions) {
var extensionsString = expectedFileExtensions.join(',');

return 'attachment reference "' + itemPath + '" must have a supported file extension (' + extensionsString + ')';
};

/**
* DEPRECATED. Use supportedExtensionsAttachmentReferenceViolation instead.
*/
exports.supportedExtensionsAttachmentViolation = function(itemPath, expectedFileExtensions) {
return exports.supportedExtensionsAttachmentReferenceViolation(itemPath, expectedFileExtensions);
};

/**
* Formats a message for the error that occurs when a file attachment does not have one of the supported file extensions.
*
* @param {string} attachmentName The name of the attachment in question
* @param {string[]} expectedFileExtensions An array of file extensions that are expected (e.g. [ 'png', 'gif', 'jpg', 'jpeg' ]).
* Element order must match that set in the validator in the document definition.
*/
exports.supportedExtensionsRawAttachmentViolation = function(attachmentName, expectedFileExtensions) {
var extensionsString = expectedFileExtensions.join(',');

return 'attachment "' + attachmentName + '" must have a supported file extension (' + extensionsString + ')';
};

/**
* Formats a message for the error that occurs when a property or element's type does not match what is defined by the validator.
*
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "synctos",
"version": "1.7.0",
"dependencies": {
"indent.js": "^0.1.3"
"indent.js": "^0.1.4"
},
"devDependencies": {
"expect.js": "^0.3.1",
Expand Down
8 changes: 8 additions & 0 deletions samples/fragment-business.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@
return new RegExp('^biz\\.[A-Za-z0-9_-]+$').test(doc._id);
},
allowAttachments: true,
attachmentConstraints: {
maximumAttachmentCount: 1,
maximumTotalSize: 2097664,
maximumIndividualSize: 512,
supportedExtensions: [ 'txt' ],
supportedContentTypes: [ 'text/plain' ],
requireAttachmentReferences: true
},
propertyValidators: {
businessLogoAttachment: {
// The name of the Sync Gateway file attachment that is to be used as the business/invoice logo image
Expand Down
Loading

0 comments on commit 3393c30

Please sign in to comment.