Giter VIP home page Giter VIP logo

synctos's Introduction

Introduction

Build Status npm version dependencies Status devDependencies Status

Synctos: The Syncmaker. A utility to aid with the process of designing well-structured sync functions for Couchbase Sync Gateway.

With this utility, you define all your JSON document types in a declarative JavaScript object format that eliminates much of the boilerplate normally required for sync functions with comprehensive validation of document contents and permissions. Not only is it invaluable in protecting the integrity of the documents that are stored in a Sync Gateway database, whenever a document fails validation, sync functions generated with synctos return specific, detailed error messages that make it easy for a client app developer to figure out exactly what went wrong. An included test fixture module also provides a simple framework to write unit tests for generated sync functions.

To learn more about Sync Gateway, check out Couchbase's comprehensive developer documentation. And, for a comprehensive introduction to synctos, see the post Validating your Sync Gateway documents with synctos on the official Couchbase blog.

For validation of documents in Apache CouchDB, see the couchster project.

Table of Contents

Installation

Synctos is distributed as an npm package, and the minimum version of Node.js that it officially supports is v8.9.0. Both of these required components can be acquired at once by installing Node.js.

If your project does not already have an npm package.json file, run npm init to create one. Don't worry too much about the answers to the questions it asks right now; the file it produces can be updated as needed later.

