diff --git a/dadi/lib/controller/documents.js b/dadi/lib/controller/documents.js index 253ffbe8..56eddb92 100644 --- a/dadi/lib/controller/documents.js +++ b/dadi/lib/controller/documents.js @@ -59,6 +59,7 @@ Collection.prototype.delete = function (req, res, next) { this.model.delete({ client: req.dadiApiClient, + description: req.body && req.body.description, query, req }).then(({deletedCount, totalCount}) => { @@ -104,7 +105,8 @@ Collection.prototype.get = function (req, res, next) { language: options.lang, query, options: queryOptions, - req + req, + version: req.params.id && options.version }).then(results => { return done(null, results) }).catch(error => { @@ -135,6 +137,7 @@ Collection.prototype.post = function (req, res, next) { if (req.params.id || req.body.update) { internals._lastModifiedBy = req.dadiApiClient && req.dadiApiClient.clientId + let description let query = {} let update = {} @@ -142,6 +145,7 @@ Collection.prototype.post = function (req, res, next) { query._id = req.params.id update = req.body } else { + description = req.body.description query = req.body.query update = req.body.update } @@ -154,6 +158,7 @@ Collection.prototype.post = function (req, res, next) { return this.model.update({ client: req.dadiApiClient, compose: options.compose, + description, internals, query, req, @@ -214,7 +219,7 @@ Collection.prototype.registerRoutes = function (route, filePath) { }) // Creating generic route. - this.server.app.use(`${route}/:id(${this.ID_PATTERN})?/:action(count|search|stats)?`, (req, res, next) => { + this.server.app.use(`${route}/:id(${this.ID_PATTERN})?/:action(count|search|stats|versions)?`, (req, res, next) => { try { // Map request method to controller method. let method = req.params.action || (req.method && req.method.toLowerCase()) @@ -252,7 +257,24 @@ Collection.prototype.stats = function (req, res, next) { Collection.prototype.unregisterRoutes = function (route) { this.server.app.unuse(`${route}/config`) - this.server.app.unuse(`${route}/:id(${this.ID_PATTERN})?/:action(count|search|stats)?`) + this.server.app.unuse(`${route}/:id(${this.ID_PATTERN})?/:action(count|search|stats|versions)?`) +} + +Collection.prototype.versions = function (req, res, next) { + let method = req.method && req.method.toLowerCase() + + if (method !== 'get') { + return next() + } + + this.model.getVersions({ + client: req.dadiApiClient, + documentId: req.params.id + }).then(response => { + return help.sendBackJSON(200, res, next)(null, response) + }).catch(error => { + return help.sendBackJSON(null, res, next)(error) + }) } module.exports = function (model, server) { diff --git a/dadi/lib/controller/index.js b/dadi/lib/controller/index.js index e2c45546..5e611ecc 100755 --- a/dadi/lib/controller/index.js +++ b/dadi/lib/controller/index.js @@ -136,15 +136,6 @@ Controller.prototype._prepareQueryOptions = function (options) { queryOptions.compose = options.compose } - // History. - if (options.includeHistory) { - queryOptions.includeHistory = options.includeHistory === 'true' - - if (options.historyFilters) { - queryOptions.historyFilters = options.historyFilters - } - } - // sorting let sort = {} let sortOptions = help.isJSON(options.sort) diff --git a/dadi/lib/model/collections/create.js b/dadi/lib/model/collections/create.js index 6de12bc7..4c428003 100644 --- a/dadi/lib/model/collections/create.js +++ b/dadi/lib/model/collections/create.js @@ -96,14 +96,6 @@ function create ({ Object.assign(document, internals) } - // Add placeholder for document history. - if (this.history) { - document._history = [] - } - - // Add initial revision number. - document._version = 1 - return Object.keys(document).reduce((documentTransform, field) => { if (field === '_id') { return documentTransform diff --git a/dadi/lib/model/collections/delete.js b/dadi/lib/model/collections/delete.js index afe3a215..706d7e01 100644 --- a/dadi/lib/model/collections/delete.js +++ b/dadi/lib/model/collections/delete.js @@ -12,6 +12,7 @@ const logger = require('@dadi/logger') * Deletes documents from the database. * * @param {Object} client - client to check permissions for + * @param {String} description - optional update description * @param {Object} query - query to find documents to delete * @param {Object} req - request to be passed to hooks * @param {Boolean} validate - whether to run validation @@ -19,6 +20,7 @@ const logger = require('@dadi/logger') */ function deleteFn ({ client, + description, query, req, validate = true @@ -103,13 +105,11 @@ function deleteFn ({ return Promise.reject(error) } - // Create a revision for each of the documents about to be deleted. - if (this.history && deletedDocuments.length > 0) { - return this.history.createEach( - deletedDocuments, - 'delete', - this - ) + // Create a revision for each of the updated documents. + if (this.history) { + return this.history.addVersion(deletedDocuments, { + description + }) } }).then(() => { // Run any `beforeDelete` hooks. diff --git a/dadi/lib/model/collections/find.js b/dadi/lib/model/collections/find.js index a038e596..e41b4409 100644 --- a/dadi/lib/model/collections/find.js +++ b/dadi/lib/model/collections/find.js @@ -24,17 +24,21 @@ const debug = require('debug')('api:model') /** * Finds documents in the database. * - * @param {Object} client - client to check permissions for - * @param {String} language - ISO code for the language to translate documents to - * @param {Object} query - query to match documents against - * @param {Object} options + * @param {Object} client - client to check permissions for + * @param {Boolean} isRestIDQuery - whether the query targets a specific document by ID + * @param {String} language - ISO code for the language to translate documents to + * @param {Object} query - query to match documents against + * @param {Object} options + * @param {Number} version - version of the document to retrieve * @return {Promise} */ function find ({ client, + isRestIDQuery, language, query = {}, - options = {} + options = {}, + version } = {}) { if (!this.connection.db) { return Promise.reject( @@ -120,27 +124,23 @@ function find ({ queryFields = fields + const queryOptions = Object.assign({}, options, { + fields: queryFields + }) + + if (isRestIDQuery && version && this.history) { + return this.history.getVersion(version, queryOptions) + } + return this._transformQuery(query, options).then(query => { return this.connection.db.find({ query, collection: this.name, - options: Object.assign({}, options, { - compose: undefined, - fields: queryFields, - historyFilters: undefined - }), + options: queryOptions, schema: this.schema, settings: this.settings }) }) - }).then(response => { - if (options.includeHistory) { - options.includeHistory = undefined - - return this._injectHistory(response, options) - } - - return response }) } diff --git a/dadi/lib/model/collections/get.js b/dadi/lib/model/collections/get.js index 14407241..f8ed3a09 100644 --- a/dadi/lib/model/collections/get.js +++ b/dadi/lib/model/collections/get.js @@ -30,6 +30,7 @@ const logger = require('@dadi/logger') * @param {Object} options * @param {Boolean} rawOutput - whether to bypass formatting routine * @param {Object} req - request object to pass to hooks + * @param {Number} version - version of the document to retrieve * @return {Promise} */ function get ({ @@ -38,7 +39,8 @@ function get ({ query = {}, options = {}, rawOutput = false, - req + req, + version }) { // Is this a RESTful query by ID? let isRestIDQuery = req && req.params && req.params.id @@ -73,9 +75,11 @@ function get ({ }).then(query => { return this.find({ client, + isRestIDQuery, language, query, - options + options, + version }) }).then(({metadata, results}) => { if (isRestIDQuery && results.length === 0) { diff --git a/dadi/lib/model/collections/getVersions.js b/dadi/lib/model/collections/getVersions.js new file mode 100644 index 00000000..5557a0c7 --- /dev/null +++ b/dadi/lib/model/collections/getVersions.js @@ -0,0 +1,63 @@ +/** + * Returns stats relating to the collection. + * + * @param {Object} client - client to check permissions for + * @param {String} documentId - ID of the object to get versions for + * @return {Promise} + */ +function getVersions ({ + client, + documentId +} = {}) { + if (!this.connection.db) { + return Promise.reject( + new Error('DB_DISCONNECTED') + ) + } + + let response = { + results: [] + } + + return this.validateAccess({ + client, + type: 'read' + }).then(() => { + if (!this.history) { + const error = new Error('History not enabled for collection') + + error.statusCode = 404 + + return Promise.reject(error) + } + + return this.history.getVersions(documentId).then(({results}) => { + if (results.length === 0) { + return this.count({ + client, + query: { + _id: documentId + } + }).then(({metadata}) => { + if (metadata.totalCount === 0) { + const error = new Error('Document not found') + + error.statusCode = 404 + + return Promise.reject(error) + } + + return results + }) + } + + return results + }).then(results => { + response.results = results + + return response + }) + }) +} + +module.exports = getVersions diff --git a/dadi/lib/model/collections/update.js b/dadi/lib/model/collections/update.js index 15426d7b..e99b2fee 100644 --- a/dadi/lib/model/collections/update.js +++ b/dadi/lib/model/collections/update.js @@ -35,6 +35,7 @@ const logger = require('@dadi/logger') * @param {Object} client - client to check permissions for * @param {Boolean|Number} compose - the composition settings for the result * @param {Object} query - query to match documents against + * @param {String} description - optional update description * @param {Object} update - properties to update documents with * @param {Object} internals - internal properties to inject in documents * @param {Boolean} rawOutput - whether to bypass output formatting @@ -46,12 +47,13 @@ const logger = require('@dadi/logger') function update ({ client, compose = true, - query = {}, - update, + description, internals = {}, + query = {}, rawOutput = false, removeInternalProperties = true, req, + update, validate = true }) { debug( @@ -200,8 +202,7 @@ function update ({ query, schema: this.schema, update: { - $set: update, - $inc: { _version: 1 } + $set: update } }) }).then(({matchedCount}) => { @@ -212,15 +213,6 @@ function update ({ return Promise.reject(error) } - - // Create a revision for each of the updated documents. - if (this.history) { - return this.history.createEach( - updatedDocuments, - 'update', - this - ) - } }).then(() => { let updatedDocumentsQuery = { _id: { @@ -266,6 +258,15 @@ function update ({ return data }) + }).then(response => { + // Create a revision for each of the updated documents. + if (this.history && updatedDocuments.length > 0) { + return this.history.addVersion(updatedDocuments, { + description + }).then(historyResponse => response) + } + + return response }).catch(error => { // Dealing with the case of an impossible query. We can simply return // an empty result set here. diff --git a/dadi/lib/model/history.js b/dadi/lib/model/history.js index ba301b3f..8a8337fe 100644 --- a/dadi/lib/model/history.js +++ b/dadi/lib/model/history.js @@ -1,70 +1,96 @@ -const debug = require('debug')('api:history') -const deepClone = require('deep-clone') +const config = require('./../../../config') +const Connection = require('./connection') -const History = function (model) { - this.model = model +/** + * Document history manager. + * + * @param {String} options.database Name of the database + * @param {String} options.name Name of the collection + */ +const History = function ({database, name}) { + this.name = name + this.connection = Connection( + { + collection: name, + database + }, + name, + config.get('datastore') + ) } -History.prototype.create = function (obj, model, done) { - // create copy of original - let revisionObj = deepClone(obj) - - revisionObj._originalDocumentId = obj._id +/** + * Stores a version of a document as a diff. + * + * @param {Array} documents Documents to add + * @param {String} options.description Optional message describing the operation + */ +History.prototype.addVersion = function (documents, {description}) { + const versions = documents.map(document => { + let version = Object.assign({}, document, { + _document: document._id + }) - delete revisionObj._id + delete version._id - const _done = function (database) { - if (Array.isArray(database.settings.internalProperties)) { - database.settings.internalProperties.forEach(property => { - delete revisionObj[property] - }) + if (typeof description === 'string' && description.length > 0) { + version._changeDescription = description } - database.insert({ - data: revisionObj, - collection: model.revisionCollection, - schema: model.schema, - settings: model.settings - }).then((doc) => { - debug('inserted %o', doc) - - // TODO: remove mongo options - database.update({ - query: { _id: obj._id }, - collection: model.name, - update: { $push: { '_history': doc[0]._id.toString() } }, - schema: model.schema - }).then((result) => { - return done(null, obj) - }).catch((err) => { - done(err) - }) - }).catch((err) => { - done(err) - }) - } - - if (model.connection.db) return _done(model.connection.db) + return version + }) - // if the db is not connected queue the insert - model.connection.once('connect', _done) + return this.connection.db.insert({ + data: versions, + collection: this.name + }) } -History.prototype.createEach = function (objs, action, model, done) { - return new Promise((resolve, reject) => { - if (objs.length === 0) return resolve() - - objs.forEach((obj, index, array) => { - obj._action = action +/** + * Returns a previous version of a given document. + * + * @param {String} version ID of a previous version + * @return {Object} + */ +History.prototype.getVersion = function (version, options = {}) { + return this.connection.db.find({ + collection: this.name, + options, + query: { + _id: version + } + }).then(response => { + response.metadata.version = version + response.results = response.results.map(result => { + result._id = result._document - this.create(obj, model, (err, doc) => { - if (err) return reject(err) + delete result._document - if (index === array.length - 1) { - return resolve() - } - }) + return result }) + + return response + }) +} + +/** + * Gets all versions available for a given document. + * + * @param {String} documentId + * @return {Array} + */ +History.prototype.getVersions = function (documentId) { + return this.connection.db.find({ + collection: this.name, + options: { + fields: { + _document: 1, + _changeDescription: 1 + } + }, + query: { + _document: documentId + } }) } diff --git a/dadi/lib/model/index.js b/dadi/lib/model/index.js index ed14a678..c0439f7d 100755 --- a/dadi/lib/model/index.js +++ b/dadi/lib/model/index.js @@ -10,6 +10,19 @@ const logger = require('@dadi/logger') const Search = require('./../search') const Validator = require('@dadi/api-validator') +const DEFAULT_HISTORY_COLLECTION_SUFFIX = 'Versions' +const INTERNAL_PROPERTIES = [ + '_apiVersion', + '_composed', + '_createdAt', + '_createdBy', + '_history', + '_id', + '_lastModifiedAt', + '_lastModifiedBy', + '_version' +] + /** * Block with metadata pertaining to an API collection. * @@ -37,18 +50,6 @@ let _models = {} * @classdesc */ const Model = function (name, schema, connection, settings) { - this.internalProperties = [ - '_apiVersion', - '_composed', - '_createdAt', - '_createdBy', - '_history', - '_id', - '_lastModifiedAt', - '_lastModifiedBy', - '_version' - ] - this.acl = require('./acl') // Attach collection name. @@ -94,36 +95,31 @@ const Model = function (name, schema, connection, settings) { ] } - // Setup history context unless requested not to. - this.storeRevisions = this.settings.storeRevisions !== false - - if (this.storeRevisions) { - this.history = new History(this) + // Unless `enableVersioning` (or `storeRevisions`, for backward-compatibility) + // is explicitly set to `false`, we enable history. + if ( + this.settings.enableVersioning !== false && + this.settings.storeRevisions !== false + ) { + let versioningCollection = this.settings.versioningCollection || + this.settings.revisionCollection || + this.name + DEFAULT_HISTORY_COLLECTION_SUFFIX - // Define the name of the revision collection for this model. - // If no value is specified, use the name of the model with - // the 'History' suffix. - this.revisionCollection = this.settings.revisionCollection - ? this.settings.revisionCollection - : this.name + 'History' + this.history = new History({ + database: this.settings.database, + name: versioningCollection + }) } - // Create connection for this model. - if (connection) { - this.connection = connection - } else { - let connectionOptions = { + // Create connection for this model, if it doesn't exist. + this.connection = connection || Connection( + { collection: this.name, - database: this.settings.database, - revisionCollection: this.revisionCollection - } - - this.connection = Connection( - connectionOptions, - this.name, - config.get('datastore') - ) - } + database: this.settings.database + }, + this.name, + config.get('datastore') + ) this.connection.setMaxListeners(35) @@ -672,7 +668,7 @@ Model.prototype.isKeyValid = function (key) { */ Model.prototype.removeInternalProperties = function (document) { return Object.keys(document).reduce((output, field) => { - if (!this.internalProperties.includes(field)) { + if (!INTERNAL_PROPERTIES.includes(field)) { output[field] = document[field] } @@ -997,8 +993,7 @@ Model.prototype.get = require('./collections/get') Model.prototype.getIndexes = require('./collections/getIndexes') Model.prototype.getRevisions = require('./collections/getRevisions') Model.prototype.getStats = require('./collections/getStats') -Model.prototype.revisions = require('./collections/getRevisions') // (!) Deprecated in favour of `getRevisions` -Model.prototype.stats = require('./collections/getStats') // (!) Deprecated in favour of `getStats` +Model.prototype.getVersions = require('./collections/getVersions') Model.prototype.update = require('./collections/update') Model.prototype.search = require('./search') diff --git a/features.json b/features.json index 951a1099..eae32691 100644 --- a/features.json +++ b/features.json @@ -4,5 +4,6 @@ "i18nv2", "collectionsv1", "validationv1", - "mediafieldv1" + "mediafieldv1", + "versionsv1" ] \ No newline at end of file diff --git a/test/acceptance/document-versioning.js b/test/acceptance/document-versioning.js new file mode 100644 index 00000000..e69b5b2d --- /dev/null +++ b/test/acceptance/document-versioning.js @@ -0,0 +1,571 @@ +const should = require('should') +const sinon = require('sinon') +const path = require('path') +const request = require('supertest') +const EventEmitter = require('events').EventEmitter +const config = require(path.join(__dirname, '/../../config')) +const help = require(path.join(__dirname, '/help')) +const app = require(path.join(__dirname, '/../../dadi/lib/')) + +const client = request(`http://${config.get('server.host')}:${config.get('server.port')}`) +const FAKE_ID = '5c334a60139c7e48eb44a9bb' + +let bearerToken + +describe('Document versioning', function () { + this.timeout(4000) + + let cleanupFn + + beforeEach(done => { + help.dropDatabase('testdb', err => { + if (err) return done(err) + + let schema1 = { + fields: { + name: { + type: 'String' + }, + surname: { + type: 'String' + }, + occupation: { + type: 'String' + }, + object: { + type: 'Object' + }, + reference: { + type: 'Reference', + settings: { + collection: 'test-history-disabled' + } + } + } + } + let schema2 = Object.assign({}, schema1, { + settings: { + enableVersioning: true + } + }) + + help.writeTempFile( + 'temp-workspace/collections/vtest/testdb/collection.test-history-enabled.json', + schema1, + callback1 => { + help.writeTempFile( + 'temp-workspace/collections/vtest/testdb/collection.test-history-disabled.json', + schema2, + callback2 => { + cleanupFn = () => { + callback1() + callback2() + } + + app.start(() => { + help.getBearerTokenWithAccessType('admin', (err, token) => { + if (err) return done(err) + + bearerToken = token + + done() + }) + }) + } + ) + } + ) + }) + }) + + afterEach(done => { + app.stop(() => { + cleanupFn() + done() + }) + }) + + describe('Versions endpoint', () => { + it('should return 404 when listing versions for a document that does not exist', done => { + client + .get(`/vtest/testdb/test-history-enabled/${FAKE_ID}/versions`) + .set('Authorization', `Bearer ${bearerToken}`) + .expect(404, done) + }) + + it('should return an empty result set when the document does not have previous versions', done => { + let document = { + name: 'John', + surname: 'Doe' + } + + client + .post('/vtest/testdb/test-history-enabled') + .set('Authorization', `Bearer ${bearerToken}`) + .send(document) + .end((err, res) => { + if (err) return done(err) + + let id = res.body.results[0]._id + + client + .get(`/vtest/testdb/test-history-enabled/${id}/versions`) + .set('Authorization', `Bearer ${bearerToken}`) + .expect(200) + .end((err, res) => { + if (err) return done(err) + + const {results} = res.body + + results.length.should.eql(0) + + done() + }) + }) + }) + + it('should list document versions', done => { + let document = { + name: 'John', + surname: 'Doe' + } + let updates = [ + { name: 'Jane' }, + { surname: 'Fonda' } + ] + + client + .post('/vtest/testdb/test-history-enabled') + .set('Authorization', `Bearer ${bearerToken}`) + .send(document) + .end((err, res) => { + if (err) return done(err) + + let id = res.body.results[0]._id + + client + .put(`/vtest/testdb/test-history-enabled/${id}`) + .set('Authorization', `Bearer ${bearerToken}`) + .send(updates[0]) + .end((err, res) => { + if (err) return done(err) + + client + .put(`/vtest/testdb/test-history-enabled/${id}`) + .set('Authorization', `Bearer ${bearerToken}`) + .send(updates[1]) + .end((err, res) => { + if (err) return done(err) + + client + .get(`/vtest/testdb/test-history-enabled/${id}/versions`) + .set('Authorization', `Bearer ${bearerToken}`) + .expect(200) + .end((err, res) => { + if (err) return done(err) + + const {results} = res.body + + results.length.should.eql(2) + results[0]._document.should.eql(id) + results[1]._document.should.eql(id) + + done() + }) + }) + }) + }) + }) + + it('should list document versions and show update description when available', done => { + let document = { + name: 'John', + surname: 'Doe' + } + let updates = [ + { + description: 'Update first name', + update: { name: 'Jane' } + }, + { + description: 'Update surname', + update: { surname: 'Fonda' } + } + ] + + client + .post('/vtest/testdb/test-history-enabled') + .set('Authorization', `Bearer ${bearerToken}`) + .send(document) + .end((err, res) => { + if (err) return done(err) + + let id = res.body.results[0]._id + + client + .put('/vtest/testdb/test-history-enabled') + .set('Authorization', `Bearer ${bearerToken}`) + .send(Object.assign({}, updates[0], { + query: { + _id: id + } + })) + .end((err, res) => { + if (err) return done(err) + + client + .put('/vtest/testdb/test-history-enabled') + .set('Authorization', `Bearer ${bearerToken}`) + .send(Object.assign({}, updates[1], { + query: { + _id: id + } + })) + .end((err, res) => { + if (err) return done(err) + + client + .get(`/vtest/testdb/test-history-enabled/${id}/versions`) + .set('Authorization', `Bearer ${bearerToken}`) + .expect(200) + .end((err, res) => { + if (err) return done(err) + + const {results} = res.body + + results.length.should.eql(2) + results[0]._document.should.eql(id) + results[0]._changeDescription.should.eql(updates[0].description) + results[1]._document.should.eql(id) + results[1]._changeDescription.should.eql(updates[1].description) + + done() + }) + }) + }) + }) + }) + }) + + describe('Rollback to previous versions', () => { + it('should rollback to a previous version where a property was added, changed and removed multiple times', done => { + let original = { + name: 'John', + surname: 'Doe' + } + + client + .post('/vtest/testdb/test-history-enabled') + .set('Authorization', `Bearer ${bearerToken}`) + .send(original) + .end((err, res) => { + if (err) return done(err) + + let id = res.body.results[0]._id + let updates = [ + { + endpoint: `/vtest/testdb/test-history-enabled/${id}`, + body: { + surname: null + } + }, + { + endpoint: `/vtest/testdb/test-history-enabled/${id}`, + body: { + surname: 'One' + } + }, + { + endpoint: `/vtest/testdb/test-history-enabled/${id}`, + body: { + surname: null + } + }, + { + endpoint: `/vtest/testdb/test-history-enabled/${id}`, + body: { + surname: 'Two' + } + }, + { + endpoint: `/vtest/testdb/test-history-enabled/${id}`, + body: { + surname: 'Three' + } + }, + { + endpoint: `/vtest/testdb/test-history-enabled/${id}`, + body: { + surname: null + } + }, + { + endpoint: `/vtest/testdb/test-history-enabled/${id}`, + body: { + surname: 'Four' + } + } + ] + + help.bulkRequest({ + method: 'put', + requests: updates, + token: bearerToken + }).then(() => { + client + .get(`/vtest/testdb/test-history-enabled/${id}/versions`) + .set('Authorization', `Bearer ${bearerToken}`) + .expect(200) + .end((err, res) => { + if (err) return done(err) + + const {results} = res.body + const getRequests = results.map(result => `/vtest/testdb/test-history-enabled/${id}?version=${result._id}`) + + help.bulkRequest({ + method: 'get', + requests: getRequests, + token: bearerToken + }).then(responses => { + responses.length.should.eql(updates.length) + responses[0].results[0].surname.should.eql('Doe') + should.not.exist(responses[1].results[0].surname) + responses[2].results[0].surname.should.eql('One') + should.not.exist(responses[3].results[0].surname) + responses[4].results[0].surname.should.eql('Two') + responses[5].results[0].surname.should.eql('Three') + should.not.exist(responses[6].results[0].surname) + + done() + }) + }) + }) + }) + }) + + it('should rollback to a previous version and compose Reference fields accordingly', done => { + const original = { + name: 'Eduardo', + surname: 'Bouças' + } + const originalReference = { + name: 'James', + surname: 'Lambie' + } + const modifiedReference = { + name: 'David', + surname: 'Longworth' + } + + client + .post('/vtest/testdb/test-history-disabled') + .set('Authorization', `Bearer ${bearerToken}`) + .send(originalReference) + .expect(200) + .end((err, res) => { + if (err) return done(err) + + let originalReferenceID = res.body.results[0]._id + + client + .post('/vtest/testdb/test-history-disabled') + .set('Authorization', `Bearer ${bearerToken}`) + .send(modifiedReference) + .expect(200) + .end((err, res) => { + if (err) return done(err) + + let modifiedReferenceID = res.body.results[0]._id + let payload = Object.assign(original, { + reference: originalReferenceID + }) + + client + .post('/vtest/testdb/test-history-enabled') + .set('Authorization', `Bearer ${bearerToken}`) + .send(payload) + .end((err, res) => { + if (err) return done(err) + + let id = res.body.results[0]._id + + client + .get(`/vtest/testdb/test-history-enabled/${id}?compose=true`) + .set('Authorization', `Bearer ${bearerToken}`) + .end((err, res) => { + if (err) return done(err) + + const {results} = res.body + + results.length.should.eql(1) + results[0].reference.name.should.eql(originalReference.name) + results[0].reference.surname.should.eql(originalReference.surname) + + client + .put(`/vtest/testdb/test-history-enabled/${id}`) + .set('Authorization', `Bearer ${bearerToken}`) + .send({ + reference: modifiedReferenceID, + surname: 'Bouças II' + }) + .end((err, res) => { + if (err) return done(err) + + client + .get(`/vtest/testdb/test-history-enabled/${id}?compose=true`) + .set('Authorization', `Bearer ${bearerToken}`) + .end((err, res) => { + if (err) return done(err) + + const {results} = res.body + + results.length.should.eql(1) + results[0].surname.should.eql('Bouças II') + results[0].reference.name.should.eql(modifiedReference.name) + results[0].reference.surname.should.eql(modifiedReference.surname) + + client + .get(`/vtest/testdb/test-history-enabled/${id}/versions`) + .set('Authorization', `Bearer ${bearerToken}`) + .expect(200) + .end((err, res) => { + if (err) return done(err) + + const {results} = res.body + const versionId = results[0]._id + + results.length.should.eql(1) + + client + .get(`/vtest/testdb/test-history-enabled/${id}?compose=true&version=${versionId}`) + .set('Authorization', `Bearer ${bearerToken}`) + .end((err, res) => { + if (err) return done(err) + + const {metadata, results} = res.body + + metadata.version.should.eql(versionId) + results[0].surname.should.eql(original.surname) + results[0].reference.name.should.eql(originalReference.name) + results[0].reference.surname.should.eql(originalReference.surname) + + done() + }) + }) + }) + }) + }) + }) + }) + }) + }) + + it('should rollback to a previous version containing Object fields', done => { + const original = { + name: 'Eduardo', + object: { + parent: { + child1: true, + child2: { + child3: false + }, + child3: 'The original string' + } + } + } + const modified = { + name: 'James', + object: { + parent: { + child1: { + child2: { + child3: 83, + child4: 'Some string' + } + }, + child5: { + child6: false, + child7: 1337 + }, + child3: 'The modified string' + }, + parent2: { + child8: 5000 + } + } + } + + client + .post('/vtest/testdb/test-history-enabled') + .set('Authorization', `Bearer ${bearerToken}`) + .send(original) + .end((err, res) => { + if (err) return done(err) + + let id = res.body.results[0]._id + + client + .get(`/vtest/testdb/test-history-enabled/${id}`) + .set('Authorization', `Bearer ${bearerToken}`) + .end((err, res) => { + if (err) return done(err) + + const {results} = res.body + + results.length.should.eql(1) + results[0].name.should.eql(original.name) + results[0].object.should.eql(original.object) + + client + .put(`/vtest/testdb/test-history-enabled/${id}`) + .set('Authorization', `Bearer ${bearerToken}`) + .send(modified) + .end((err, res) => { + if (err) return done(err) + + client + .get(`/vtest/testdb/test-history-enabled/${id}`) + .set('Authorization', `Bearer ${bearerToken}`) + .end((err, res) => { + if (err) return done(err) + + const {results} = res.body + + results.length.should.eql(1) + results[0].name.should.eql(modified.name) + results[0].object.should.eql(modified.object) + + client + .get(`/vtest/testdb/test-history-enabled/${id}/versions`) + .set('Authorization', `Bearer ${bearerToken}`) + .expect(200) + .end((err, res) => { + if (err) return done(err) + + const {results} = res.body + const versionId = results[0]._id + + results.length.should.eql(1) + + client + .get(`/vtest/testdb/test-history-enabled/${id}?version=${versionId}`) + .set('Authorization', `Bearer ${bearerToken}`) + .end((err, res) => { + if (err) return done(err) + + const {metadata, results} = res.body + + metadata.version.should.eql(versionId) + results[0].name.should.eql(original.name) + results[0].object.should.eql(original.object) + + done() + }) + }) + }) + }) + }) + }) + }) + }) +}) diff --git a/test/acceptance/help.js b/test/acceptance/help.js index 7885aab9..b4773db2 100755 --- a/test/acceptance/help.js +++ b/test/acceptance/help.js @@ -9,6 +9,32 @@ const _ = require('underscore') var clientCollectionName = config.get('auth.clientCollection') +module.exports.bulkRequest = function ({method = 'get', requests, token}) { + const client = request(`http://${config.get('server.host')}:${config.get('server.port')}`) + let results = [] + + return requests.reduce((result, request, index) => { + return result.then(() => { + return new Promise((resolve, reject) => { + let endpoint = typeof request === 'string' + ? request + : request.endpoint + + client[method](endpoint) + .set('Authorization', `Bearer ${token}`) + .send(request.body) + .end((err, res) => { + if (err) return reject(err) + + results[index] = res.body + + resolve(results) + }) + }) + }) + }, Promise.resolve()) +} + // create a document with random string via the api module.exports.createDoc = function (token, done) { request('http://' + config.get('server.host') + ':' + config.get('server.port')) diff --git a/test/acceptance/rest-endpoints/collections-api/get.js b/test/acceptance/rest-endpoints/collections-api/get.js index 27524bc0..d16b0088 100644 --- a/test/acceptance/rest-endpoints/collections-api/get.js +++ b/test/acceptance/rest-endpoints/collections-api/get.js @@ -828,155 +828,6 @@ describe('Collections API – GET', function () { }) }) - it('should add history to results when querystring param includeHistory=true', function (done) { - var client = request(connectionString) - - client - .post('/vtest/testdb/test-schema') - .set('Authorization', 'Bearer ' + bearerToken) - .send({field1: 'original field content'}) - .expect(200) - .end(function (err, res) { - if (err) return done(err) - - var doc = res.body.results[0] - var body = { - query: { _id: doc._id }, - update: {field1: 'updated'} - } - - client - .put('/vtest/testdb/test-schema/') - .set('Authorization', 'Bearer ' + bearerToken) - .send(body) - .expect(200) - .end(function (err, res) { - if (err) return done(err) - - res.body.results[0]._id.should.equal(doc._id) - res.body.results[0].field1.should.equal('updated') - - client - .get('/vtest/testdb/test-schema?includeHistory=true&filter={"_id": "' + doc._id + '"}') - .set('Authorization', 'Bearer ' + bearerToken) - .expect(200) - .expect('content-type', 'application/json') - .end(function (err, res) { - if (err) return done(err) - - res.body['results'].should.exist - res.body['results'].should.be.Array - res.body['results'][0]._history.should.exist - res.body['results'][0]._history[0].field1.should.eql('original field content') - done() - }) - }) - }) - }) - - it('should add history to results when querystring param includeHistory=true, translating internal fields to the prefix defined in config', function (done) { - var originalPrefix = config.get('internalFieldsPrefix') - var client = request(connectionString) - - config.set('internalFieldsPrefix', '$') - - client - .post('/vtest/testdb/test-schema') - .set('Authorization', 'Bearer ' + bearerToken) - .send({field1: 'original field content'}) - .expect(200) - .end(function (err, res) { - if (err) return done(err) - - var doc = res.body.results[0] - - var body = { - query: { $id: doc.$id }, - update: {field1: 'updated'} - } - - client - .put('/vtest/testdb/test-schema/') - .set('Authorization', 'Bearer ' + bearerToken) - .send(body) - .expect(200) - .end(function (err, res) { - if (err) return done(err) - - res.body.results[0].$id.should.equal(doc.$id) - res.body.results[0].field1.should.equal('updated') - - client - .get('/vtest/testdb/test-schema/' + doc.$id + '?includeHistory=true') - .set('Authorization', 'Bearer ' + bearerToken) - .expect(200) - .expect('content-type', 'application/json') - .end(function (err, res) { - if (err) return done(err) - - res.body.results.should.exist - res.body.results.should.be.Array - res.body.results[0].$history.should.exist - res.body.results[0].$history[0].$id.should.exist - res.body.results[0].$history[0].field1.should.eql('original field content') - - config.set('internalFieldsPrefix', originalPrefix) - - done() - }) - }) - }) - }) - - it('should use specified historyFilters when querystring param includeHistory=true', function (done) { - var client = request(connectionString) - - client - .post('/vtest/testdb/test-schema') - .set('Authorization', 'Bearer ' + bearerToken) - .send({ field1: 'ABCDEF', field2: 2001 }) - .expect(200) - .end(function (err, res) { - if (err) return done(err) - var doc = res.body.results[0] - - var body = { - query: { _id: doc._id }, - update: {field1: 'GHIJKL'} - } - - client - .put('/vtest/testdb/test-schema/') - .set('Authorization', 'Bearer ' + bearerToken) - .send(body) - .expect(200) - .end(function (err, res) { - if (err) return done(err) - - res.body.results[0]._id.should.equal(doc._id) - res.body.results[0].field1.should.equal('GHIJKL') - - client - .get('/vtest/testdb/test-schema?filter={"_id": "' + doc._id + '"}&includeHistory=true&historyFilters={"field2":2001}') - .set('Authorization', 'Bearer ' + bearerToken) - .expect(200) - .expect('content-type', 'application/json') - .end(function (err, res) { - if (err) return done(err) - - res.body['results'].should.exist - res.body['results'].should.be.Array - res.body['results'][0].field1.should.exist - res.body['results'][0].field1.should.eql('GHIJKL') - res.body['results'][0].field2.should.exist - res.body['results'][0]._history.should.exist - res.body['results'][0]._history[0].field1.should.eql('ABCDEF') - done() - }) - }) - }) - }) - it('should return single document when querystring param count=1', function (done) { // create a bunch of docs var ac = new EventEmitter() diff --git a/test/acceptance/rest-endpoints/collections-api/post.js b/test/acceptance/rest-endpoints/collections-api/post.js index 8a95ce87..2b2533fb 100644 --- a/test/acceptance/rest-endpoints/collections-api/post.js +++ b/test/acceptance/rest-endpoints/collections-api/post.js @@ -297,8 +297,7 @@ describe('Collections API – POST', function () { should.exist(res.body.results[0]._id) res.body.results[0]._id.should.not.eql(input._id) - should.exist(res.body.results[0]._version) - res.body.results[0]._version.should.not.eql(input._version) + should.not.exist(res.body.results[0]._version) done() }) diff --git a/test/acceptance/rest-endpoints/collections-api/put.js b/test/acceptance/rest-endpoints/collections-api/put.js index 41c63103..5c586908 100644 --- a/test/acceptance/rest-endpoints/collections-api/put.js +++ b/test/acceptance/rest-endpoints/collections-api/put.js @@ -525,8 +525,7 @@ describe('Collections API – PUT', function () { should.exist(res.body.results[0]._id) res.body.results[0]._id.should.not.eql(update._id) - should.exist(res.body.results[0]._version) - res.body.results[0]._version.should.not.eql(update._version) + should.not.exist(res.body.results[0]._version) client .get('/vtest/testdb/put-test-schema?filter={"_id": "' + doc._id + '"}') @@ -553,8 +552,7 @@ describe('Collections API – PUT', function () { should.exist(res.body.results[0]._id) res.body.results[0]._id.should.not.eql(update._id) - should.exist(res.body.results[0]._version) - res.body.results[0]._version.should.not.eql(update._version) + should.not.exist(res.body.results[0]._version) done() }) diff --git a/test/unit/controller.js b/test/unit/controller.js index 3961b54c..79140a2c 100755 --- a/test/unit/controller.js +++ b/test/unit/controller.js @@ -65,6 +65,7 @@ describe('Controller', () => { ) let stub = sinon.stub(mod, 'get').resolves({}) let req = { + params: {}, url: '/foo/bar' } @@ -82,6 +83,7 @@ describe('Controller', () => { ) let stub = sinon.stub(mod, 'get').resolves({}) let req = { + params: {}, url: '/foo/bar?filter={"fieldName":"test", "busted":56488}' } @@ -105,6 +107,7 @@ describe('Controller', () => { ) let stub = sinon.stub(mod, 'get').resolves({}) let req = { + params: {}, url: '/foo/bar?filter={"fieldName": null}' } @@ -134,6 +137,7 @@ describe('Controller', () => { ) let stub = sinon.stub(mod, 'get').resolves({}) let req = { + params: {}, url: '/foo/bar?filter={"fieldName": null, "field2": "xx"}' } @@ -161,6 +165,7 @@ describe('Controller', () => { let mod = model('schemaTest', schema, null, { database: 'testdb' }) let stub = sinon.stub(mod, 'get').resolves({}) let req = { + params: {}, url: '/foo/bar?filter={"fieldMixed.innerProperty":"foo"}' } @@ -182,6 +187,7 @@ describe('Controller', () => { ) let stub = sinon.stub(mod, 'get').resolves({}) let req = { + params: {}, url: '/foo/bar?filter={"fieldName":"test"}&skip=-1' } let res = { @@ -211,6 +217,7 @@ describe('Controller', () => { ) let stub = sinon.stub(mod, 'get').resolves({}) let req = { + params: {}, url: '/foo/bar?filter={"fieldName":"test"}&page=-1' } let res = { @@ -246,6 +253,7 @@ describe('Controller', () => { ) let stub = sinon.stub(mod, 'get').resolves({}) let req = { + params: {}, url: '/v1/bar' } @@ -274,6 +282,7 @@ describe('Controller', () => { ) let stub = sinon.stub(mod, 'get').resolves({}) let req = { + params: {}, url: '/v1/bar' } @@ -300,6 +309,7 @@ describe('Controller', () => { ) let stub = sinon.stub(mod, 'get').resolves({}) let req = { + params: {}, url: '/foo/bar' } @@ -325,6 +335,7 @@ describe('Controller', () => { ) let stub = sinon.stub(mod, 'get').resolves({}) let req = { + params: {}, url: '/foo/bar' } @@ -341,6 +352,7 @@ describe('Controller', () => { it('should send response', () => { let mod = model('testModel') let req = { + params: {}, url: '/foo/bar' } let res = { diff --git a/test/unit/model/history.js b/test/unit/model/history.js deleted file mode 100644 index eb561104..00000000 --- a/test/unit/model/history.js +++ /dev/null @@ -1,196 +0,0 @@ -const should = require('should') -const model = require(__dirname + '/../../../dadi/lib/model') -const History = require(__dirname + '/../../../dadi/lib/model/history') -const connection = require(__dirname + '/../../../dadi/lib/model/connection') -const _ = require('underscore') -const help = require(__dirname + '/../help') - -describe('History', function () { - afterEach(done => { - help.clearCollection('testModelName', err => { - help.clearCollection('testModelNameHistory', err => { - connection.resetConnections().then(() => done()) - }) - }) - }) - - it('should export a constructor', function (done) { - History.should.be.Function - done() - }) - - describe('initialization options', function () { - it('should take a model name as an argument', function (done) { - const mod = model('testModelName', help.getModelSchema(), null, { database: 'testdb' }) - const h = new History(mod).model.name.should.equal('testModelName') - done() - }) - - it('should attach specified history collection if `storeRevisions` is true', function (done) { - const mod = model('testModelName', help.getModelSchema(), null, { database: 'testdb', storeRevisions: true, revisionCollection: 'modelHistory' }) - should.exist(mod.revisionCollection) - mod.revisionCollection.should.equal('modelHistory') - - done() - }) - }) - - describe('`create` method', function () { - it('should be added to history', function (done) { - const mod = model('testModelName', help.getModelSchema(), null, { database: 'testdb', storeRevisions: true }) - var h = new History(mod).model.history.create.should.be.Function - done() - }) - - it('should save model to history', function (done) { - const mod = model('testModelName', help.getModelSchema(), null, { database: 'testdb', storeRevisions: true }) - - help.whenModelsConnect([mod], () => { - mod.create({fieldName: 'foo'}, function (err, result) { - if (err) return done(err) - - // find the obj we just created - mod.find({fieldName: 'foo'}, function (err, doc) { - if (err) return done(err) - - mod.history.create(doc.results[0], mod, (err, res) => { - if (err) return done(err) - - mod.find({fieldName: 'foo'}, function (err, doc) { - if (err) return done(err) - - should.exist(doc['results']) - doc['results'][0]._history.length.should.equal(1) - done() - }) - }) - }) - }) - }) - }) - }) - - describe('`createEach` method', function () { - it('should be added to history', function (done) { - const mod = model('testModelName', help.getModelSchema(), null, { database: 'testdb' }) - new History(mod).model.history.createEach.should.be.Function - done() - }) - - it('should save all models to history', function (done) { - this.timeout(4000) - - const mod = model('testModelName', help.getModelSchema(), null, { database: 'testdb', storeRevisions: true }) - - help.whenModelsConnect([mod], () => { - mod.create({fieldName: 'foo-1'}, function (err, result) { - if (err) return done(err) - mod.create({fieldName: 'foo-2'}, function (err, result) { - if (err) return done(err) - // find the objs we just created - mod.find({}, function (err, docs) { - if (err) return done(err) - - mod.history.createEach(docs['results'], 'delete', mod).then(() => { - mod.find({ fieldName: { '$regex': '^foo-' } }, function (err, docs) { - if (err) return done(err) - - docs.results[0]._history.length.should.equal(1) - docs.results[1]._history.length.should.equal(1) - done() - }) - }).catch((err) => { - done(err) - }) - }) - }) - }) - }) - }) - - it('should add action=update to history revisions when a document is updated', function (done) { - const mod = model('testModelName', help.getModelSchema(), null, { database: 'testdb', storeRevisions: true }) - - help.whenModelsConnect([mod], () => { - mod.create({ fieldName: 'foo-1' }, function (err, result) { - mod.update({ fieldName: 'foo-1' }, { fieldName: 'foo-2' }, function (err, result) { - mod.find({}, { includeHistory: true }, function (err, docs) { - should.exist(docs.results[0]._history) - should.exist(docs.results[0]._history[0]) - should.exist(docs.results[0]._history[0]._action) - docs.results[0]._history[0]._action.should.eql('update') - done() - }) - }) - }) - }) - }) - - it('should add action=delete to history revisions when a document is deleted', function (done) { - const mod = model('testModelName', help.getModelSchema(), null, { database: 'testdb', storeRevisions: true }) - - help.whenModelsConnect([mod], () => { - mod.create({ fieldName: 'foo-1' }, function (err, result) { - mod.delete({ fieldName: 'foo-1' }, function (err, result) { - const modHistory = model('testModelNameHistory', help.getModelSchema(), null, { database: 'testdb', storeRevisions: true }) - - help.whenModelsConnect([modHistory], () => { - modHistory.find({}, {}, function (err, docs) { - should.exist(docs.results[0]) - should.exist(docs.results[0]._originalDocumentId) - should.exist(docs.results[0]._action) - docs.results[0]._action.should.eql('delete') - done() - }) - }) - }) - }) - }) - }) - - it('should add the original document id to history revisions', function (done) { - const mod = model('testModelName', help.getModelSchema(), null, { database: 'testdb', storeRevisions: true }) - - help.whenModelsConnect([mod], () => { - mod.create({ fieldName: 'foo-1' }, function (err, result) { - const id = result.results[0]._id - - mod.update({ fieldName: 'foo-1' }, { fieldName: 'foo-2' }, function (err, result) { - mod.find({}, { includeHistory: true }, function (err, docs) { - should.exist(docs.results[0]._history) - should.exist(docs.results[0]._history[0]) - should.exist(docs.results[0]._history[0]._originalDocumentId) - - docs.results[0]._history[0]._originalDocumentId.should.eql(id.toString()) - done() - }) - }) - }) - }) - }) - - it('should create history revisions when a document is deleted using a nested query', function (done) { - const mod = model('testModelName', help.getModelSchema(), null, { database: 'testdb', storeRevisions: true }) - - help.whenModelsConnect([mod], () => { - mod.create({ fieldName: 'foo-1' }, function (err, result) { - const doc = result.results[0] - mod.delete({ _id: { '$in': [ doc._id ] } }, function (err, result) { - const modHistory = model('testModelNameHistory', help.getModelSchema(), null, { database: 'testdb', storeRevisions: false }) - - help.whenModelsConnect([modHistory], () => { - return modHistory.find({}, {}, function (err, docs) { - should.exist(docs.results) - docs.results.length.should.be.above(0) - should.exist(docs.results[0]._originalDocumentId) - should.exist(docs.results[0]._action) - docs.results[0]._action.should.eql('delete') - done() - }) - }) - }) - }) - }) - }) - }) -}) diff --git a/test/unit/model/index.js b/test/unit/model/index.js index f2701bd7..02d9b3be 100755 --- a/test/unit/model/index.js +++ b/test/unit/model/index.js @@ -11,7 +11,7 @@ var config = require(__dirname + '/../../../config') describe('Model', function () { beforeEach((done) => { help.clearCollection('testModelName', function () { - help.clearCollection('testModelNameHistory', function () { + help.clearCollection('testModelNameVersions', function () { done() }) }) @@ -98,12 +98,13 @@ describe('Model', function () { ) should.exist(mod.settings) - mod.revisionCollection.should.equal('testModelNameHistory') + should.exist(mod.history) + mod.history.name.should.equal('testModelNameVersions') done() }) - it('should attach history collection if specified', function (done) { + it('should attach history collection if specified (using legacy `revisionCollection` property)', function (done) { var mod = model( 'testModelName', help.getModelSchema(), @@ -113,7 +114,22 @@ describe('Model', function () { revisionCollection: 'modelHistory' } ) - mod.revisionCollection.should.equal('modelHistory') + mod.history.name.should.equal('modelHistory') + + done() + }) + + it('should attach history collection if specified', function (done) { + var mod = model( + 'testModelName', + help.getModelSchema(), + null, + { + database: 'testdb', + versioningCollection: 'modelHistory' + } + ) + mod.history.name.should.equal('modelHistory') done() }) @@ -128,8 +144,8 @@ describe('Model', function () { storeRevisions: true } ) - should.exist(mod.revisionCollection) - mod.revisionCollection.should.equal('testModelNameHistory') + should.exist(mod.history) + mod.history.name.should.equal('testModelNameVersions') done() }) @@ -145,8 +161,41 @@ describe('Model', function () { revisionCollection: 'modelHistory' } ) - should.exist(mod.revisionCollection) - mod.revisionCollection.should.equal('modelHistory') + should.exist(mod.history) + mod.history.name.should.equal('modelHistory') + + done() + }) + + it('should attach history collection if `enableVersioning` is true', function (done) { + var mod = model( + 'testModelName', + help.getModelSchema(), + null, + { + database: 'testdb', + enableVersioning: true + } + ) + should.exist(mod.history) + mod.history.name.should.equal('testModelNameVersions') + + done() + }) + + it('should attach specified history collection if `enableVersioning` is true', function (done) { + const mod = model( + 'testModelName', + help.getModelSchema(), + null, + { + database: 'testdb', + enableVersioning: true, + versioningCollection: 'modelHistory' + } + ) + should.exist(mod.history) + mod.history.name.should.equal('modelHistory') done() }) @@ -464,158 +513,6 @@ describe('Model', function () { }) }) - describe('includeHistory param', function () { - beforeEach((done) => { - acceptanceHelper.dropDatabase('testdb', err => { - done() - }) - }) - - it('should override `done` method if options.includeHistory = true', function (done) { - var mod = model('testModelName', help.getModelSchema(), null, { database: 'testdb', storeRevisions: true }) - - var method = sinon.spy(model.Model.prototype, '_injectHistory') - - mod.create({fieldName: 'foo'}, function (err, result) { - if (err) return done(err) - - mod.find({fieldName: 'foo'}, { includeHistory: true }, function (err, results) { - if (err) return done(err) - - method.restore() - method.called.should.eql(true) - - results.results.should.exist - results.results.should.be.Array - results.results[0].fieldName.should.eql('foo') - done() - }) - }) - }) - - it('should add history to results if options.includeHistory = true', function (done) { - var mod = model('testModelName', help.getModelSchema(), null, { database: 'testdb', storeRevisions: true }) - - help.whenModelsConnect([mod]).then(() => { - mod.create({fieldName: 'foo'}, function (err, result) { - if (err) return done(err) - - mod.update({fieldName: 'foo'}, {fieldName: 'bar'}, function (err, result) { - if (err) return done(err) - - mod.find({}, { includeHistory: true }, function (err, results) { - if (err) return done(err) - - results.results.should.exist - results.results.should.be.Array - results.results[0]._history.should.exist - results.results[0]._history[0].fieldName.should.eql('foo') - done() - }) - }) - }) - }) - }) - - it('should use specified historyFilters when includeHistory = true', function (done) { - var mod = model('testModelName', help.getModelSchema(), null, { database: 'testdb', storeRevisions: true }) - - mod.create({fieldName: 'foo'}, function (err, result) { - if (err) return done(err) - - mod.update({fieldName: 'foo'}, {fieldName: 'bar'}, function (err, result) { - if (err) return done(err) - - mod.find({}, { includeHistory: true, historyFilters: '{ "fieldName": "foo" }' }, function (err, results) { - if (err) return done(err) - results.results.should.exist - results.results.should.be.Array - should.exist(results.results[0]._history) - should.exist(results.results[0].fieldName) - done() - }) - }) - }) - }) - }) - - describe('Version number', function () { - beforeEach((done) => { - acceptanceHelper.dropDatabase('testdb', err => { - done() - }) - }) - - it('should add _version:1 to a new document', function (done) { - var mod = model('testModelName', help.getModelSchema(), null, { database: 'testdb', storeRevisions: true }) - - mod.create({fieldName: 'foo'}, function (err, results) { - if (err) return done(err) - - results.results.should.exist - results.results.should.be.Array - results.results[0]._version.should.eql(1) - done() - }) - }) - - it('should increment the version number when updating a document', function (done) { - var mod = model('testModelName', help.getModelSchema(), null, { database: 'testdb', storeRevisions: true }) - mod.create({fieldName: 'foo'}, function (err, result) { - if (err) return done(err) - mod.update({fieldName: 'foo'}, {fieldName: 'bar'}, function (err, result) { - if (err) return done(err) - mod.find({fieldName: 'bar'}, { includeHistory: true }, function (err, results) { - if (err) return done(err) - results.results.should.exist - results.results.should.be.Array - results.results[0]._version.should.eql(2) - results.results[0]._history[0]._version.should.eql(1) - done() - }) - }) - }) - }) - }) - - describe('`revisions` method', function () { - beforeEach((done) => { - acceptanceHelper.dropDatabase('testdb', err => { - done() - }) - }) - - it('should accept id param and return history collection', function (done) { - var mod = model('testModelName', help.getModelSchema(), null, { database: 'testdb', storeRevisions: true }) - - mod.create({fieldName: 'foo'}, function (err, result) { - if (err) return done(err) - - mod.update({fieldName: 'foo'}, {fieldName: 'bar'}, function (err, result) { - if (err) return done(err) - - mod.find({fieldName: 'bar'}, function (err, doc) { - if (err) return done(err) - - var doc_id = doc.results[0]._id - var revision_id = doc.results[0]._history[0] // expected history object - - model('testModelName', help.getModelSchema(), null, { database: 'testdb' }).revisions(doc_id, {}, function (err, result) { - if (err) return done(err) - - result.should.be.Array - - if (result[0]) { - result[0]._id.toString().should.equal(revision_id.toString()) - } - }) - done() - }) - }) - }) - }) - }) - describe('`getIndexes` method', function () { beforeEach((done) => { acceptanceHelper.dropDatabase('testdb', err => { @@ -890,29 +787,6 @@ describe('Model', function () { }) }) - it('should save model to history collection', function (done) { - let mod = model( - 'testModelName', - help.getModelSchema(), - null, - { database: 'testdb' } - ) - - mod.create({fieldName: 'foo'}, err => { - if (err) return done(err) - - mod.find({fieldName: 'foo'}, (err, doc) => { - if (err) return done(err) - - should.exist(doc['results']) - doc['results'][0]._history.should.be.Array - doc['results'][0]._history.length.should.equal(0) // no updates yet - - done() - }) - }) - }) - it('should pass error to callback if validation fails', function (done) { let schema = help.getModelSchema() let mod = model( @@ -984,29 +858,6 @@ describe('Model', function () { }) }) - it('should save model to history collection', () => { - let mod = model( - 'testModelName', - help.getModelSchema(), - null, - { database: 'testdb' } - ) - - return mod.create({ - documents: { fieldName: 'foo' } - }).then(documents => { - return mod.find({ - query: { fieldName: 'foo' } - }) - }).then(({metadata, results}) => { - should.exist(metadata) - should.exist(results) - - results[0]._history.should.be.Array - results[0]._history.length.should.equal(0) // no updates yet - }) - }) - it('should reject with error if validation fails', done => { let schema = help.getModelSchema() let mod = model( @@ -1108,10 +959,13 @@ describe('Model', function () { should.exist(result['results'] && result['results'][0]) result['results'][0].field1.should.equal('bar') - should.exist(result['results'][0]._history) - result['results'][0]._history.length.should.equal(1) // one revision, from the update + mod.getVersions({ + documentId: result.results[0]._id + }).then(({results}) => { + results.length.should.equal(1) - done() + done() + }) }) }) }) @@ -1204,8 +1058,11 @@ describe('Model', function () { should.exist(results && results[0]) results[0].field1.should.equal('bar') - should.exist(results[0]._history) - results[0]._history.length.should.equal(1) // one revision, from the update + return mod.getVersions({ + documentId: results[0]._id + }) + }).then(({results}) => { + results.length.should.equal(1) }) })