Comparing version
Baucis Change Log | ||
================= | ||
v0.10.1 | ||
------- | ||
Improvements to optimistic locking. Adds the 'locking' controller option. When set to true, this option enables automatic version increments and strict version checking. When enabled, `__v` must always be sent with updates, and the baucis query must always have the `__v` key selected. | ||
If this option is not enabled, no extra lock checking or version incrementing is performed outside what is normally done by Mongoose. | ||
The 'always check version' controller option has been deprecated. | ||
v0.10.0 | ||
@@ -5,0 +13,0 @@ ------- |
@@ -58,8 +58,11 @@ // __Dependencies__ | ||
var operator = request.headers['x-baucis-update-operator']; | ||
var conditions; | ||
var updateWrapper = {}; | ||
var update = extend(request.body); | ||
var versionKey = request.baucis.controller.get('schema').get('versionKey'); | ||
var alwaysCheckVersion = request.baucis.controller.get('always check version'); | ||
var version = update[versionKey]; | ||
var lock = request.baucis.controller.get('locking') === true; | ||
var updateVersion = update[versionKey]; | ||
var done = function (error, saved) { | ||
if (error) return next(error); | ||
if (!saved) return response.send(404); | ||
request.baucis.documents = saved; | ||
@@ -69,48 +72,41 @@ next(); | ||
if (operator && validOperators.indexOf(operator) === -1) return next(new Error('Unsupported update operator: ' + operator)); | ||
if (alwaysCheckVersion && (version !== 0) && !version) return response.send(409); | ||
if (lock && !Number.isFinite(updateVersion)) return response.send(409); | ||
request.baucis.query.exec(function (error, doc) { | ||
if (error) return next(error); | ||
if (!doc) return response.send(404); | ||
var previousVersion = doc[versionKey]; | ||
if (alwaysCheckVersion && !doc.isSelected(versionKey)) { | ||
next(new Error('version key "'+ versionKey + '" was not selected.')); | ||
return; | ||
// Save with non-default operator | ||
if (operator) { | ||
if (validOperators.indexOf(operator) === -1) return next(new Error('Unsupported update operator: ' + operator)); | ||
// Ensure that some paths have been enabled for the operator. | ||
if (!request.baucis.controller.get('allow ' + operator)) return next(new Error('Update operator not enabled for this controller: ' + operator)); | ||
// Make sure paths have been whitelisted for this operator. | ||
if (request.baucis.controller.checkBadUpdateOperatorPaths(operator, Object.keys(update))) { | ||
return next(new Error("Can't use update operator with non-whitelisted paths.")); | ||
} | ||
if (previousVersion && (version || version === 0) && version < previousVersion) { | ||
response.send(409); | ||
return; | ||
} | ||
if (!operator) { | ||
doc.set(update); | ||
doc.save(done); | ||
return; | ||
} | ||
// Non-default operator | ||
var conditions = request.baucis.controller.getFindByConditions(request); | ||
var updateWrapper = {}; | ||
conditions = request.baucis.controller.getFindByConditions(request); | ||
if (lock) conditions[versionKey] = updateVersion; | ||
updateWrapper[operator] = update; | ||
request.baucis.controller.get('model').findOneAndUpdate(conditions, updateWrapper, done); | ||
return; | ||
} | ||
// Oh man I really want this to trigger validation… | ||
// var pathParts = 'arbitrary.$.llama'.split('.'); | ||
// | ||
// doc.get(parts[0]) //arr | ||
// subdoc = ...;// find one(s) that matches where for query | ||
// subdoc.push(parts[2], val) | ||
// Default update operator with `doc.save`. | ||
request.baucis.query.exec(function (error, doc) { | ||
if (error) return next(error); | ||
if (!doc) return response.send(404); | ||
// Ensure that some paths have been enabled for the operator. | ||
if (!request.baucis.controller.get('allow ' + operator)) return next(new Error('Update operator not enabled for this controller: ' + operator)); | ||
var currentVersion = doc[versionKey]; | ||
// Make sure paths have been whitelisted for this operator. | ||
if (request.baucis.controller.checkBadUpdateOperatorPaths(operator, Object.keys(update))) { | ||
return next(new Error("Can't use update operator with non-whitelisted paths.")); | ||
if (lock) { | ||
// Make sure the version key was selected. | ||
if (!doc.isSelected(versionKey)) return next(new Error('Version key "'+ versionKey + '" was not selected.')); | ||
// Update and current version have been found. | ||
// Check if they're equal. | ||
if (updateVersion !== currentVersion) response.send(409); | ||
// One is not allowed to set __v and increment in the same update. | ||
delete update[versionKey]; | ||
doc.increment(); | ||
} | ||
request.baucis.controller.get('model').findOneAndUpdate(conditions, updateWrapper, done); | ||
doc.set(update); | ||
doc.save(done); | ||
}); | ||
@@ -117,0 +113,0 @@ }, |
@@ -5,3 +5,3 @@ { | ||
"homepage": "https://github.com/wprl/baucis", | ||
"version": "0.10.0", | ||
"version": "0.10.1", | ||
"main": "index.js", | ||
@@ -8,0 +8,0 @@ "scripts": { |
@@ -1,2 +0,2 @@ | ||
baucis v0.10.0 | ||
baucis v0.10.1 | ||
============== | ||
@@ -230,3 +230,3 @@ | ||
| head, get, post, put, del | May be set to false to disable those HTTP verbs completely for the controller | | ||
| always check version | Force all PUTs to send the document version (__v). By default, if a version is not sent with an update no version checking is performed. | | ||
| locking | Enable optimistic locking. Requires that all PUTs must send the document version (__v) and will send a 409 response if there is a version conflict. | | ||
@@ -233,0 +233,0 @@ An example of embedding a controller within another controller |
@@ -248,3 +248,3 @@ var expect = require('expect.js'); | ||
url: 'http://localhost:8012/api/v1/stores/123/tools/' + id, | ||
json: { name: 'Screwdriver', __v: body[0].__v + 10 } | ||
json: { name: 'Screwdriver' } | ||
}; | ||
@@ -271,3 +271,2 @@ request.put(options, function (error, response, body) { | ||
expect(body[0]).to.have.property('name', 'Axe'); | ||
expect() | ||
@@ -472,3 +471,3 @@ var id = body[0]._id; | ||
expect(response).to.have.property('statusCode', 500); | ||
expect(body).to.contain('Error: The "X-Baucis-Push header" is deprecated. Use "X-Baucis-Update-Operator: $push" instead.'); | ||
expect(body).to.contain('Error: The "X-Baucis-Push header" is deprecated. Use "X-Baucis-Update-Operator: $push" instead.'); | ||
done(); | ||
@@ -744,18 +743,24 @@ }); | ||
var options = { | ||
url: 'http://localhost:8012/api/v1/stores/Westlake', | ||
url: 'http://localhost:8012/api/v1/liens', | ||
json: true, | ||
body: { __v: 10 } | ||
body: { title: 'Franklin' } | ||
}; | ||
request.put(options, function (error, response, body) { | ||
request.post(options, function (error, response, body) { | ||
if (error) return done(error); | ||
expect(response).to.have.property('statusCode', 200); | ||
expect(response).to.have.property('statusCode', 201); | ||
var options = { | ||
url: 'http://localhost:8012/api/v1/stores/Westlake', | ||
url: 'http://localhost:8012/api/v1/liens/' + body._id, | ||
json: true, | ||
body: { __v: 0 } | ||
body: { title: 'Ranken', __v: 0 } | ||
}; | ||
request.put(options, function (error, response, body) { | ||
if (error) return done(error); | ||
expect(response).to.have.property('statusCode', 409); | ||
done(); | ||
request.put(options, function (error, response, body) { | ||
if (error) return done(error); | ||
expect(response).to.have.property('statusCode', 409); | ||
done(); | ||
}); | ||
}); | ||
@@ -765,6 +770,7 @@ }); | ||
it('should not send "409 Conflict" if there is no version conflict (equal)', function (done) { | ||
it('should send "409 Conflict" if there is a version conflict (greater than)', function (done) { | ||
var options = { | ||
url: 'http://localhost:8012/api/v1/cheeses/Camembert', | ||
json: true | ||
url: 'http://localhost:8012/api/v1/liens', | ||
json: true, | ||
body: { title: 'Smithton' } | ||
}; | ||
@@ -776,9 +782,9 @@ request.get(options, function (error, response, body) { | ||
var options = { | ||
url: 'http://localhost:8012/api/v1/cheeses/Camembert', | ||
url: 'http://localhost:8012/api/v1/liens/' + body[1]._id, | ||
json: true, | ||
body: { __v: body.__v } | ||
body: { __v: body[1].__v + 10 } | ||
}; | ||
request.put(options, function (error, response, body) { | ||
if (error) return done(error); | ||
expect(response).to.have.property('statusCode', 200); | ||
expect(response).to.have.property('statusCode', 409); | ||
done(); | ||
@@ -789,5 +795,5 @@ }); | ||
it('should not send "409 Conflict" if there is no version conflict (greater than)', function (done) { | ||
it('should not send "409 Conflict" if there is no version conflict (equal)', function (done) { | ||
var options = { | ||
url: 'http://localhost:8012/api/v1/cheeses/Camembert', | ||
url: 'http://localhost:8012/api/v1/liens', | ||
json: true | ||
@@ -800,5 +806,5 @@ }; | ||
var options = { | ||
url: 'http://localhost:8012/api/v1/cheeses/Camembert', | ||
url: 'http://localhost:8012/api/v1/liens/' + body[1]._id, | ||
json: true, | ||
body: { __v: body.__v + 10 } | ||
body: { __v: body[1].__v } | ||
}; | ||
@@ -813,29 +819,53 @@ request.put(options, function (error, response, body) { | ||
it('should send "409 Conflict" if alwaysCheckVersion is enabled and no version is sent', function (done) { | ||
it('should cause an error if locking is enabled and no version is selected', function (done) { | ||
var options = { | ||
url: 'http://localhost:8012/api/v1/stores/Westlake', | ||
url: 'http://localhost:8012/api/v1/liens', | ||
json: true, | ||
body: { } | ||
body: { title: 'Forest Expansion' } | ||
}; | ||
request.put(options, function (error, response, body) { | ||
request.get(options, function (error, response, body) { | ||
if (error) return done(error); | ||
expect(response).to.have.property('statusCode', 409); | ||
done(); | ||
expect(response).to.have.property('statusCode', 200); | ||
var options = { | ||
url: 'http://localhost:8012/api/v1/liens/' + body[0]._id, | ||
json: true, | ||
qs: { select: '-__v' }, | ||
body: { __v: 1000 } | ||
}; | ||
request.put(options, function (error, response, body) { | ||
if (error) return done(error); | ||
expect(response).to.have.property('statusCode', 500); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
it('should cause an error if alwaysCheckVersion is enabled and no version is selected', function (done) { | ||
it('should cause an error if locking is enabled and no version is selected', function (done) { | ||
var options = { | ||
url: 'http://localhost:8012/api/v1/stores/Westlake', | ||
url: 'http://localhost:8012/api/v1/liens', | ||
json: true, | ||
qs: { select: '-__v' }, | ||
body: { __v: 1000 } | ||
body: { title: 'Forest Expansion' } | ||
}; | ||
request.put(options, function (error, response, body) { | ||
request.get(options, function (error, response, body) { | ||
if (error) return done(error); | ||
expect(response).to.have.property('statusCode', 500); | ||
done(); | ||
expect(response).to.have.property('statusCode', 200); | ||
var options = { | ||
url: 'http://localhost:8012/api/v1/liens/' + body[0]._id, | ||
json: true, | ||
qs: { select: '-__v' }, | ||
body: { __v: 1000 } | ||
}; | ||
request.put(options, function (error, response, body) { | ||
if (error) return done(error); | ||
expect(response).to.have.property('statusCode', 500); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
it('should not send 409 if locking is not enabled'); | ||
}); |
@@ -22,7 +22,2 @@ var mongoose = require('mongoose'); | ||
Stores.on('save', function (next) { | ||
this.increment(); | ||
next(); | ||
}); | ||
var Tools = new Schema({ | ||
@@ -44,9 +39,6 @@ name: { type: String, required: true }, | ||
Cheese.on('save', function (next) { | ||
this.increment(); | ||
next(); | ||
}); | ||
var Beans = new Schema({ koji: Boolean }); | ||
var Deans = new Schema({ room: { type: Number, unique: true } }); | ||
var Liens = new Schema({ title: String }); | ||
var Means = new Schema({ average: Number }); | ||
@@ -58,2 +50,4 @@ if (!mongoose.models['tool']) mongoose.model('tool', Tools); | ||
if (!mongoose.models['dean']) mongoose.model('dean', Deans); | ||
if (!mongoose.models['lien']) mongoose.model('lien', Liens); | ||
if (!mongoose.models['mean']) mongoose.model('mean', Means); | ||
@@ -83,4 +77,3 @@ // Tools embedded controller | ||
findBy: 'name', | ||
select: '-mercoledi', | ||
'always check version': true | ||
select: '-mercoledi' | ||
}); | ||
@@ -133,5 +126,21 @@ | ||
baucis.rest({ | ||
singular: 'lien', | ||
locking: true | ||
}); | ||
baucis.rest({ | ||
singular: 'mean', | ||
locking: true, | ||
'always check version': true | ||
}); | ||
app = express(); | ||
app.use('/api/v1', baucis()); | ||
app.use(function (error, request, response, next) { | ||
if (error) return response.send(500, error.toString()); | ||
next(); | ||
}); | ||
server = app.listen(8012); | ||
@@ -138,0 +147,0 @@ |
@@ -59,2 +59,7 @@ var mongoose = require('mongoose'); | ||
app.use(function (error, request, response, next) { | ||
if (error) return response.send(500, error.toString()); | ||
next(); | ||
}); | ||
server = app.listen(8012); | ||
@@ -61,0 +66,0 @@ |
@@ -104,2 +104,7 @@ // __Dependencies__ | ||
app.use(function (error, request, response, next) { | ||
if (error) return response.send(500, error.toString()); | ||
next(); | ||
}); | ||
server = app.listen(8012); | ||
@@ -106,0 +111,0 @@ |
620684
0.32%3733
1.06%