Skip to content

Commit

Permalink
feat: validation for store.add() (#165)
Browse files Browse the repository at this point in the history
  • Loading branch information
mbad0la authored and gr2m committed Sep 12, 2017
1 parent 7fb1573 commit 6663e78
Show file tree
Hide file tree
Showing 7 changed files with 262 additions and 3 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +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

Returns `store` API.

Expand Down
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ function Store (dbName, options) {
dbName: dbName,
PouchDB: options.PouchDB,
emitter: emitter,
validate: options.validate,
get remote () {
return options.remote
}
Expand Down
14 changes: 13 additions & 1 deletion lib/helpers/add-many.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
var clone = require('lodash/clone')
var uuid = require('pouchdb-utils').uuid
var validate = require('../validate')

var addTimestamps = require('../utils/add-timestamps')
var bulkDocs = require('./db-bulk-docs')
Expand All @@ -17,5 +18,16 @@ module.exports = function addMany (state, docs, prefix) {
})
}

return bulkDocs(state, docs)
var validationPromises = docs.map(function (doc) {
return validate(state, doc)
})

return Promise.all(validationPromises)

.then(function () {
return bulkDocs(state, docs)
})
.catch(function (error) {
throw error
})
}
6 changes: 5 additions & 1 deletion lib/helpers/add-one.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ var clone = require('lodash/clone')
var PouchDBErrors = require('pouchdb-errors')
var Promise = require('lie')
var uuid = require('pouchdb-utils').uuid
var validate = require('../validate')

