diff --git a/Readme.md b/Readme.md index c0106c6..99a822a 100644 --- a/Readme.md +++ b/Readme.md @@ -207,6 +207,25 @@ user.save(function(err){ Destroy and invoke optional `fn(err)`. Emits "destroy" when successfully deleted. + +### .toError(fn) + + Register a custom error handler for the model class and/or model instances. + If called on the Model class, the error handler is used for static methods as well as the methods of the model + instances. If called on a model instance, the error handler is only used for methods of this instance. + +```js +var handler1 = function(err, res) {...}; +var handler2 = function(err, res) {...}; + +var Car = model('Car') + .toError(handler1); + +var car1 = new Car(); // uses handler1 + +var car2 = new Car(); +car2.toError(handler2); // uses handler2 +``` ## Links diff --git a/lib/proto.js b/lib/proto.js index f448b61..b4d93e9 100644 --- a/lib/proto.js +++ b/lib/proto.js @@ -144,8 +144,9 @@ exports.destroy = function(fn){ this.request .del(url) .set(this.model._headers) - .end(function(res){ - if (res.error) return fn(error(res), res); + .end(function(err, res){ + if (err || res.error) + return fn(self._toError(err,res), res); self.destroyed = true; self.model.emit('destroy', self, res); self.emit('destroy'); @@ -178,8 +179,10 @@ exports.save = function(fn){ .post(url) .set(this.model._headers) .send(self) - .end(function(res){ - if (res.error) return fn(error(res), res); + .end(function(err, res){ + if (err || res.error) + return fn(self._toError(err,res), res); + if (res.body) self.primary(res.body[key]); self.dirty = {}; self.model.emit('save', self, res); @@ -206,8 +209,9 @@ exports.update = function(fn){ .put(url) .set(this.model._headers) .send(self) - .end(function(res){ - if (res.error) return fn(error(res), res); + .end(function(err,res){ + if (err || res.error) + return fn(self._toError(err, res), res); self.dirty = {}; self.model.emit('save', self, res); self.emit('save'); @@ -288,13 +292,31 @@ exports.toJSON = function(){ }; /** - * Response error helper. + * Register a custom error handler for the model instance. + * + * @param {Function} fn + * @return {Object} self + * @api public + */ + +exports.toError = function(fn) { + if (fn) { + this._toError = fn; + } + + return this; +} + +/** + * Standard error handler. * - * @param {Response} er + * @param {Error} err + * @param {Response} res * @return {Error} * @api private */ -function error(res) { - return new Error('got ' + res.status + ' response'); +exports._toError = function(err, res) { + if (err) return err; + else return new Error('got ' + res.status + ' response'); } diff --git a/lib/static.js b/lib/static.js index 55923da..7b6759b 100644 --- a/lib/static.js +++ b/lib/static.js @@ -2,7 +2,11 @@ * Module dependencies. */ -var Collection = require('collection'); +try { + var Collection = require('collection'); +} catch (e) { + var Collection = require('component-collection'); +} var request = require('superagent'); var noop = function(){}; @@ -146,8 +150,9 @@ exports.destroyAll = function(fn){ this.request .del(url) .set(this._headers) - .end(function(res){ - if (res.error) return fn(error(res), null, res); + .end(function(err, res){ + if (err || res.error) + return fn(self._toError(err,res), null, res); fn(null, [], res); }); }; @@ -165,8 +170,9 @@ exports.all = function(fn){ this.request .get(url) .set(this._headers) - .end(function(res){ - if (res.error) return fn(error(res), null, res); + .end(function(err, res){ + if (err || res.error) + return fn(self._toError(err,res), null, res); var col = new Collection; for (var i = 0, len = res.body.length; i < len; ++i) { col.push(new self(res.body[i])); @@ -189,21 +195,38 @@ exports.get = function(id, fn){ this.request .get(url) .set(this._headers) - .end(function(res){ - if (res.error) return fn(error(res), null, res); + .end(function(err, res){ + if (err || res.error) + return fn(self._toError(err,res), null, res); var model = new self(res.body); fn(null, model, res); }); }; /** - * Response error helper. + * Register a custom error handler for the model class and its instances. * - * @param {Response} er + * @param {Function} fn + * @return {Object} self + * @api public + */ +exports.toError = function(fn) { + if (fn) { + this._toError = this.prototype._toError = fn; + } + + return this; +} + +/** + * Standard error handler. + * + * @param {Error} err + * @param {Response} res * @return {Error} * @api private */ - -function error(res) { - return new Error('got ' + res.status + ' response'); +exports._toError = function(err, res) { + if (err) return err; + else return new Error('got ' + res.status + ' response'); } diff --git a/package.json b/package.json index 6cacab3..3f67577 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,15 @@ "main": "lib/index.js", "dependencies": { "component-emitter": "~1.1.3", - "collection": "git://github.com/component/collection#0.0.2", + "component-collection": "~0.0.2", "component-each": "~0.2.5", "superagent": "~0.21.0" }, + "browser": { + "emitter": "component-emitter", + "collection": "component-collection", + "each": "component-each" + }, "devDependencies": { "express": "3.0.0" } diff --git a/test/model.js b/test/model.js index 3e36103..f088afd 100644 --- a/test/model.js +++ b/test/model.js @@ -441,3 +441,63 @@ describe('Model#isValid()', function(){ assert(0 == user.errors.length); }) }) + +describe('Model#toError()', function() { + function getErrorHandler(static) { + return function(err, res) { + return {error: err, response: res, static: static}; + }; + } + + it('should be called on errors', function(done) { + var Car = model('Car') + .attr('id') + .attr('color'); + + var car = new Car({color: 'blue'}); + + car.toError(getErrorHandler(false)); + + car.save(function(err) { + assert(null == err.error); + assert(500 == err.response.status); + assert(false == err.static); + + done(); + }); + }); + + it('should overwrite the static error handler', function(done) { + var Car = model('Car') + .attr('id') + .attr('color') + .toError(getErrorHandler(true)); + + var car = new Car({color: 'red'}); + + car.toError(getErrorHandler(false)); + + car.save(function(err) { + assert(null == err.error); + assert(500 == err.response.status); + assert(false == err.static); + + done(); + }); + }); + + it('should use the default error handler if not called', function(done) { + var Car = model('Car') + .attr('id') + .attr('color'); + + var car = new Car({color: 'yellow'}); + + car.save(function(err) { + assert(err instanceof Error); + assert('got 500 response' == err.message); + + done(); + }); + }); +}); diff --git a/test/server.js b/test/server.js index f0abc86..48cc406 100644 --- a/test/server.js +++ b/test/server.js @@ -101,5 +101,21 @@ app.post('/users', function(req, res){ res.send({ id: id }); }); +app.get('/cars', function(req, res) { + setTimeout(function() { + res.send([]); + }, 500); +}); + +app.get('/cars/:id', function(req, res) { + setTimeout(function() { + res.send({id: req.params.id, color: 'silver'}); + }, 500); +}); + +app.post('/cars', function(req, res) { + res.send(500); +}); + app.listen(4000); console.log('test server listening on port 4000'); diff --git a/test/statics.js b/test/statics.js index d579f50..8245090 100644 --- a/test/statics.js +++ b/test/statics.js @@ -68,3 +68,59 @@ describe('Model.route(string)', function(){ assert('/api/u/edit' == User.url('edit')); }) }) + +describe('Model.toError()', function() { + var Car = model('Car') + .attr('id') + .attr('color') + .toError(function(err, res) { + return {error: err, response: res}; + }); + + var _request_get = Car.request.get; + + before( function(done) { + Car.request.get = function(url, data, fn) { + var req = _request_get(url, data, fn); + req.timeout(1); + return req; + }; + done(); + }); + + after( function(done) { + Car.request.get = _request_get; + done(); + }); + + + it('should be called on errors (e.g. request timeout)', function(done) { + Car.all(function(err, res) { + assert(err.error instanceof Error); + assert(-1 != err.error.message.indexOf('timeout')); + done(); + }); + }); + + it('should be inherited by model instances', function(done) { + var car = new Car({color: 'silver'}); + + car.save(function(err) { + assert(500 == err.response.status); + done(); + }); + }); + + it('should use the default error handler if not called', function(done) { + var Car = model('Car') + .attr('id') + .attr('color'); + + Car.all(function(err) { + assert(err instanceof Error); + assert(-1 != err.message.indexOf('timeout')); + + done(); + }); + }); +});