diff --git a/README.md b/README.md index 93c7903..dc6e5a1 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ new Store(dbName, options) | **`options.remote`** | Object | PouchDB instance | Yes (ignores `remoteBaseUrl` from [Store.defaults](#storedefaults)) | **`options.remote`** | Promise | Resolves to either string or PouchDB instance | see above | **`options.PouchDB`** | Constructor | [PouchDB custom builds](https://pouchdb.com/custom.html) | Yes (unless preset using [Store.defaults](#storedefaults))) -| **`options.validate`** | Function | Validation function to execute before DB operations (Can return promise for async validation) | No +| **`options.validate`** | Function(doc) | Validation function to execute before DB operations (Can return promise for async validation) | No Returns `store` API. diff --git a/lib/helpers/update-many.js b/lib/helpers/update-many.js index ffc48c3..9a195ec 100644 --- a/lib/helpers/update-many.js +++ b/lib/helpers/update-many.js @@ -24,12 +24,13 @@ module.exports = function updateMany (state, array, change, prefix) { .then(function (docs) { if (change) { - return docs.map(function (doc) { + return Promise.all(docs.map(function (doc) { if (doc instanceof Error) { return doc } - return changeObject(change, doc) - }) + + return changeObject(state, change, doc) + })) } return docs.map(function (doc, index) { diff --git a/lib/helpers/update-one.js b/lib/helpers/update-one.js index a0672d0..ae73e28 100644 --- a/lib/helpers/update-one.js +++ b/lib/helpers/update-one.js @@ -21,7 +21,8 @@ module.exports = function updateOne (state, idOrDoc, change, prefix) { if (!change) { return assign(doc, idOrDoc, {_id: doc._id, _rev: doc._rev, hoodie: doc.hoodie}) } - return changeObject(change, doc) + + return changeObject(state, change, doc) }) .then(function (_doc) { diff --git a/lib/remove.js b/lib/remove.js index a05615f..6ad79f6 100644 --- a/lib/remove.js +++ b/lib/remove.js @@ -15,7 +15,17 @@ module.exports = remove * @return {Promise} */ function remove (state, prefix, objectsOrIds, change) { - return Array.isArray(objectsOrIds) - ? updateMany(state, objectsOrIds.map(markAsDeleted.bind(null, change)), null, prefix) - : updateOne(state, markAsDeleted(change, objectsOrIds), null, prefix) + if (Array.isArray(objectsOrIds)) { + return Promise.all(objectsOrIds.map(markAsDeleted.bind(null, state, change))) + + .then(function (docs) { + return updateMany(state, docs, null, prefix) + }) + } + + return markAsDeleted(state, change, objectsOrIds) + + .then(function (doc) { + return updateOne(state, doc, null, prefix) + }) } diff --git a/lib/utils/change-object.js b/lib/utils/change-object.js index fba9bd0..32823c0 100644 --- a/lib/utils/change-object.js +++ b/lib/utils/change-object.js @@ -1,15 +1,23 @@ var assign = require('lodash/assign') +var validate = require('../validate') /** * change object either by passing changed properties * as an object, or by passing a change function that * manipulates the passed object directly **/ -module.exports = function changeObject (change, object) { +module.exports = function changeObject (state, change, object) { + var updatedObject = assign({}, object) + if (typeof change === 'object') { - return assign(object, change) + updatedObject = assign(object, change) + } else { + change(updatedObject) } - change(object) - return object + return validate(state, updatedObject) + + .then(function () { + return updatedObject + }) } diff --git a/lib/utils/mark-as-deleted.js b/lib/utils/mark-as-deleted.js index 7f871a9..c15d8fd 100644 --- a/lib/utils/mark-as-deleted.js +++ b/lib/utils/mark-as-deleted.js @@ -1,13 +1,17 @@ var assign = require('lodash/assign') var changeObject = require('./change-object') -// Normalizes objectOrId, applies changes if any, and mark as deleted -module.exports = function markAsDeleted (change, objectOrId) { +// Normalizes objectOrId, validates changes if any, applies them, and mark as deleted +module.exports = function markAsDeleted (state, change, objectOrId) { var object = typeof objectOrId === 'string' ? { _id: objectOrId } : objectOrId if (change) { - changeObject(change, object) + return changeObject(state, change, object) + + .then(function (doc) { + return assign({_deleted: true}, doc) + }) } - return assign({_deleted: true}, object) + return Promise.resolve(assign({_deleted: true}, object)) } diff --git a/tests/integration/remove.js b/tests/integration/remove.js index 1c3b7fa..d08b66e 100644 --- a/tests/integration/remove.js +++ b/tests/integration/remove.js @@ -293,6 +293,93 @@ test('remove(id, changeFunction) updates before removing', function (t) { }) }) +test('remove(id, changeFunction) fails modification validation', function (t) { + t.plan(3) + + var validationCallCount = 0 + + var name = uniqueName() + var store = new Store(name, { + PouchDB: PouchDB, + remote: 'remote-' + name, + validate: function () { + if (validationCallCount) { + throw new Error('Could not modify object') + } + + ++validationCallCount + } + }) + + store.add({ + _id: 'foo', + foo: 'bar' + }) + + .then(function () { + return store.remove('foo', function (doc) { + doc.foo = 'changed' + return doc + }) + }) + + .catch(function (error) { + t.equals(validationCallCount, 1, 'Expecting Remove to fail validation') + t.is(error.name, 'ValidationError') + t.is(error.message, 'Could not modify object') + }) +}) + +test('remove([ids], changeFunction) fails modification validation when one validation fails', function (t) { + t.plan(4) + + var validationCallCount = 0 + + var name = uniqueName() + var store = new Store(name, { + PouchDB: PouchDB, + remote: 'remote-' + name, + validate: function (doc) { + if (validationCallCount > 1) { + if (doc._id === 'bar') { + throw new Error() + } + } + + ++validationCallCount + } + }) + + store.add([ + { _id: 'foo', foo: 'bar' }, + { _id: 'bar', foo: 'bar' } + ]) + + .then(function () { + return store.remove( + ['foo', 'bar'], + function (doc) { + doc.foo = 'changed' + return doc + } + ) + }) + + .catch(function (error) { + t.equals(validationCallCount, 3, 'Expecting last remove to fail validation') + t.is(error.name, 'ValidationError') + t.is(error.message, 'document validation failed') + + return null + }) + + .then(store.findAll) + + .then(function (objects) { + t.is(objects.length, 2) + }) +}) + test('store.remove(object) creates deletedAt timestamp', function (t) { t.plan(4) diff --git a/tests/integration/update.js b/tests/integration/update.js index 425bf7c..923804a 100644 --- a/tests/integration/update.js +++ b/tests/integration/update.js @@ -117,6 +117,46 @@ test('store.update(id, updateFunction)', function (t) { }) }) +test('store.update(id, updateFunction) fails validation', function (t) { + t.plan(5) + + var name = uniqueName() + var store = new Store(name, { + PouchDB: PouchDB, + remote: 'remote-' + name, + validate: function (doc) { + if (doc.foo) { + throw new Error() + } + } + }) + + store.add({ _id: 'exists' }) + + .then(function () { + return store.update('exists', function (object) { + object.foo = object._id + 'bar' + }) + }) + + .catch(function (error) { + t.is(error.name, 'ValidationError') + t.is(error.message, 'document validation failed') + + return null + }) + + .then(function () { + return store.find('exists') + }) + + .then(function (doc) { + t.is(doc._id, 'exists') + t.false(/^2-/.test(doc._rev)) + t.is(doc.foo, undefined) + }) +}) + test('store.update(object)', function (t) { t.plan(3) @@ -485,3 +525,63 @@ test('store.update(array)', function (t) { t.is(objects[1].bar, 'baz') }) }) + +test('store.update([objects], change) fails to update as one doc fails validation', function (t) { + t.plan(12) + + var validationCallCount = 0 + + var name = uniqueName() + var store = new Store(name, { + PouchDB: PouchDB, + remote: 'remote-' + name, + validate: function (doc) { + if (validationCallCount > 2) { + if (doc.foo === 'baz') { + throw new Error('document validation failed') + } + } + + ++validationCallCount + } + }) + + return store.add([ + { _id: '1', foo: 'foo' }, + { _id: '2', foo: 'bar' }, + { _id: '3', foo: 'baz', bar: 'foo' } + ]) + + .then(function () { + return store.update( + [1, 2, 3], + { + bar: 'bar', + hoodie: {ignore: 'me'} + } + ) + }) + + .catch(function (error) { + t.is(validationCallCount, 5, 'needs to fail the validation for last object update') + t.is(error.name, 'ValidationError') + t.is(error.message, 'document validation failed') + + return null + }) + + .then(store.findAll) + + .then(function (objects) { + objects.forEach(function (object, idx) { + t.ok(object.foo, 'old value remains') + t.false(/^2-/.test(object._rev)) + + if (idx === 2) { + t.is(object.bar, 'foo', 'object not updated') + } else { + t.is(object.bar, undefined) + } + }) + }) +})