var internals = addOne.internals = {}
internals.addTimestamps = require('../utils/add-timestamps')
Expand All @@ -26,8 +27,11 @@ function addOne (state, doc, prefix) {

delete doc.hoodie

return internals.put(state, internals.addTimestamps(doc))
return validate(state, doc)

.then(function () {
return internals.put(state, internals.addTimestamps(doc))
})
.catch(function (error) {
if (error.status === 409) {
var conflict = new Error('Object with id "' + doc._id + '" already exists')
Expand Down
40 changes: 40 additions & 0 deletions lib/validate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
module.exports = validate

var Promise = require('lie')

function validate (state, doc) {
if (state.validate === undefined) {
return Promise.resolve()
}

return Promise.resolve()

.then(function () {
return state.validate(doc)
})
.catch(function (rejectValue) {
var error = new Error()

if (rejectValue instanceof Error) {
Object.keys(rejectValue).map(function (key) {
error[key] = rejectValue[key]
})

if (rejectValue.message) {
error.message = rejectValue.message
} else {
error.message = 'document validation failed'
}
} else {
if (typeof rejectValue === 'string') {
error.message = rejectValue
} else {
error.message = 'check error value for more details'
error.value = rejectValue
}
}

error.name = 'ValidationError'
throw error
})
}
31 changes: 30 additions & 1 deletion tests/integration/add.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,8 @@ test('adds multiple objects to db', function (t) {
var name = uniqueName()
var store = new Store(name, {
PouchDB: PouchDB,
remote: 'remote-' + name
remote: 'remote-' + name,
validate: function () {}
})

store.add({
Expand Down Expand Up @@ -164,6 +165,34 @@ test('adds multiple objects to db', function (t) {
.catch(t.error)
})

test('fail validation adding multiple objects to db', function (t) {
t.plan(2)

var name = uniqueName()
var store = new Store(name, {
PouchDB: PouchDB,
remote: 'remote-' + name,
validate: function () { throw new Error('Validation failed for the given docs') }
})

store.add([{
foo: 'bar'
}, {
foo: 'baz'
}, {
_id: 'foo',
foo: 'baz'
}])

.then(function () {
t.fail('Expecting ValidationError')
})
.catch(function (error) {
t.is(error.name, 'ValidationError')
t.is(error.message, 'Validation failed for the given docs')
})
})

test('store.add(object) makes createdAt and updatedAt timestamps', function (t) {
t.plan(4)

Expand Down
172 changes: 172 additions & 0 deletions tests/unit/add-one-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ test('add-one non-409 error', function (t) {
simple.mock(addOne.internals, 'addTimestamps')

var state = {}

var doc = {}
addOne(state, doc)

Expand All @@ -25,3 +26,174 @@ test('add-one non-409 error', function (t) {
simple.restore()
})
})

test('add-one ValidationError', function (t) {
t.plan(2)

var state = {
validate: function () { throw new Error('Validation failed for the given docs') }
}

var doc = {}
addOne(state, doc)

.then(function () {
t.fail('should throw an ValidationError')
})

.catch(function (error) {
t.is(error.name, 'ValidationError', 'validation error name matches')
t.is(error.message, 'Validation failed for the given docs', 'error message matches intent')
})
})

test('add-one validate rejects with Error (without message)', function (t) {
t.plan(2)

var state = {
validate: function () { return Promise.reject(new Error()) }
}

var doc = {}
addOne(state, doc)

.then(function () {
t.fail('should throw an ValidationError')
})

.catch(function (error) {
t.is(error.name, 'ValidationError', 'validation error name matches')
t.is(error.message, 'document validation failed', 'error message matches intent')
})
})

test('add-one validate rejects with Error (with message)', function (t) {
t.plan(2)

var state = {
validate: function () { return Promise.reject(new Error('Validation failed for the given docs')) }
}

var doc = {}
addOne(state, doc)

.then(function () {
t.fail('should throw an ValidationError')
})

.catch(function (error) {
t.is(error.name, 'ValidationError', 'validation error name matches')
t.is(error.message, 'Validation failed for the given docs', 'error message matches intent')
})
})

test('add-one validate rejects with a string', function (t) {
t.plan(2)

var state = {
validate: function () { return Promise.reject('Validation failed for the given docs') }
}

var doc = {}
addOne(state, doc)

.then(function () {
t.fail('should throw an ValidationError')
})

.catch(function (error) {
t.is(error.name, 'ValidationError', 'validation error name matches')
t.is(error.message, 'Validation failed for the given docs', 'error message matches intent')
})
})

test('add-one validate rejects with a value I', function (t) {
t.plan(3)

var state = {
validate: function () { return Promise.reject(false) }
}

var doc = {}
addOne(state, doc)

.then(function () {
t.fail('should throw an ValidationError')
})

.catch(function (error) {
t.is(error.name, 'ValidationError', 'validation error name matches')
t.is(error.message, 'check error value for more details', 'error message matches intent')
t.is(error.value, false, 'error.value is false')
})
})

test('add-one validate rejects with a value II', function (t) {
t.plan(3)

var state = {
validate: function () { return Promise.reject(1) }
}

var doc = {}
addOne(state, doc)

.then(function () {
t.fail('should throw an ValidationError')
})

.catch(function (error) {
t.is(error.name, 'ValidationError', 'validation error name matches')
t.is(error.message, 'check error value for more details', 'error message matches intent')
t.is(error.value, 1, 'error.value is 1')
})
})

test('add-one validate rejects with a value III', function (t) {
t.plan(4)

var state = {
validate: function () { return Promise.reject({ failure: true, tries: 1 }) }
}

var doc = {}
addOne(state, doc)

.then(function () {
t.fail('should throw an ValidationError')
})

.catch(function (error) {
t.is(error.name, 'ValidationError', 'validation error name matches')
t.is(error.message, 'check error value for more details', 'error message matches intent')
t.is(error.value.failure, true, 'error.value.failure is true')
t.is(error.value.tries, 1, 'error.value.tries is 1')
})
})

test('add-one validation fails with custom error', function (t) {
t.plan(4)

var customError = new Error('custom error message')

customError.status = 401
customError.errorCode = 'DB_401'

var state = {
validate: function () { throw customError }
}

var doc = {}
addOne(state, doc)

.then(function () {
t.fail('should throw an ValidationError')
})

.catch(function (error) {
t.is(error.name, 'ValidationError', 'validation error name matches')
t.is(error.message, 'custom error message', 'error message matches intent')
t.is(error.status, 401, 'error.status is 401')
t.is(error.errorCode, 'DB_401', 'error.errorCode is DB_401')
})
})

0 comments on commit 6663e78

Please sign in to comment.