Next, to install synctos locally (i.e. in your project's node_modules directory) and to add it to your project as a development dependency automatically, run npm install synctos --save-dev from the project's root directory.

For more info on npm package management, see the official npm documentation for How to install local packages and Working with package.json.

A note on JavaScript/ECMAScript compatibility:

Sync Gateway uses the otto JavaScript engine to execute sync functions from within its Go codebase. The version of otto, and thus the version of ECMAScript, that are supported varies depending on the version of Sync Gateway:

  • Sync Gateway 1.x is pinned to commit 5282a5a of otto, which does not support any of the features introduced in ECMAScript 2015 (aka ES6/ES2015). In fact, it does not promise compatibility with ECMAScript 5, offering only that "For now, otto is a hybrid ECMA3/ECMA5 interpreter. Parts of the specification are still works in progress."
  • Sync Gateway 2.x is pinned to commit a813c59 of otto, which also does not support any of the features introduced in ECMAScript 2015, but it does at least offer full compatibility with ECMAScript 5, aside from a handful of noted RegExp incompatibilities.

It is for compatibility with these specific versions of otto that synctos intentionally generates code that does not make use of many of the conveniences of modern JavaScript (e.g. let, const, for...of loops, arrow functions, spread operators, etc.). For the same reason, it is always best to verify sync functions that are generated by synctos or otherwise within a live instance of Sync Gateway before deploying to production to ensure that your own custom code is supported by the corresponding version of the otto JS interpreter.

As a convenience, otto - and, by extension, Sync Gateway - does support the Underscore.js utility belt library (specifically version 1.4.4), which provides a great many useful functions. As such, it may help to fill in some of the gaps that would otherwise be filled by a newer version of the ECMAScript specification.

Usage

Running

Once synctos is installed, you can run it from your project's directory as follows:

node_modules/.bin/synctos /path/to/my-document-definitions.js /path/to/my-generated-sync-function.js

Or as a custom script in your project's package.json as follows:

"scripts": {
  "build": "synctos /path/to/my-document-definitions.js /path/to/my-generated-sync-function.js"
}

This will take the sync document definitions that are defined in /path/to/my-document-definitions.js and build a new sync function that is output to /path/to/my-generated-sync-function.js. The generated sync function contents can then be inserted into the definition of a bucket/database in a Sync Gateway configuration file as a multi-line string surrounded with backquotes/backticks ( ` ).

Generated sync functions are compatible with Sync Gateway 1.x and 2.x.

NOTE: Due to a known issue in Sync Gateway versions up to and including 1.2.1, when specifying a bucket/database's sync function in a configuration file as a multi-line string, you will have to be sure to escape any literal backslash characters in the sync function body. For example, if your sync function contains a regular expression like new RegExp('\\w+'), you will have to escape the backslashes when inserting the sync function into the configuration file so that it becomes new RegExp('\\\\w+'). The issue has been resolved in Sync Gateway version 1.3.0 and later.

Validating

To validate that your document definitions file is structured correctly and does not contain any obvious semantic violations, execute the built in validation script as follows:

node_modules/.bin/synctos-validate /path/to/my-document-definitions.js

Or as a custom script in your project's package.json as follows:

"scripts": {
  "validate": "synctos-validate /path/to/my-document-definitions.js"
}

If the specified document definitions contain any violations, the utility will exit with a non-zero status code and output a list of the violations to standard error (stderr). Otherwise, if validation was successful, the utility will exit normally and will not output anything.

Be aware that the validation utility cannot verify the behaviour of custom functions (e.g. dynamic constraints, custom actions, custom validation functions) in a document definition. However, the Testing section of the README describes how to write test cases for such custom code.

Specifications

Document definitions must conform to the following specification. See the samples/ directory and Kashoo's official document definitions repository for some examples.

At the top level, the document definitions object contains a property for each document type that is to be supported by the Sync Gateway bucket. For example:

{
  myDocType1: {
    channels: ...,
    typeFilter: ...,
    propertyValidators: ...
  },
  myDocType2: {
    channels: ...,
    typeFilter: ...,
    propertyValidators: ...
  }
}

Document type definitions

Each document type is defined as an object with a number of properties that control authorization, content validation and access control.

Essential document constraints

The following properties include the basics necessary to build a document definition:

  • typeFilter: (required) A function that is used to identify documents of this type. It accepts as function parameters (1) the new document, (2) the old document that is being replaced (if any) and (3) the name of the current document type. For the sake of convenience, a simple type filter function (simpleTypeFilter) is available that attempts to match the document's type property value to the document type's name (e.g. if a document definition is named "message", then a candidate document's type property value must be "message" to be considered a document of that type); if the document definition does not include an explicit type property validator, then, for convenience, the type property will be implicitly included in the document definition and validated with the built in typeIdValidator (see the validator's description for more info). NOTE: 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. And, if the old document has been deleted or simply does not exist, the second parameter will be null.

An example of the simple type filter:

typeFilter: simpleTypeFilter

And an example of a more complex custom type filter:

typeFilter: function(doc, oldDoc, currentDocType) {
  var typePropertyMatches;
  if (oldDoc) {
    if (doc._deleted) {
      typePropertyMatches = oldDoc.type === currentDocType;
    } else {
      typePropertyMatches = doc.type === oldDoc.type && oldDoc.type === currentDocType;
    }
  } else {
    // The old document does not exist or was deleted - we can rely on the new document's type
    typePropertyMatches = doc.type === currentDocType;
  }

  if (typePropertyMatches) {
    return true;
  } else {
    // The type property did not match - fall back to matching the document ID pattern
    var docIdRegex = /^message\.[A-Za-z0-9_-]+$/;

    return docIdRegex.test(doc._id);
  }
}
  • channels: (required if authorizedRoles and authorizedUsers are undefined - see the Advanced document properties section for more info) The channels to assign to documents of this type. If used in combination with the authorizedRoles and/or authorizedUsers properties, authorization will be granted if the user making the modification matches at least one of the channels and/or authorized roles/usernames for the corresponding operation type (add, replace or remove). May be specified as either a plain object or a function that returns a dynamically-constructed object and accepts as parameters (1) the new document and (2) the old document that is being replaced (if any). NOTE: In cases where the document is in the process of being deleted, the first parameter's _deleted property will be true, and if the old document has been deleted or simply does not exist, the second parameter will be null. Either way the object is specified, it may include the following properties, each of which may be either an array of channel names or a single channel name as a string:
    • view: (optional) The channel(s) that confer read-only access to documents of this type.
    • add: (required if write is undefined) The channel(s) that confer the ability to create new documents of this type. Any user with a matching channel also gains implicit read access. Use the special channel "!" to allow any authenticated user to add a document of this type.
    • replace: (required if write is undefined) The channel(s) that confer the ability to replace existing documents of this type. Any user with a matching channel also gains implicit read access. Use the special channel "!" to allow any authenticated user to replace a document of this type.
    • remove: (required if write is undefined) The channel(s) that confer the ability to delete documents of this type. Any user with a matching channel also gains implicit read access. Use the special channel "!" to allow any authenticated user to delete a document of this type.
    • write: (required if one or more of add, replace or remove are undefined) The channel(s) that confer the ability to add, replace or remove documents of this type. Exists as a convenience in cases where the add, replace and remove operations should share the same channel(s). Any user with a matching channel also gains implicit read access. Use the special channel "!" to allow any authenticated user to write a document of this type.

For example:

channels: {
  add: [ 'create', 'new' ],
  replace: 'update',
  remove: 'delete'
}

Or:

channels: function(doc, oldDoc) {
  return {
    view: doc._id + '-readonly',
    write: [ doc._id + '-edit', doc._id + '-admin' ]
  };
}
  • propertyValidators: (required) An object/hash of validators that specify the format of each of the document type's supported properties. Each entry consists of a key that specifies the property name and a value that specifies the validation to perform on that property. Each property element must declare a type and, optionally, some number of additional parameters. Any property that is not declared here will be rejected by the sync function unless the allowUnknownProperties field is set to true. In addition to a static value (e.g. propertyValidators: { ... }), this property may also be assigned a value dynamically via a function (e.g. propertyValidators: function(doc, oldDoc) { ... }) where the parameters are as follows: (1) the document (if deleted, the _deleted property will be true) and (2) the document that is being replaced (if any; it will be null if it has been deleted or does not exist).

An example static definition:

propertyValidators: {
  myProp1: {
    type: 'boolean',
    required: true
  },
  myProp2: {
    type: 'array',
    mustNotBeEmpty: true
  }
}

And a dynamic definition:

propertyValidators: function(doc, oldDoc) {
  var dynamicProp = (doc._id.indexOf('foobar') >= 0) ? { type: 'string' } : { type: 'float' }

  return {
    myDynamicProp: dynamicProp
  };
}
Advanced document constraints

Additional properties that provide finer grained control over documents:

  • 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. In addition to a static value (e.g. allowUnknownProperties: true), this property may also be assigned a value dynamically via a function (e.g. allowUnknownProperties: function(doc, oldDoc) { ... }) where the parameters are as follows: (1) the document (if deleted, the _deleted property will be true) and (2) the document that is being replaced (if any; it will be null if it has been deleted or does not exist). Defaults to false.
  • documentIdRegexPattern: (optional) A regular expression pattern that must be satisfied by the document's ID for a new document of this type to be created. Note that the constraint is not applied when a document is being replaced or deleted. In addition to a static value (e.g. documentIdRegexPattern: /^payment\.[a-zA-Z0-9_-]+$/), this constraint may also be assigned a value dynamically via a function (e.g. documentIdRegexPattern: function(doc) { ... }) where it receives a single parameter as follows: (1) the new document. No restriction by default.
  • immutable: (optional) The document cannot be replaced or deleted after it is created. Note that, when this property is enabled, 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. In addition to a static value (e.g. immutable: true), this property may also be assigned a value dynamically via a function (e.g. immutable: function(doc, oldDoc) { ... }) where the parameters are as follows: (1) the document (if deleted, the _deleted property will be true) and (2) the document that is being replaced (if any; it will be null if it has been deleted or does not exist). 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. In addition to a static value (e.g. cannotReplace: true), this property may also be assigned a value dynamically via a function (e.g. cannotReplace: function(doc, oldDoc) { ... }) where the parameters are as follows: (1) the document (if deleted, the _deleted property will be true) and (2) the document that is being replaced (if any; it will be null if it has been deleted or does not exist). Defaults to false.
  • cannotDelete: (optional) As with the immutable constraint, the document cannot be deleted after it is created. However, this constraint does not prevent the document from being replaced. In addition to a static value (e.g. cannotDelete: true), this property may also be assigned a value dynamically via a function (e.g. cannotDelete: function(doc, oldDoc) { ... }) where the parameters are as follows: (1) the document (if deleted, the _deleted property will be true) and (2) the document that is being replaced (if any; it will be null if it has been deleted or does not exist). Defaults to false.
  • authorizedRoles: (required if channels and authorizedUsers are undefined) The roles that are authorized to add, replace and remove documents of this type. If used in combination with the channels and/or authorizedUsers properties, authorization will be granted if the user making the modification matches at least one of the roles and/or authorized channels/usernames for the corresponding operation type (add, replace or remove). May be specified as either a plain object or a function that returns a dynamically-constructed object and accepts as parameters (1) the new document and (2) the old document that is being replaced (if any). NOTE: In cases where the document is in the process of being deleted, the first parameter's _deleted property will be true, and if the old document has been deleted or simply does not exist, the second parameter will be null. Either way the object is specified, it may include the following properties, each of which may be either an array of role names or a single role name as a string:
    • add: (optional) The role(s) that confer the ability to create new documents of this type.
    • replace: (optional) The role(s) that confer the ability to replace existing documents of this type.
    • remove: (optional) The role(s) that confer the ability to delete documents of this type.
    • write: (optional) The role(s) that confer the ability to add, replace or remove documents of this type. Exists as a convenience in cases where the add, replace and remove operations should share the same role(s).

For example:

authorizedRoles: {
  add: 'manager',
  replace: [ 'manager', 'employee' ],
  remove: 'manager'
}

Or:

authorizedRoles: function(doc, oldDoc) {
  return {
    write: oldDoc ? oldDoc.roles : doc.roles
  };
}
  • authorizedUsers: (required if channels and authorizedRoles are undefined) The names of users that are explicitly authorized to add, replace and remove documents of this type. If used in combination with the channels and/or authorizedRoles properties, authorization will be granted if the user making the modification matches at least one of the usernames and/or authorized channels/roles for the corresponding operation type (add, replace or remove). May be specified as either a plain object or a function that returns a dynamically-constructed object and accepts as parameters (1) the new document and (2) the old document that is being replaced (if any). NOTE: In cases where the document is in the process of being deleted, the first parameter's _deleted property will be true, and if the old document has been deleted or simply does not exist, the second parameter will be null. Either way the object is specified, it may include the following properties, each of which may be either an array of usernames or a single username as a string:
    • add: (optional) The user(s) that have the ability to create new documents of this type.
    • replace: (optional) The user(s) that have the ability to replace existing documents of this type.
    • remove: (optional) The user(s) that have the ability to delete documents of this type.
    • write: (optional) The user(s) that have the ability to add, replace or remove documents of this type. Exists as a convenience in cases where the add, replace and remove operations should share the same user(s).

For example:

authorizedUsers: {
  add: [ 'sally', 'roger', 'samantha' ],
  replace: [ 'roger', 'samantha' ],
  remove: 'samantha'
}

Or:

authorizedUsers: function(doc, oldDoc) {
  return {
    write: oldDoc ? oldDoc.users : doc.users
  };
}
  • accessAssignments: (optional) Defines either the channel access to assign to users/roles or the role access to assign to users when a document of the corresponding type is successfully created or replaced. The constraint can be defined as either a list, where each entry is an object that defines users, roles and/or channels properties, depending on the access assignment type, or it can be defined dynamically as a function that accepts the following parameters: (1) the new document and (2) the old document that is being replaced/deleted (if any). NOTE: If the old document has been deleted or simply does not exist, the second parameter will be null. When a document that included access assignments is deleted, its access assignments will be revoked. The assignment types are specified as follows:
    • Channel access assignments:
      • type: May be either "channel", null or missing/undefined.
      • channels: An array of channel names to assign to users and/or roles.
      • roles: An array of role names to which to assign the channels.
      • users: An array of usernames to which to assign the channels.
    • Role access assignments:
      • type: Must be "role".
      • roles: An array of role names to assign to users.
      • users: An array of usernames to which to assign the roles.

An example of a static mix of channel and role access assignments:

accessAssignments: [
  {
    type: 'role',
    users: [ 'user3', 'user4' ],
    roles: [ 'role1', 'role2' ]
  },
  {
    type: 'channel',
    users: [ 'user1', 'user2' ],
    channels: [ 'channel1' ]
  },
  {
    type: 'channel',
    users: function(doc, oldDoc) {
      return doc.users;
    },
    roles: function(doc, oldDoc) {
      return doc.roles;
    },
    channels: function(doc, oldDoc) {
      return [ doc._id + '-channel3', doc._id + '-channel4' ];
    }
  },
]

And an example of dynamic channel and role access assignments:

accessAssignments: function(doc, oldDoc) {
  var accessAssignments = [ ];

  if (doc.channels) {
    accessAssignments.push(
      {
        type: 'channel',
        channels: doc.channels,
        roles: doc.channelRoles,
        users: doc.channelUsers
      });
  }

  if (doc.roles) {
    accessAssignments.push(
      {
        type: 'role',
        roles: doc.roles,
        users: doc.roleUsers
      });
  }

  return assignments;
}
  • allowAttachments: (optional) Whether to allow the addition of file attachments for the document type. In addition to a static value (e.g. allowAttachments: true), this property may also be assigned a value dynamically via a function (e.g. allowAttachments: function(doc, oldDoc) { ... }) where the parameters are as follows: (1) the document (if deleted, the _deleted property will be true) and (2) the document that is being replaced (if any; it will be null if it has been deleted or does not exist). Defaults to false to prevent malicious/misbehaving clients from polluting the bucket/database with unwanted files. See the attachmentConstraints property and the attachmentReference validation type for more options.
  • attachmentConstraints: (optional) Various constraints to apply to file attachments associated with a document type. Its settings only apply if the document definition's allowAttachments property is true. In addition to a static value (e.g. attachmentConstraints: { }), this property may also be assigned a value dynamically via a function (e.g. attachmentConstraints: function(doc, oldDoc) { ... }) where the parameters are as follows: (1) the document (if deleted, the _deleted property will be true) and (2) the document that is being replaced (if any; it will be null if it has been deleted or does not exist). Additional parameters:
    • maximumAttachmentCount: (optional) The maximum number of attachments that may be assigned to a single document of this type. In addition to a static value (e.g. maximumAttachmentCount: 2), this property may also be assigned a value dynamically via a function (e.g. maximumAttachmentCount: function(doc, oldDoc) { ... }) where the parameters are as follows: (1) the document (if deleted, the _deleted property will be true) and (2) the document that is being replaced (if any; it will be null if it has been deleted or does not exist). 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. In addition to a static value (e.g. maximumIndividualSize: 256), this property may also be assigned a value dynamically via a function (e.g. maximumIndividualSize: function(doc, oldDoc) { ... }) where the parameters are as follows: (1) the document (if deleted, the _deleted property will be true) and (2) the document that is being replaced (if any; it will be null if it has been deleted or does not exist). 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. In addition to a static value (e.g. maximumTotalSize: 1024), this property may also be assigned a value dynamically via a function (e.g. maximumTotalSize: function(doc, oldDoc) { ... }) where the parameters are as follows: (1) the document (if deleted, the _deleted property will be true) and (2) the document that is being replaced (if any; it will be null if it has been deleted or does not exist). 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"). In addition to a static value (e.g. supportedExtensions: [ 'png', 'gif', 'jpg' ]), this property may also be assigned a value dynamically via a function (e.g. supportedExtensions: function(doc, oldDoc) { ... }) where the parameters are as follows: (1) the document (if deleted, the _deleted property will be true) and (2) the document that is being replaced (if any; it will be null if it has been deleted or does not exist). 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"). In addition to a static value (e.g. supportedContentTypes: [ 'image/png', 'image/gif', 'image/jpeg' ]), this property may also be assigned a value dynamically via a function (e.g. supportedContentTypes: function(doc, oldDoc) { ... }) where the parameters are as follows: (1) the document (if deleted, the _deleted property will be true) and (2) the document that is being replaced (if any; it will be null if it has been deleted or does not exist). No restriction by default.
    • requireAttachmentReferences: (optional) Whether every one of a document's attachments must have a corresponding attachmentReference-type property referencing it. In addition to a static value (e.g. requireAttachmentReferences: true), this property may also be assigned a value dynamically via a function (e.g. requireAttachmentReferences: function(doc, oldDoc) { ... }) where the parameters are as follows: (1) the document (if deleted, the _deleted property will be true) and (2) the document that is being replaced (if any; it will be null if it has been deleted or does not exist). Defaults to false.
    • filenameRegexPattern: (optional) A regular expression pattern that must be satisfied by each attachment's filename. In addition to a static value (e.g. filenameRegexPattern: /^foo|bar$/), this property may also be assigned a value dynamically via a function (e.g. filenameRegexPattern: function(doc, oldDoc) { ... }) where the parameters are as follows: (1) the document (if deleted, the _deleted property will be true) and (2) the document that is being replaced (if any; it will be null if it has been deleted or does not exist). No restriction by default.
  • expiry: (optional) Specifies when documents of this type will expire and subsequently get purged from the database. The constraint is only applied when a document is created or replaced. NOTE: This constraint is only supported by Sync Gateway 2.0 and later and, furthermore, it is not supported when using a Walrus database (i.e. the development/testing backend for Sync Gateway databases where contents are typically stored in memory only, rather than Couchbase Server). The constraint's value may be specified in one of the following ways:
    • an integer whose value is greater than 2,592,000 (i.e. the number of seconds in 30 days), indicating an absolute point in time as the number of seconds since the Unix epoch (1970-01-01T00:00Z)
    • an integer whose value is at most 2,592,000 (i.e. the number of seconds in 30 days), indicating a relative offset as the number of seconds in the future
    • a string in ISO 8601 date and time format. Unlike for other constraints that accept a date/time string, this constraint does not consider any component of the date to be optional. In other words, the string must include year, month, day, hour, minute, second and time zone offset (e.g. "2018-04-22T14:47:35-07:00").
    • a JavaScript Date object
    • a function that accepts as parameters (1) the new document and (2) the old document that is being replaced (if any; it will be null if it has been deleted or does not exist) and returns any of the aforementioned results
  • customActions: (optional) Defines custom actions to be executed at various events during the generated sync function's execution. Specified as an object where each property specifies a JavaScript function to be executed when the corresponding event is completed. In each case, the function accepts as parameters (1) the new document, (2) the old document that is being replaced/deleted (if any) and (3) an object that is populated with metadata generated by each event. 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. At each stage of the generated sync function's execution, the third parameter (the custom action metadata parameter) is augmented with properties that provide additional context to the custom action being executed. Custom actions may call functions from the standard sync function API (e.g. requireAccess, requireAdmin, requireRole, requireUser, access, role, channel) and may indicate errors via the throw statement to prevent the document from being written. The custom actions that are available, in the order their corresponding events occur:
    1. onTypeIdentificationSucceeded: Executed immediately after the document's type is determined and before checking authorization. The custom action metadata object parameter contains the following properties:
    • documentTypeId: The unique ID of the document type.
    • documentDefinition: The full definition of the document type.
    1. onAuthorizationSucceeded: Executed immediately after the user is authorized to make the modification and before validating document contents. Not executed if user authorization is denied. The custom action metadata object parameter includes properties from all previous events in addition to the following properties:
    • authorization: An object that indicates which channels, roles and users were used to authorize the current operation, as specified by the channels, roles and users list properties.
    1. onValidationSucceeded: Executed immediately after the document's contents are validated and before channels are assigned to users/roles and the document. Not executed if the document's contents are invalid. The custom action metadata object parameter includes properties from all previous events but does not include any additional properties.
    2. onAccessAssignmentsSucceeded: Executed immediately after channel access is assigned to users/roles and before document expiry is set. Not executed if the document definition does not include an accessAssignments constraint. The custom action metadata object parameter includes properties from all previous events in addition to the following properties:
    • accessAssignments: A list that contains each of the access assignments that were applied. Each element is an object that represents either a channel access assignment or a role access assignment depending on the value of its type property. The assignment types are specified as follows:
      • Channel access assignments:
        • type: Value of "channel".
        • channels: A list of channels that were assigned to the users/roles.
        • usersAndRoles: A list of the combined users and/or roles to which the channels were assigned. Note that, as per the sync function API, each role element's value is prefixed with "role:".
      • Role access assignments:
        • type: Value of "role".
        • roles: A list of roles that were assigned to the users.
        • users: A list of users to which the roles were assigned. Note that, as per the sync function API, each role element's value is prefixed with "role:".
    1. onExpiryAssignmentSucceeded: Executed immediately after document expiry is set and before channels are assigned to the document. Not executed if the document definition does not include an expiry constraint. NOTE: Only supported by Sync Gateway 2.0 and later. The custom action metadata object parameter includes properties from all previous events in addition to the following properties:
    • expiryDate: A JavaScript Date object that specifies the absolute point in time at which the document will expire and be purged from the database.
    1. onDocumentChannelAssignmentSucceeded: Executed immediately after channels are assigned to the document. The last step before the sync function is finished executing and the document revision is written. The custom action metadata object parameter includes properties from all previous events in addition to the following properties:
    • documentChannels: A list of channels that were assigned to the document.

An example of an onAuthorizationSucceeded custom action that stores a property in the metadata object parameter for later use by the onDocumentChannelAssignmentSucceeded custom action:

customActions: {
  onAuthorizationSucceeded: function(doc, oldDoc, customActionMetadata) {
    var extraChannel = customActionMetadata.documentTypeId + '-modify';
    if (oldDoc && !oldDoc._deleted) {
      // If the document is being replaced or deleted, ensure the user has the document type's "-modify" channel in addition to one of
      // the channels from the document definition's "channels" property that was already authorized
      requireAccess(extraChannel);
    }

    // Store the extra modification validation channel name for future use
    customActionMetadata.extraModifyChannel = extraChannel;
  },
  onDocumentChannelAssignmentSucceeded: function(doc, oldDoc, customActionMetadata) {
    // Ensure the extra modification validation channel is also assigned to the document
    channel(customActionMetadata.extraModifyChannel);
  }
}

Content validation

There are a number of validation types that can be used to define each property/element/key's expected format in a document.

Simple type validation

Validation for simple data types (e.g. integers, floating point numbers, strings, dates/times, etc.):

  • string: The value is a string of characters. Additional parameters:
    • mustNotBeEmpty: If true, an empty string is not allowed. Defaults to false.
    • mustBeTrimmed: If true, a string that has leading or trailing whitespace characters is not allowed. Defaults to false.
    • regexPattern: A regular expression pattern that must be satisfied for values to be accepted (e.g. new RegExp('\\d+') or /[A-Za-z]+/). No restriction by default.
    • minimumLength: The minimum number of characters (inclusive) allowed in the string. No restriction by default.
    • maximumLength: The maximum number of characters (inclusive) allowed in the string. No restriction by default.
    • minimumValue: Reject strings with an alphanumeric sort order that is less than this. No restriction by default.
    • minimumValueExclusive: Reject strings with an alphanumeric sort order that is less than or equal to this. No restriction by default.
    • maximumValue: Reject strings with an alphanumeric sort order that is greater than this. No restriction by default.
    • maximumValueExclusive: Reject strings with an alphanumeric sort order that is greater than or equal to this. No restriction by default.
    • mustEqualIgnoreCase: The item's value must be equal to the specified value, ignoring differences in case. For example, "CAD" and "cad" would be considered equal by this constraint. No restriction by default.
  • integer: The value is a number with no fractional component. Additional parameters:
    • minimumValue: Reject values that are less than this. No restriction by default.
    • minimumValueExclusive: Reject values that are less than or equal to this. No restriction by default.
    • maximumValue: Reject values that are greater than this. No restriction by default.
    • maximumValueExclusive: Reject values that are greater than or equal to this. No restriction by default.
  • float: The value is a number with an optional fractional component (i.e. it is either an integer or a floating point number). Additional parameters:
    • minimumValue: Reject values that are less than this. No restriction by default.
    • minimumValueExclusive: Reject values that are less than or equal to this. No restriction by default.
    • maximumValue: Reject values that are greater than this. No restriction by default.
    • maximumValueExclusive: Reject values that are greater than or equal to this. No restriction by default.
  • boolean: The value is either true or false. No additional parameters.
  • datetime: The value is a simplified ECMAScript ISO 8601 date string with optional time and time zone components (e.g. "2016-06-18T18:57:35.328-08:00"). If both time and time zone are omitted, the time is assumed to be midnight UTC. If a time is provided but the time zone is omitted, the time zone is assumed to be the Sync Gateway server's local time zone. Additional parameters:
    • minimumValue: Reject date/times that are less than this. May be either an ECMAScript ISO 8601 date string with optional time and time zone components OR a JavaScript Date object. No restriction by default.
    • minimumValueExclusive: Reject date/times that are less than or equal to this. May be either an ECMAScript ISO 8601 date string with optional time and time zone components OR a JavaScript Date object. No restriction by default.
    • maximumValue: Reject date/times that are greater than this. May be either an ECMAScript ISO 8601 date string with optional time and time zone components OR a JavaScript Date object. No restriction by default.
    • maximumValueExclusive: Reject date/times that are greater than or equal to this. May be either an ECMAScript ISO 8601 date string with optional time and time zone components OR a JavaScript Date object. No restriction by default.
  • date: The value is a simplified ECMAScript ISO 8601 date string without time and time zone components (e.g. "2016-06-18"). For the purposes of date comparisons (e.g. by way of the minimumValue, maximumValue, etc. parameters), the time is assumed to be midnight UTC. Additional parameters:
    • minimumValue: Reject dates that are less than this. May be either an ECMAScript ISO 8601 date string without time and time zone components OR a JavaScript Date object. No restriction by default.
    • minimumValueExclusive: Reject dates that are less than or equal to this. May be either an ECMAScript ISO 8601 date string without time and time zone components OR a JavaScript Date object. No restriction by default.
    • maximumValue: Reject dates that are greater than this. May be either an ECMAScript ISO 8601 date string without time and time zone components OR a JavaScript Date object. No restriction by default.
    • maximumValueExclusive: Reject dates that are greater than or equal to this. May be either an ECMAScript ISO 8601 date string without time and time zone components OR a JavaScript Date object. No restriction by default.
  • time: The value is a simplified ECMAScript ISO 8601 time string without date and time zone components (e.g. "18:57:35.328"). Additional parameters:
    • minimumValue: Reject times that are less than this. Must be an ECMAScript ISO 8601 time string without date and time zone components.
    • minimumValueExclusive: Reject times that are less than or equal to this. Must be an ECMAScript ISO 8601 time string without date and time zone components.
    • maximumValue: Reject times that are greater than this. Must be an ECMAScript ISO 8601 time string without date and time zone components.
    • maximumValueExclusive: Reject times that are greater than or equal to this. Must be an ECMAScript ISO 8601 time string without date and time zone components.
  • timezone: The value is a simplified ECMAScript ISO 8601 time zone string without date and zone components (e.g. "Z" or "-05:00"). Additional parameters:
    • minimumValue: Reject time zones that are less than this. Must be an ECMAScript ISO 8601 time zone string.
    • minimumValueExclusive: Reject time zones that are less than or equal to this. Must be an ECMAScript ISO 8601 time zone string.
    • maximumValue: Reject time zones that are greater than this. Must be an ECMAScript ISO 8601 time zone string.
    • maximumValueExclusive: Reject time zones that are greater than or equal to this. Must be an ECMAScript ISO 8601 time zone string.
  • 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 (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. Additional parameters:
    • minimumValue: Reject UUIDs that are less than this. No restriction by default.
    • minimumValueExclusive: Reject UUIDs that are less than or equal to this. No restriction by default.
    • maximumValue: Reject UUIDs that are greater than this. No restriction by default.
    • maximumValueExclusive: Reject UUIDs that are greater than or equal to this. No restriction by default.
  • 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.
    • 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.
Complex type validation

Validation for complex data types (e.g. objects, arrays, hashtables):

  • array: An array/list of elements. Additional parameters:
    • mustNotBeEmpty: If true, an array with no elements is not allowed. Defaults to false.
    • minimumLength: The minimum number of elements (inclusive) allowed in the array. Undefined by default.
    • maximumLength: The maximum number of elements (inclusive) allowed in the array. Undefined by default.
    • arrayElementsValidator: The validation that is applied to each element of the array. Any validation type, including those for complex data types, may be used. Undefined by default. An example:
myArray1: {
  type: 'array',
  mustNotBeEmpty: true,
  arrayElementsValidator: {
    type: 'string',
    regexPattern: new RegExp('[A-Za-z0-9_-]+')
  }
}
  • object: An object that is able to declare which properties it supports so that unrecognized properties are rejected. Additional parameters:
    • allowUnknownProperties: Whether to allow the existence of properties that are not explicitly declared in the object definition. Not applied recursively to objects that are nested within this object. Defaults to false if the propertyValidators parameter is specified; otherwise, it defaults to true.
    • propertyValidators: An object/hash of validators to be applied to the properties that are explicitly supported by the object. Any validation type, including those for complex data types, may be used for each property validator. Undefined by default. If defined, then any property that is not declared will be rejected by the sync function unless the allowUnknownProperties parameter is true. An example:
myObj1: {
  type: 'object',
  propertyValidators: {
    myProp1: {
      type: 'date',
      immutable: true
    },
    myProp2: {
      type: 'integer',
      minimumValue: 1
    }
  }
}
  • hashtable: An object/hash that, unlike the object type, does not declare the names of the properties it supports and may optionally define a single validator that is applied to all of its element values. Additional parameters:
    • minimumSize: The minimum number of elements allowed in the hashtable. Unconstrained by default.
    • maximumSize: The maximum number of elements allowed in the hashtable. Unconstrained by default.
    • hashtableKeysValidator: The validation that is applied to each of the keys in the object/hash. Undefined by default. Additional parameters:
      • mustNotBeEmpty: If true, empty key strings are not allowed. Defaults to false.
      • regexPattern: A regular expression pattern that must be satisfied for key strings to be accepted. Undefined by default.
    • hashtableValuesValidator: The validation that is applied to each of the values in the object/hash. Undefined by default. Any validation type, including those for complex data types, may be used. An example:
myHash1: {
  type: 'hashtable',
  hashtableKeysValidator: {
    mustNotBeEmpty: false,
    regexPattern: new RegExp('\\w+')
  },
  hashtableValuesValidator: {
    type: 'object',
    required: true,
    propertyValidators: {
      mySubObjProp1: {
        type: 'string'
      }
    }
  }
}
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:
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:

  • required: The value cannot be null or missing/undefined. Defaults to false.
  • immutable: The item cannot be changed from its existing value if the document is being replaced. The constraint is applied recursively so that, even if a value that is nested an arbitrary number of levels deep within an immutable complex type is modified, the document change will be rejected. A value of null is treated as equal to missing/undefined and vice versa. Does not apply when creating a new document or deleting an existing document. Differs from immutableStrict in that it checks for semantic equality of specialized string validation types (e.g. date, datetime, time, timezone, uuid); for example, the two uuid values of "d97b3a52-78d5-4112-9705-e4ab436f5114" and "D97B3A52-78D5-4112-9705-E4AB436F5114" are considered equal with this constraint since they differ only by case. Defaults to false.
  • immutableStrict: The item cannot be changed from its existing value if the document is being replaced. Differs from immutable in that specialized string validation types (e.g. date, datetime, time, timezone, uuid) are not compared semantically; for example, the two time values of "12:45" and "12:45:00.000" are not considered equal because the strings are not strictly equal. Defaults to false.
  • immutableWhenSet: As with the immutable property, the item cannot be changed from its existing value if the document is being replaced. However, it differs in that it does not prevent modification if the item is either null or missing/undefined in the existing document. Differs from immutableWhenSetStrict in that it checks for semantic equality of specialized string validation types (e.g. date, datetime, time, timezone, uuid); for example, the two datetime values of "2018-01-01T21:09:00.000Z" and "2018T16:09-05:00" are considered equal with this constraint since they represent the same point in time. Defaults to false.
  • immutableWhenSetStrict: As with the immutableWhenSet property, the item cannot be changed if it already has a value. However, it differs in that specialized string validation types (e.g. date, datetime, time, timezone, uuid) are not compared semantically; for example, the two date values of "2018" and "2018-01-01" are not considered equal because the strings are not strictly equal. Defaults to false.
  • mustEqual: The value of the property or element must be equal to the specified value. Useful in cases where the item's value should be computed from other properties of the document (e.g. a reference ID that is encoded into the document's ID or a number that is the result of some calculation performed on other properties in the document). For that reason, this constraint is perhaps most useful when specified as a dynamic constraint (e.g. mustEqual: function(doc, oldDoc, value, oldValue) { ... }) rather than as a static value (e.g. mustEqual: 'foobar'). If this constraint is set to null, then only values of null or missing/undefined will be accepted for the corresponding property or element. Differs from mustEqualStrict in that it checks for semantic equality of specialized string validation types (e.g. date, datetime, time, timezone, uuid); for example, the two datetime values of "2018-02-12T11:02:00.000Z" and "2018-02-12T11:02+00:00" are considered equal with this constraint since they represent the same point in time. No constraint by default.
  • 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). 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: {
  myStringProp: {
    type: 'string'
  },
  myCustomProp: {
    type: 'integer',
    minimumValue: 1,
    maximumValue: 100,
    customValidation: function(doc, oldDoc, currentItemEntry, validationItemStack) {
      var parentObjectElement = validationItemStack[validationItemStack.length - 1];
      var parentObjectName = parentObjectElement.itemName;
      var parentObjectValue = parentObjectElement.itemValue;
      var parentObjectOldValue = parentObjectElement.oldItemValue;

      var currentPropName = currentItemEntry.itemName;
      var currentPropValue = currentItemEntry.itemValue;
      var currentPropOldValue = currentItemEntry.oldItemValue;

      var currentPropPath = parentObjectName + '.' + currentPropName;
      var myStringPropPath = parentObjectName + '.myStringProp';

      var validationErrors = [ ];

      if (parentObjectValue.myStringProp && !currentPropValue) {
        validationErrors.push('property "' + currentPropPath + '" must be defined when "' + myStringPropPath + '" is defined');
      }

      if (currentPropOldValue && currentPropValue && currentPropValue < currentPropOldValue) {
        validationErrors.push('property "' + currentPropPath + '" must not decrease in value');
      }

      return validationErrors;
    }
  }
}
Predefined validators

The following predefined item validators may also be useful:

  • typeIdValidator: A property validator that is suitable for application to the property that specifies the type of a document. Its constraints include ensuring the value is a string, is neither null nor undefined, is not an empty string and cannot be modified. NOTE: If a document type specifies simpleTypeFilter as its type filter, it is not necessary to explicitly include a type property validator; it will be supported implicitly as a typeIdValidator. An example usage:
propertyValidators: {
  type: typeIdValidator,
  foobar: {
    type: 'string'
  }
}
Dynamic constraint validation

In addition to defining any of the item validation constraints above, including type, as static values (e.g. maximumValue: 99, mustNotBeEmpty: true), it is possible to specify them dynamically via function (e.g. regexPattern: function(doc, oldDoc, value, oldValue) { ... }). This is useful if, for example, the constraint should be based on the value of another property/element in the document or computed based on the previous stored value of the current property/element. The function should expect to receive the following parameters:

  1. The current document.
  2. The document that is being replaced (if any). Note that, if the document is missing (e.g. it doesn't exist yet) or it has been deleted, this parameter will be null.
  3. The current value of the property/element/key.
  4. The previous value of the property/element/key as stored in the revision of the document that is being replaced (if any).

For example:

propertyValidators: {
  sequence: {
    type: 'integer',
    required: true,
    // The value must always increase by at least one with each revision
    minimumValue: function(doc, oldDoc, value, oldValue) {
      return !isValueNullOrUndefined(oldValue) ? oldValue + 1 : 0;
    }
  },
  category: {
    type: 'enum',
    required: true,
    // The list of valid categories depends on the beginning of the document's ID
    predefinedValues: function(doc, oldDoc, value, oldValue) {
      return (doc._id.indexOf('integerDoc-') === 0) ? [ 1, 2, 3 ] : [ 'a', 'b', 'c' ];
    }
  },
  referenceId: {
    type: 'string',
    required: true,
    // The reference ID must be constructed from the value of the category field
    regexPattern: function(doc, oldDoc, value, oldValue) {
      return new RegExp('^foobar-' + doc.category + '-[a-zA-Z_-]+$');
    }
  }
}

Definition file

A document definitions file specifies all the document types that belong to a single Sync Gateway bucket/database. Such a file can contain either a plain JavaScript object or a JavaScript function that returns the documents' definitions wrapped in an object.

For example, a document definitions file implemented as an object:

{
  myDocType1: {
    channels: {
      view: 'view',
      write: 'write'
    },
    typeFilter: function(doc, oldDoc, docType) {
      return oldDoc ? oldDoc.type === docType : doc.type === docType;
    },
    propertyValidators: {
      type: typeIdValidator,
      myProp1: {
        type: 'integer'
      }
    }
  },
  myDocType2: {
    channels: {
      view: 'view',
      write: 'write'
    },
    typeFilter: function(doc, oldDoc, docType) {
      return oldDoc ? oldDoc.type === docType : doc.type === docType;
    },
    propertyValidators: {
      type: typeIdValidator,
      myProp2: {
        type: 'datetime'
      }
    }
  }
}

Or a functionally equivalent document definitions file implemented as a function:

function() {
  var sharedChannels = {
    view: 'view',
    write: 'write'
  };

  function myDocTypeFilter(doc, oldDoc, docType) {
    return oldDoc ? oldDoc.type === docType : doc.type === docType;
  }

  return {
    myDocType1: {
      channels: sharedChannels,
      typeFilter: myDocTypeFilter,
      propertyValidators: {
        type: typeIdValidator,
        myProp1: {
          type: 'integer'
        }
      }
    },
    myDocType2: {
      channels: sharedChannels,
      typeFilter: myDocTypeFilter,
      propertyValidators: {
        type: typeIdValidator,
        myProp2: {
          type: 'datetime'
        }
      }
    }
  };
}

As demonstrated above, the advantage of defining a function rather than an object is that you may also define variables and functions that can be shared between document types but at the cost of some brevity.

Modularity

Document definitions are also modular. By invoking the importDocumentDefinitionFragment macro, the contents of external files can be imported into the main document definitions file. For example, each individual document definition from the example above can be specified as a fragment in its own separate file:

  • my-doc-type1-fragment.js:
{
  channels: sharedChannels,
  typeFilter: myDocTypeFilter,
  propertyValidators: {
    type: typeIdValidator,
    myProp1: {
      type: 'integer'
    }
  }
}
  • my-doc-type2-fragment.js:
{
  channels: sharedChannels,
  typeFilter: myDocTypeFilter,
  propertyValidators: {
    type: typeIdValidator,
    myProp2: {
      type: 'datetime'
    }
  }
}

And then each fragment can be imported into the main document definitions file:

function() {
  var sharedChannels = {
    view: 'view',
    write: 'write'
  };

  function myDocTypeFilter(doc, oldDoc, docType) {
    return oldDoc ? oldDoc.type === docType : doc.type === docType;
  }

  return {
    myDocType1: importDocumentDefinitionFragment('my-doc-type1-fragment.js'),
    myDocType2: importDocumentDefinitionFragment('my-doc-type2-fragment.js')
  };
}

As you can see, the fragments can also reference functions (e.g. myDocTypeFilter) and variables (e.g. sharedChannels) that were defined in the main document definitions file. Organizing document definitions in this manner helps to keep configuration manageable.

Helper functions

Custom code (e.g. type filters, custom validation functions, custom actions) within document definitions have access to Underscore.js's API by way of the _ variable (e.g. _.map(...), _.union(...)). In addition, synctos provides some extra predefined functions for common operations:

  • isDocumentMissingOrDeleted(candidate): Determines whether the given candidate document is either missing (i.e. null or undefined) or deleted (i.e. its _deleted property is true). Useful in cases where, for example, the old document (i.e. oldDoc parameter) is non-existant or deleted and you want to treat both cases as equivalent.
  • isValueNullOrUndefined(value): Determines whether the given value parameter is either null or undefined. In many cases, it is useful to treat both states the same.
  • jsonStringify(value): Converts a value into a JSON string. May be useful in a customValidation constraint for formatting a custom error message, for example. Serves as a direct replacement for JSON.stringify since Sync Gateway does not support the global JSON object.

Testing

The synctos project includes a variety of specifications/test cases to verify the behaviours of its various features. However, if you need to write a custom validation function, dynamic type filter, dynamic assignment of channels to users/roles, etc. or you would otherwise like to verify a generated sync function, this project includes a test fixture module (src/testing/test-fixture-maker.js) that is useful in automating much of the work that can go into writing test cases.

The post Testing your Sync Gateway functions with synctos on the official Couchbase blog provides a detailed walkthrough, with examples, for setting up and running tests. The following section also provides a brief overview of the process.

Note that synctos uses the mocha test framework for writing and executing test cases and the Chai library for assertions. The following instructions assume that you will too, but by no means are your projects restricted to using either of these libraries for their own tests.

Some other test runners/frameworks that might be of interest:

And some alternate assertion libraries:

Install the testing libraries locally and add them as development dependencies in the project's package.json file (e.g. npm install mocha --save-dev, npm install chai --save-dev).

After that, create a new specification file in your project's test/ directory (e.g. test/foobar-spec.js) and import the test fixture module into the empty spec:

var testFixtureMaker = require('synctos').testFixtureMaker;

Create a new describe block to encapsulate the forthcoming test cases, initialize the synctos test fixture and also reset the state after each test case using the afterEach function. For example:

describe('My new sync function', function() {
  var testFixture = testFixtureMaker.initFromDocumentDefinitions('/path/to/my-doc-definitions.js');

  afterEach(function() {
    testFixture.resetTestEnvironment();
  });

  ...
});

Now you can begin writing specs/test cases inside the describe block using the test fixture's convenience functions to verify the behaviour of the generated sync function. For example, to verify that a new document passes validation, specifies the correct channels, roles and usernames for authorization, and assigns the desired channel access to a list of users:

it('can create a myDocType document', function() {
  var doc = {
    _id: 'myDocId',
    type: 'myDocType',
    foo: 'bar',
    bar: -32,
    members: [ 'joe', 'nancy' ]
  };

  testFixture.verifyDocumentCreated(
    doc,
    {
      expectedChannels: [ 'my-add-channel1', 'my-add-channel2' ],
      expectedRoles: [ 'my-add-role' ],
      expectedUsers: [ 'my-add-user' ]
    },
    [
      {
        expectedUsers: function(doc, oldDoc) {
          return doc.members;
        },
        expectedChannels: function(doc, oldDoc) {
          return 'view-' + doc._id;
        }
      }
    ]);
});

Or to verify that a document cannot be created because it fails validation:

it('cannot create a myDocType doc when required property foo is missing', function() {
  var doc = {
    _id: 'myDocId',
    type: 'myDocType',
    bar: 79
  };

  testFixture.verifyDocumentNotCreated(
    doc,
    'myDocType',
    [ testFixture.validationErrorFormatter.requiredValueViolation('foo') ],
    {
      expectedChannels: [ 'my-add-channel1', 'my-add-channel2' ],
      expectedRoles: [ 'my-add-role' ],
      expectedUsers: [ 'my-add-user' ]
    });
});

The testFixture.validationErrorFormatter object in the preceding example provides a variety of functions that can be used to specify expected validation error messages. See the src/testing/validation-error-formatter.js module in this project for documentation.

To execute the tests in the test/ directory, ensure that the project's package.json file contains a "test" script. For example:

"scripts": {
  "test": "mocha test/"
}

Once the test script is configured in package.json, run the tests with the npm test command.

You will find many more examples in this project's test/ directory and in the example project synctos-test-examples.

Refer to the Validating section of the README for information on using the validation utility to verify the structure and semantics of a document definitions file.

synctos's People

Contributors

amrikr avatar cclark avatar dependabot[bot] avatar dkichler avatar greenkeeper[bot] avatar greenkeeperio-bot avatar oldsneerjaw avatar skwoka avatar ziemek avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

synctos's Issues

Arrays can be assigned to items that expect object or hashtable

Consider the following document definition:

myDocType: {
  typeFilter: simpleTypeFilter,
  channels: { write: 'write' },
  objectProp: {
    type: 'object'
  },
  hashtableProp: {
    type: 'hashtable'
  },
  type: {
    type: 'string',
    required: true,
    mustNotBeEmpty: true,
    immutable: true
  }
}

A generated sync function would erroneously allow the following document for a user with the "write" channel:

{
  "type": "myDocType",
  "objectProp": [ "foo" ]
  "hashtableProp": [ "bar" ]
}

Reproduced in synctos v1.1.0.

Support dynamic document constraints

A number of constraints may be specified for a document type as static values. However, for greater flexibility, it should be possible to define these constraints dynamically, as a function. For example (not to be considered final):

{
  myDoc: {
    typeFilter: ...
    channels: ...
    propertyValidators: function(doc, oldDoc) {
      return ...;
    },
    immutable: function(doc, oldDoc) {
      return ...;
    },
    allowAttachments: function(doc, oldDoc) {
      return ...;
    }
  }
}

The full list of constraints for which to allow dynamic construction:

  • propertyValidators
  • allowUnknownProperties
  • immutable
  • cannotReplace
  • cannotDelete
  • allowAttachments
  • attachmentConstraints:
    • maximumAttachmentCount
    • maximumIndividualSize
    • maximumTotalSize
    • supportedExtensions
    • supportedContentTypes
    • requireAttachmentReferences

Related to #94.

Backticks in document definitions cause syntax errors

Because Sync Gateway uses backticks (`) to denote the beginning and end of multiline strings, any backtick that happens to appear in a document definition will be treated by Sync Gateway as the closing character of the sync function's multiline string, resulting in a syntax error (e.g. "FATAL: Error reading config file config/synctos-test.json: invalid character 'p' after object key:value pair -- rest.ParseCommandLine() at config.go:476") when the sync function is loaded. Therefore, backticks should be replaced with the escape sequence "\`" when generating a sync function.

Document-wide constraints on file attachments

Support document-level properties that limit

  • the size, in bytes, of each individual file attachment associated with documents of that type (default: unlimited)
  • the total size, in bytes, of all file attachments associated with a single document of that type (default: unlimited)
  • the maximum number of file attachments that may be associated with a single document of that type (default: unlimited)
  • the content/MIME types that are allowed (default: unconstrained)
  • the file extensions that are allowed (default: unconstrained)
  • whether each attachment must have a corresponding property of the attachmentReference validation type (default: false)

Here is an example configuration (not to be considered final) that limits each attachment to 200 KB (i.e. 204,800 bytes), the total combined size of all attachments to 600 KB (i.e. 614,400 bytes), the maximum number of file attachments to 5, the supported content types to "text/plain" and "text/html", the file extensions that are supported to "txt" and "html" and that each attachment must have a corresponding attachmentReference:

foo: {
  typeFilter: ...,
  channels: ...,
  propertyValidators: ...,
  attachmentConstraints: {
    maximumIndividualSize: 204800,
    maximumTotalSize: 614400,
    maximumAttachmentCount: 5,
    supportedExtensions: [ 'txt', 'html' ],
    supportedContentTypes: [ 'text/plain', 'text/html' ],
    requireAttachmentReferences: true
  }
}

Allow hashtable document definitions

Currently every document type is presumed to use the object validation type, where each property's name and type is known beforehand. However, there may be cases where it is useful to use hashtable validation for a particular document type so that the property/key names don't need to be known beforehand while still providing the flexibility to perform validation on the property values.

For example (not to be considered final):

myDocType: {
  typeFilter: ...
  channels: ...
  type: 'hashtable',
  hashtableKeysValidator: {
    regexPattern: /^\w+$/
  },
  hashtableElementsValidator: {
    type: 'integer',
    required: true,
    minimumValue: 0
  }
}

For the sake of backward compatibility, the default validation type for documents should continue to be object.

New property validation type for type identifier properties

Because a document's type is commonly specified via a property on the document itself (often called something like "type") in combination with a typeFilter value of simpleTypeFilter, add a new property validation type specifically for such type identifier properties (e.g. called "typeIdValidator") that is shorthand for the following:

{
  type: "string",
  required: true,
  mustNotBeEmpty: true,
  immutable: true
}

Related to #73.

Using the test helper functionality by passing in the couchbase config JSON or function directly

Overview

I have a need to use the test-helper.js functionality but without using the sync function generation.

With that in mind I simply want to pass in the couchbase configuration .json file path, or pass in the sync function directly to the test helper so it can be tested without the need for having the sync function in a separate file or a configuration.

Details

Currently the only methods for initialising the testHelper take a file path to a definition file or to a .js file that contains the sync function. If the test helper is initialised with no arguments it defaults to the method that initialises with the sync function file path which then crashes as no path is supplied.

Ideally the default init function would be able to be used with no arguments so the 'syncFunction' property can be set from an external source, that way it's up to the user how they load the sync function into the test helper.

If this isn't desired then the ability to load in the actual couch base configuration .json would be just as good.

Modular document definition files

For a bucket/database with a large number of document types, it can be difficult to navigate through the document definition file to find the document type(s) you're looking for. Make document definitions modular by adding a macro that allows fragments to be imported directly into the main document definition file from secondary files.

For example:

{
    ...

    // A request/requisition for payment of an invoice
    paymentRequisition: importDocumentDefinitionFragment('payment-requisition-fragment.js'),

    // References the payment requisitions and payment attempts that were created for an invoice
    paymentRequisitionsReference: importDocumentDefinitionFragment('payment-requisitions-reference-fragment.js')

    ...
}

Imported fragments must have access to the functions and variables that are defined in the main document definitions file.

Run specs on live Sync Gateway instance

Currently test cases are executed in a mock environment by NodeJS. To ensure test results are as accurate and reliable as possible, add an option to take the sync functions generated from test document definitions, write them to a Sync Gateway config file, use that config to launch a live Sync Gateway instance and then execute the tests against that live instance.

Accept Date object for date/time constraint parameters

Currently the minimum and maximum value parameters of the date and datetime validation types must be specified as ISO 8601 strings. Support the use of a Date object as well. For example:

propertyValidators: {
  myDate: {
    type: 'date',
    minimumValue: new Date(78234736),
    maximumValueExclusive: new Date(2017, 3, 14)
  }
}

The full list of date and datetime parameters that should support Date objects:

  • minimumValue
  • minimumValueExclusive
  • maximumValue
  • maximumValueExclusive

Synctos compatibility with sync-gateway-adminui

Hi Folks,
So after the latest fix, I am able to upload the sync function through admin ui but looks like the admin ui code is completely unstable.
After adding few docs I refreshed the admin console. It try to rerun the sync-function on page refresh and throws error around immutable fields saying onChange failed because of sync function rejection to update immutable fields. This is weird because i am not even making an update call, I am just refreshing the page.

How do you guys deploy sync function in production, and how do you test it? Is there a way to stay away from this admin UI console?

Range validation parameters that exclude the minimum/maximum values

The minimumValue and maximumValue constraints limit the range of values that can be assigned to items of integer, float, date and datetime types. However, the minimum and maximum values are inclusive, which can be problematic for floating point numbers where, for example, you might want to ensure that all values are less than 7.5. What do you use as the maximum (inclusive) value? 7.49? 7.499999?

Instead, introduce new constraints (e.g. minimumValueExclusive, maximumValueExclusive) for these data validation types where the minimum and maximum values are exclusive rather than inclusive.

Support document authorization by specific users

Currently document type definitions are limited to providing a list of channels to which a user must belong (either directly or indirectly through membership in a role that includes the channel(s)) in order to create/replace/delete documents of that type. However, Sync Gateway also exposes the requireUser function to sync functions, which specifies that only one of the provided users may perform the operation. Add configuration support for defining the list of users that a document requires.

See Authorizing Document Updates for more info. Related to #22.

Provide default type filter function

Because many/most document types are likely to use a type filter function that determines the type by simply matching the value of the document's type property, provide a default function (e.g. defaultTypeFilter) that checks that the type property's value exactly matches the document type name (the key that identifies the document in the document definitions object).

For example,

myDocType: {
  channels: ...,
  propertyValidators: ...,
  typeFilter: simpleTypeFilter
}

would return true if a candidate document's type property is "myDocType".

Option to assign channel definitions directly from the document itself

Add convenience configuration that allows a user to define a document's channels as a property in the document itself, rather than predefining them in configuration.

For example, if the channels property were used to specify channels and the sync function were to receive the following document:

{
  "_id": "myDoc",
  "foo": "bar",
  "channels": [ "channel1", "channel2" ]
}

it would assign "channel1" and "channel2" to the document when it is written (i.e. in the call to the official sync function API's channel function).

When verifying authorization to replace or delete such a document, the sync function must be sure to pass the channels from the old document's channels property to requireAccess to ensure a malicious client wouldn't be able to overwrite the document by specifying a channel that it does have access to in the new document's channels property.

Include an implicit type property when a simple type filter is used

The simple type filter (i.e. simpleTypeFilter) is a useful shorthand for documents that are identified exclusively by a type identifier property called "type". However, currently it is still necessary to explicitly include a property validator for the "type" property in the document's definition even when a simple type filter is used for that document type. Instead, when a simple type filter is specified for a document type and no explicit "type" property validator exists, implicitly assume the document type includes a property validator called "type" with the following constraints:

{
  type: "string",
  required: true,
  mustNotBeEmpty: true,
  immutable: true
}

Related to #72.

Embed indent.js as a static dependency

The synctos package has a single non-development dependency: indent.js. To keep the project light and self contained, embed the most recent version of indent.js statically into the synctos npm project's lib directory.

Does not return a non-zero exit status when sync function generation fails

The make-sync-function script returns an exit status of zero in all cases, even if the script failed (e.g. because the document definitions file does not exist). It should return a non-zero exit status in failure cases.

To reproduce the issue, execute the following commands:

$ ./make-sync-function /path/to/file-that-does-not-exist.js /path/to/generated-sync-function.js
$ echo $?

The second command displays the exit status from the previous command. It should be non-zero when the document definitions file does not exist, but currently it is always zero.

Enum property validation type

In some cases, it is necessary to limit a property to a certain set of acceptable values. Implement a new property validation type called "enum" for such cases. The list/set of acceptable values should allow both string and integer values.

For example (not to be considered final):

propertyValidators: {
  category: {
    type: 'enum',
    predefinedValues: [ 1, 2, 3, 'fruit', 'vegetable' ]
  }
}

Parameter to allow unknown properties in a document or object

Currently sync functions do not allow root-level properties that are not explicitly declared in the document definition. The same is true of objects nested within a document that declare a propertyValidators parameter. Add an optional parameter (e.g. allowUnknownProperties) to these components that, when enabled on a document, allows a document to include root-level properties that are not explicitly declared in the document definition and, when enabled on a nested object, allows unknown properties on that level of the object graph. Should be disabled by default.

Decompose the sync function template

The sync function template file (etc/sync-function-template.js) has grown to a size that makes it increasingly difficult to navigate its embedded functions and variables. Group related components into external files/modules and import them into each generated sync function using a technique similar to that used in #67 for modular document definitions.

Tool to validate structure and semantics of a document definitions file

Currently the make-sync-function script injects the specified document definitions file directly into the sync function template without any validation of its contents. Instead, it should verify the structure of the document definitions and output a warning for any violation it finds.

Examples of things to check for:

  • if the document definitions are declared as a function, that function must return an object
  • each document type includes a channels property
  • channels in a document type's channels property are declared either as strings or arrays of strings
  • a document type's channels property declares EITHER write OR add, replace and remove
  • each document type includes a typeFilter that is a function
  • if any of a document type's allowAttachments, immutable, cannotDelete or cannotReplace constraints are defined, they must have boolean values
  • each validator in a document declares a supported validation type
  • each of a validator's additional parameters conform to the validation type's expected input values (e.g. you can't assign a string value to a float property's minimumValue constraint)
  • a validation parameter is assigned to a validation type that supports that parameter (e.g. you can't assign regexPattern to an integer property)
  • an instance of a customValidation parameter is declared as a function
  • etc.

Option to combine add, replace and remove channels into a single "write" channel definition

Currently it is necessary to specify the channel(s) for the add, replace and remove operations individually, even if they are all the same value. Provide support for a single write parameter in the document channels object that is then applied to any of the operations in question.

For example, instead of

channels: {
  view: 'my-view-channel',
  add: 'my-write-channel',
  replace: 'my-write-channel',
  remove: 'my-write-channel'
}

allow the following:

channels: {
  view: 'my-view-channel',
  write: 'my-write-channel'
}

The name of the parameter is subject to debate.

Support dynamic assignment of channels to roles and users

Sync Gateway exposes the access function to sync functions, which allows channels to be assigned to users and roles on the fly (more info). Add support for document definitions to supply configuration that is applied after the user is authorized (i.e. via the requireAccess function) and the document's contents pass validation, but before channels are assigned (i.e. via the channel function).

For example (not to be considered final):

myDocType: {
  channels: ...,
  typeFilter: ...,
  propertyValidators: ...,
  accessAssignments: [
    {
      users: function(doc, oldDoc) {
        return doc.members;
      },
      roles: function(doc, oldDoc) {
        return doc.groups;
      },
      channels: function(doc, oldDoc) {
        return doc.channels;
      }
    },
    {
      users: [ 'user1' ],
      roles: [ 'admins', 'power-users' ],
      channels: 'channel1'
    }
  ]
},

Sync-gateway not excepting any test function provided in synctos tests.

Process

  • Copy any test sync function from synctos repo. And create a simple walrus config.
  • Bring up sync-gateway server
  • load admin > sync page
  • chrome dev tools report the serve error while sync-function compilation

Uncaught SyntaxError: Invalid or unexpected token at compileSyncFunction (sync:580) at SyncModel.setSyncFunction (sync:580) at sync:580 at Object.callback (sync:579) at on_end (sync:579) at XMLHttpRequest.on_state_change (sync:579)

  • I tried to see what it is compiling to and this is the stuff inside validation functions.

function isIso8601DateTimeString(value) { var regex = new RegExp('^(([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01]))([T ]([01][0-9]|2[0-4])(:[0-5][0-9])?(:[0-5][0-9])?([\\.,][0-9]{1,3})?)?([zZ]|([\\+-])([01][0-9]|2[0-3]):?([0-5][0-9])?)?;function makeArray(maybeArray){return Array.isArray(maybeArray)?maybeArray:[maybeArray]}function inArray(string,array){return-1!=array.indexOf(string)}function anyInArray(any,array){for(var i=0;i<any.length;++i)if(inArray(any[i],array))return!0;return!1}function requireUser(names){if(shouldValidate&&(names=makeArray(names),!inArray(realUserCtx.name,names)))throw{forbidden:"wrong user"}}function requireRole(roles){if(shouldValidate&&(roles=makeArray(roles),!anyInArray(realUserCtx.roles,roles)))throw{forbidden:"missing role"}}function requireAccess(channels){if(shouldValidate&&(channels=makeArray(channels),!anyInArray(realUserCtx.channels,channels)))throw{forbidden:"missing channel access"}}function channel(){var args=Array.prototype.slice.apply(arguments);results.channels=Array.prototype.concat.apply(results.channels,args)}function access(users,channels){results.access.push({users:makeArray(users),channels:makeArray(channels)})}function role(users,channels){results.roles.push({users:makeArray(users),channels:makeArray(channels)})}function reject(code,message){results.reject=[code,message]}var _=require("underscore"),log=console.log.bind(console),shouldValidate=null!==realUserCtx&&null!==realUserCtx.name,results={channels:[],access:[],roles:[],reject:!1};try{syncFun(newDoc,oldDoc)}catch(x){if(x.forbidden)reject(403,x.forbidden);else{if(!x.unauthorized)throw x;reject(401,x.unauthorized)}}return results}); return regex.test(value); }

  • You can see some junk stuff appended to the validation regex.

Am I missing something here?

Support dynamic assignment of roles to users

Sync Gateway exposes the role function to sync functions, which allows roles to be assigned to users on the fly (more info). Add support for document definitions to supply role-assignment configuration that is applied after the user is authorized and the document's contents pass validation, but before channels are assigned (i.e. via the channel function).

For example (not to be considered final):

myDocType: {
  channels: ...,
  typeFilter: ...,
  propertyValidators: ...,
  accessAssignments: [
    {
      type: 'role',
      users: function(doc, oldDoc) {
        return doc.members;
      },
      roles: function(doc, oldDoc) {
        return doc.groups;
      }
    },
    {
      type: 'role',
      users: [ 'user1' ],
      roles: [ 'admins', 'power-users' ]
    }
  ]
},

Note that the type property is used to distinguish "role" assignments from "channel" assignments in the preceding example.

Related to #24 (Support dynamic assignment of channels to roles and users).

Output message of make-sync-function is incorrect

Output message appends the output path to the directory containing the make-sync-function

to reproduce:

~/sync-project$> node_modules/synctos/make-sync-function document-definitions/sync-doc-defn.js sync-functions/sync-function.js
Sync function written to ~/sync-project/document-definitions/node_modules/synctos/sync-functions/sync-function.js

Output should be:
Sync function written to ~/sync-project/sync-functions/sync-function.js
or
Sync function written to sync-functions/sync-function.js

Test helper convenience functions to build validation error messages

Currently a user of the test helper module has to manually construct the text of the error messages they expect to encounter when document validation fails. This requires that the caller knows exactly what the expected error message content will be and it makes their test cases brittle. For the sake of convenience and keeping things DRY, include convenience functions to automatically format each of the built in validation error messages for use in specs.

For example, instead of

testHelper.verifyDocumentNotCreated(
  doc,
  'myDocType',
  [
    'item "provider" must not be empty',
    'item "accountId" must not be less than 1',
    'property "unrecognized-property3" is not supported'
  ],
  [ 'channel1', 'channel2' ]);

one could call

testHelper.verifyDocumentNotCreated(
  doc,
  'myDocType',
  [
    testHelper.formatNotEmptyMessage('provider'),
    testHelper.formatMinimumValueMessage('accountId', 1),
    testHelper.formatUnsupportedPropertyMessage('unrecognized-property3')
  ],
  [ 'channel1', 'channel2' ]);

Support minimum/maximum size constraint on hashtable validation type

Just as the array property validation type allows one to constraint on the minimum and maximum length of array values (via the minimumLength and maximumLength constraints), allow one to specify the minimum and maximum number of elements that can be stored in a value that is declared with the hashtable validation type.

For example (not to be considered final):

propertyValidators: {
  accounts: {
    type: 'hashtable',
    required: true,
    minimumSize: 1,
    maximumSize: 2
  }
}

Helper function to determine whether a document is missing or deleted

Add a simple helper function (e.g. isDocumentMissingOrDeleted) that takes as input a document object and returns either true or false indicating whether that document is missing (i.e. it is null or undefined) or deleted (i.e. its "_deleted" property is true). This is useful for those cases where one wants to treat both cases (e.g. when handling the oldDoc parameter) as equivalent.

Macros for date and date/time property range constraints

Support some basic macros in the range constraints (e.g. minimumValue, maximumValue) for date and datetime validation types.

For example:

  • today: midnight UTC on today's date
  • tomorrow: midnight UTC on tomorrow's date
  • yesterday: midnight UTC on yesterday's date
  • now: the current moment in time

Some example usages:

myDocType: {
  channels: ...,
  typeFilter: ...,
  propertyValidators: {
    createdAt: {
      type: 'datetime',
      maximumValue: now
    },
    dueDate: {
      type: 'date',
      minimumValue: tomorrow
    }
  }
}

Option to initialize test helper module with document definition file

Currently the test helper module is initialized with the path to a sync function file generated by synctos via the init function. However, it would be convenient for a caller to have the option to use the document definitions file's path instead and have the test helper module automatically generate the sync function in memory to reduce the overhead for setting up a testing environment.

Support custom actions to be executed on a document type

Allow a document definition to define fully custom action functions to be executed when the sync function is run. It should allow at least two different types:

  • custom functions that are run after the document's type is identified, but before user authorization is verified (e.g. customPreAction)
  • custom functions that are run after user authorization is verified and the document's contents pass validation, but before channels are assigned (e.g. customPostAction)

For example (not to be considered final):

myDocType: {
  channels: ...,
  typeFilter: ...,
  propertyValidators: ...,
  customPreAction: function(doc, oldDoc) {
    // Do something fancy before any validation is performed
  },
  customPostAction: function(doc, oldDoc) {
    // Do something fancy after all validation has passed
  }
}

Access assignments should receive null when old document is deleted

To simplify handling of dynamic access assignments when replacing an old document that was deleted, the user's access assignment functions (i.e. users, roles and channels) should receive an oldDoc parameter value of null rather than an object whose _deleted property is set to true. That way the user does not have to implement special handling for deleted vs. non-existant oldDoc. This is the same way that a deleted oldDoc is handled when passed to a typeFilter function.

See issue #24 for more info on access assignments.

Issue identified in synctos 1.3.0.

Finer grained control over whether null and missing values are accepted

Currently it is possible to prevent a property or element's value from being null or missing/undefined by assigning the required constraint to the item in question in the document definition. But it is not possible to specify only that an item must not be null or that it must not be missing.

This is generally desirable as the default behaviour since many programming languages are incapable of distinguishing between null and missing values. Consider that, in such languages, if a JSON deserializer encounters a missing value, the corresponding property in the resulting object will be null. Furthermore, it is up to a JSON serializer to decide whether to omit a null value (i.e. in the resulting JSON, the property value in question would be missing altogether rather than set to null) or to write it as null. In the face of this uncertainty, it is logical to, by default, favour a definition of "required" that treats null and missing values as equal, as is the case with the required constraint.

However, there may be cases where it makes sense to allow a value of null but not a missing value or vice versa when expecting documents from client apps written in languages that distinguish between null and missing values. For example (not to be considered final):

propertyValidators: {
  birthdate: {
    type: 'date',
    // The value may be null, but it must not be missing
    mustNotBeMissing: true
  },
  children: {
    type: 'array',
    arrayElementsValidator: {
      type: 'string',
      // Null values are not allowed in the array
      mustNotBeNull: true
    }
  },
  characteristics: {
    type: 'hashtable',
    hashtableValuesValidator: {
      type: 'integer',
      // Null values are not allowed in the hashtable
      mustNotBeNull: true
    }
  }
}

Support dynamic item validation constraints

Currently validation constraints for properties/elements (e.g. minimumValue, maximumValue, regexPattern, etc.) may be defined only as static values. This can be limiting when, for example, you'd like to ensure that a document's "date" property value is no earlier than today's date (a value that will sometimes differ between executions of the sync function). Instead, allow each validation constraint to be defined as a function that dynamically constructs and returns the value that is to be applied at runtime. For example (not to be considered final):

propertyValidators: {
  startDate: {
    type: 'date',
    // The date may not be later than today's date
    maximumValue: function(doc, oldDoc, value, oldValue) {
      var nowLocal = new Date();
      var todayUtc = Date.UTC(nowLocal.getFullYear(), nowLocal.getMonth(), nowLocal.getDate());

      return todayUtc;
    }
  },
  sequence: {
    type: 'integer',
    required: true,
    // The value must always increase by at least one with each revision
    minimumValue: function(doc, oldDoc, value, oldValue) {
      return !isValueNullOrUndefined(oldValue) ? oldValue + 1 : 0;
    }
  },
  category: {
    type: 'enum',
    required: true,
    // The valid categories depends on the beginning of the document's ID
    predefinedValues: function(doc, oldDoc, value, oldValue) {
      return (doc._id.indexOf('foo-') === 0) ? [ 1, 2, 3 ] : [ 'a', 'b', 'c' ];
    }
  },
  referenceId: {
    type: 'string',
    required: true,
    // The reference ID must be constructed from the value of the category field
    regexPattern: function(doc, oldDoc, value, oldValue) {
      return new RegExp('^foobar-' + doc.category + '-[a-zA-Z_-]+$');
    }
  }
}

Option to uglify a generated sync function

By default, synctos generates sync functions that are optimized for human readability with proper indentation and plenty of inline comments. However, some may prefer to forego such readability for a more compact representation when a sync function is inserted into a Sync Gateway configuration file. To that end, add a commandline option to the make-sync-function script to uglify a generated sync function (e.g. using uglify-js).

Tangentially related to #35.

Move test-helper module documentation to the top of the file

The test-helper module is currently organized such that all of the documentation for exported function is at the very bottom. Reorganize so that exported functions are documented at the top of the module file instead. That way, developers that are attempting to use the module will be able to more easily determine its functionality.

Parameter to indicate that an item cannot be modified if it has a value

Similar to the immutable constraint on items, such a constraint (e.g. immutableIfSet) would only prevent modifications to properties and elements that already have a value assigned. In other words, the value of an item that is protected by this constraint can only be set if it is currently undefined (or perhaps null as well?). Does not apply to documents.

Conditional validation type

Feature Request

Description

Currently each property or element can be defined with only one validation type (e.g. string, integer, datetime, etc.). However, there are cases where it may be beneficial to allow a value to be one of several types (e.g. a date as either an ISO 8601 date/time string or as a Unix timestamp) while still being able to set some constraints on them.

Add a new validation type (e.g. conditional) that selects one of several different validators to apply to a property or element based on some condition (specified as a function).

Examples

propertyValidators: {
  date: {
    type: 'conditional',
    required: true,
    skipValidationWhenValueUnchangedStrict: true,
    validationCandidates: [
      {
        condition: function(doc, oldDoc, currentItemEntry, validationItemStack) {
          return typeof currentItemEntry.itemValue === 'string';
        },
        validator: {
          type: 'datetime',
          minimumValue: '1970-01-01T00:00Z'
        }
      },
      {
        condition: function(doc, oldDoc, currentItemEntry, validationItemStack) {
          return typeof currentItemEntry.itemValue === 'number';
        },
        validator: {
          type: 'integer',
          minimumValue: 0
        }
      }
    ]
  }
}

Item constraint that requires an exact value match

With the introduction of #94 (Support dynamic item validation constraints), it is possible to specify a dynamically computed value for a property or element constraint (e.g. minimumValue: function(doc, oldDoc, value, oldValue) { ... }). In some cases, it may be useful to include a constraint that dynamically specifies a value that must be matched exactly by the property/element value.

For example (not to be considered final):

propertyValidators: {
  processorId: {
    type: 'string',
    required: true,
    mustEqual: function(doc, oldDoc, value, oldValue) {
      var docIdRegex = /^paymentProcessor\.([A-Za-z0-9_-]+)$/;
      var regexMatches = typeRegex.exec(doc._id);

      return (regexMatches.length > 1) ? regexMatches[1] : '';
    }
  }
}

Note that, while the primary use case may be as a dynamic item constraint, it should also accept a static value, as with all other item constraints, for the sake of consistency.

Also note that a constraint value of null should be treated literally. In other words, if a mustEqual constraint specifies null, then the property/element's value must be null as well. But a constraint of undefined means that the mustEqual constraint is not applicable.

Decompose specifications file for sample document definitions

The specs for the sample document definitions are all defined in a single file: test/sample-sync-doc-definitions-spec.js. The file's large size makes it unwieldy to navigate to the test cases that one might be interested in. Decompose the file such that the specification for each document definition is defined in its own file. Consider using npm modules to accomplish this task.

Final argument of custom validation constraint receives incorrect value

The customValidation constraint accepts four arguments. The last argument is meant to contain a copy of the stack of validation items minus the item that is currently under evaluation. In other words, the first element of the array should contain the document object and the last element should contain the immediate parent of the item under evaluation.

However, it currently contains a copy of the stack of validation items minus all elements other than the item under evaluation. In other words, the array consists of a single element that contains the item under evaluation.

For example, with the following document definition

{
  customValidationDoc: {
    typeFilter: simpleTypeFilter,
    channels: { write: 'write' },
    propertyValidators: {
      baseProp: {
        type: 'object',
        propertyValidators: {
          customValidationProp: {
            type: 'string',
            customValidation: function(doc, oldDoc, currentItemElement, validationItemStack) {
              return [ ];
            }
          }
        }
      }
    }
  }
}

the validationItemStack parameter should be an array representing the elements [doc, baseProp] but instead it is an array representing the elements [customValidationProp].

Option to output a generated sync function as a single-line JSON string

Add a commandline option to the make-sync-function script that, instead of outputting the result as a multi-line JavaScript block, escapes all line ending, backslash and double quote characters so that the output is encased in a single-line JSON-compatible string written to the target file. This option is useful to produce Sync Gateway JSON configurations that don't need to rely on the non-standard backtick/backquote ( ` ) character for multi-line strings in a JSON file (more info).

For example, invocation should look like the following:

./make-sync-function --json-string /path/to/my-doc-definitions.js /path/to/my-new-sync-function.txt

or

./make-sync-function -j /path/to/my-doc-definitions.js /path/to/my-new-sync-function.txt

Tangentially related to #70.

Support document authorization by role

Currently document type definitions are limited to providing a list of channels to which a user must belong (either directly or indirectly through membership in a role that includes the channel(s)) in order to create/replace/delete documents of that type. However, Sync Gateway also exposes the requireRole function to sync functions, which specifies that the user must belong to one of the roles provided. Add configuration support for defining the list of roles that a document requires.

See Authorizing Document Updates for more info. Related to #23.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.