diff --git a/.jshintrc b/.jshintrc index 8b243dadc4..149fb61160 100644 --- a/.jshintrc +++ b/.jshintrc @@ -3,7 +3,9 @@ "it": true, "describe": true, "before": true, + "beforeEach": true, "after": true, + "afterEach": true, "exports": true }, "unused": true, diff --git a/lib/application.js b/lib/application.js index fa8870b1ea..5ff5ac8a79 100644 --- a/lib/application.js +++ b/lib/application.js @@ -11,7 +11,7 @@ var stripSlashes = function (name) { module.exports = { init: function () { _.extend(this, { - methods: ['find', 'get', 'create', 'update', 'remove'], + methods: ['find', 'get', 'create', 'update', 'patch', 'remove'], mixins: mixins, services: {}, providers: [] diff --git a/lib/feathers.js b/lib/feathers.js index a95f94a064..e843bd81c7 100644 --- a/lib/feathers.js +++ b/lib/feathers.js @@ -13,12 +13,16 @@ var providers = require('./providers'); * @api public */ -function createApplication() { +function createApplication(config) { var app = express(); Proto.mixin(Application, app); app.init(); + // Add REST provider by default, can always be disabled using app.disable('feathers rest') - app.use(express.urlencoded()).use(express.json()).configure(providers.rest()); + if(!config || config.rest) { + app.use(express.urlencoded()).use(express.json()).configure(providers.rest()); + } + return app; } diff --git a/lib/mixins/event.js b/lib/mixins/event.js index 67eef68e00..02ad907b79 100644 --- a/lib/mixins/event.js +++ b/lib/mixins/event.js @@ -6,7 +6,8 @@ var EventEmitter = require('events').EventEmitter; var eventMappings = { create: 'created', update: 'updated', - remove: 'removed' + remove: 'removed', + patch: 'patched' }; /** diff --git a/lib/providers/index.js b/lib/providers/index.js index 8b8e267ac7..e46f18ef89 100644 --- a/lib/providers/index.js +++ b/lib/providers/index.js @@ -2,6 +2,6 @@ module.exports = { rest: require('./rest'), - socketio: require('./socketio'), - primus: require('./primus') + socketio: require('./socket/socketio'), + primus: require('./socket/primus') }; diff --git a/lib/providers/rest/index.js b/lib/providers/rest/index.js index 67fa613db2..e814fd4fc1 100644 --- a/lib/providers/rest/index.js +++ b/lib/providers/rest/index.js @@ -1,19 +1,22 @@ 'use strict'; -var _ = require('lodash'); var wrappers = require('./wrappers'); module.exports = function (config) { config = config || {}; - var responder = config.responder || function (req, res) { - res.format(_.extend({ + var handler = config.handler || function (req, res) { + res.format({ 'application/json': function () { res.json(res.data); } - }, config.formatters)); + }); }; + if(typeof config === 'function') { + handler = config; + } + return function () { var app = this; @@ -36,16 +39,18 @@ module.exports = function (config) { // GET / -> service.find(cb, params) app.get(uri, app.rest.find(service)) - // GET /:id -> service.get(cb, id, params) + // GET /:id -> service.get(id, params, cb) .get(uri + '/:id', app.rest.get(service)) - // POST -> service.create(cb, data, params) + // POST -> service.create(data, params, cb) .post(uri, app.rest.create(service)) - // PUT /:id -> service.update(cb, id, data, params) + // PUT /:id -> service.update(id, data, params, cb) .put(uri + '/:id', app.rest.update(service)) - // DELETE /:id -> service.remove(cb, id, params) - .del(uri + '/:id', app.rest.remove(service)); + // DELETE /:id -> service.remove(id, params, cb) + .del(uri + '/:id', app.rest.remove(service)) + // PATCH /:id -> service.patch(id, data, params, callback) + .patch(uri + '/:id', app.rest.patch(service)); - app.use(uri, responder); + app.use(uri, handler); }); }; }; diff --git a/lib/providers/rest/wrappers.js b/lib/providers/rest/wrappers.js index 29fa57c3f7..509d585a12 100644 --- a/lib/providers/rest/wrappers.js +++ b/lib/providers/rest/wrappers.js @@ -102,6 +102,17 @@ module.exports = { }; }, + patch: function(service) { + return function(req, res, next) { + var error = checkMethod(res, service, 'patch'); + if (error) { + return next(error); + } + + service.patch(req.params.id, req.body, getParams(req), wrap(req, res, next)); + }; + }, + remove: function(service) { return function(req, res, next) { var error = checkMethod(res, service, 'remove'); diff --git a/lib/providers/socket/commons.js b/lib/providers/socket/commons.js new file mode 100644 index 0000000000..40711e804d --- /dev/null +++ b/lib/providers/socket/commons.js @@ -0,0 +1,50 @@ +var _ = require('lodash'); + +// The position of the params parameters for a service method so that we can extend them +// default is 1 +var paramsPositions = { + find: 0, + update: 2, + patch: 2 +}; + +// Set up the service method handlers for a service and socket. +exports.setupMethodHandler = function setupMethodHandler (emitter, params, service, path, method) { + var name = path + '::' + method; + var position = typeof paramsPositions[method] !== 'undefined' ? paramsPositions[method] : 1; + + if (typeof service[method] === 'function') { + emitter.on(name, function () { + var args = _.toArray(arguments); + args[position] = _.extend({}, args[position], params); + service[method].apply(service, args); + }); + } +}; + +// Set up event handlers for a given service and connected sockets. +// Send it through the service dispatching mechanism (`removed(data, params, callback)`, +// `updated(data, params, callback)` and `created(data, params, callback)`) if it +// exists. +exports.setupEventHandler = function setupEventHandler (info, service, path, ev) { + var defaultDispatcher = function (data, params, callback) { + callback(null, data); + }; + + service.on(ev, function (data) { + // Check if there is a method on the service with the same name as the event + var dispatcher = typeof service[ev] === 'function' ? service[ev] : defaultDispatcher; + var eventName = path + ' ' + ev; + + info.emitters().forEach(function (emitter) { + dispatcher(data, info.params(emitter), function (error, dispatchData) { + if (error) { + emitter[info.method]('error', error); + } else if (dispatchData) { + emitter[info.method](eventName, dispatchData); + } + }); + }); + }); +}; + diff --git a/lib/providers/primus.js b/lib/providers/socket/primus.js similarity index 55% rename from lib/providers/primus.js rename to lib/providers/socket/primus.js index 04aeb19172..8aa2463401 100644 --- a/lib/providers/primus.js +++ b/lib/providers/socket/primus.js @@ -4,11 +4,11 @@ var _ = require('lodash'); var Proto = require('uberproto'); var Primus = require('primus'); var Emitter = require('primus-emitter'); +var commons = require('./commons'); module.exports = function(config, configurer) { return function() { var app = this; - var services = {}; app.enable('feathers primus'); @@ -23,30 +23,35 @@ module.exports = function(config, configurer) { } var primus = this.primus = new Primus(server, config); + var info = { + emitters: function() { + return primus; + }, + params: function(spark) { + return spark.request.feathers; + }, + method: 'send' + }; + primus.use('emitter', Emitter); - _.each(services, function(service, path) { - // If the service emits events that we want to listen to (Event mixin) - if (typeof service.on === 'function' && service._serviceEvents) { - _.each(service._serviceEvents, function(ev) { - service.on(ev, function(data) { - primus.forEach(function (spark) { - spark.send(path + ' ' + ev, data); - }); - }); + // For a new connection, set up the service method handlers + primus.on('connection', function (spark) { + _.each(self.services, function (service, path) { + _.each(self.methods, function (method) { + commons.setupMethodHandler(spark, spark.request.feathers, service, path, method); }); - } + }); }); - primus.on('connection', function(spark) { - _.each(services, function(service, path) { - _.each(self.methods, function(method) { - var name = path + '::' + method; - if (service[method]) { - spark.on(name, service[method].bind(service)); - } + // Set up events and event dispatching + _.each(self.services, function (service, path) { + // If the service emits events that we want to listen to (Event mixin) + if (typeof service.on === 'function' && service._serviceEvents) { + _.each(service._serviceEvents, function (ev) { + commons.setupEventHandler(info, service, path, ev); }); - }); + } }); if (typeof configurer === 'function') { @@ -56,9 +61,5 @@ module.exports = function(config, configurer) { return result; } }, app); - - app.providers.push(function(path, service) { - services[path] = service; - }); }; }; diff --git a/lib/providers/socket/socketio.js b/lib/providers/socket/socketio.js new file mode 100644 index 0000000000..fe3a9aff67 --- /dev/null +++ b/lib/providers/socket/socketio.js @@ -0,0 +1,63 @@ +'use strict'; + +var _ = require('lodash'); +var socketio = require('socket.io'); +var Proto = require('uberproto'); +var commons = require('./commons'); + +module.exports = function (config) { + return function () { + var app = this; + + app.enable('feathers socketio'); + + // Monkey patch app.setup(server) + Proto.mixin({ + setup: function (server) { + var self = this; + var result = this._super.apply(this, arguments); + + if (this.disabled('feathers socketio')) { + return result; + } + + var io = this.io = socketio.listen(server); + // The info object we can pass to commons.setupEventHandler + var info = { + emitters: function() { + return io.sockets.clients(); + }, + params: function(socket) { + return socket.handshake.feathers; + }, + method: 'emit' + }; + + // For a new connection, set up the service method handlers + io.sockets.on('connection', function (socket) { + _.each(self.services, function (service, path) { + _.each(self.methods, function (method) { + commons.setupMethodHandler(socket, socket.handshake.feathers, service, path, method); + }); + }); + }); + + // Set up events and event dispatching + _.each(self.services, function (service, path) { + // If the service emits events that we want to listen to (Event mixin) + if (typeof service.on === 'function' && service._serviceEvents) { + _.each(service._serviceEvents, function (ev) { + commons.setupEventHandler(info, service, path, ev); + }); + } + }); + + if (typeof config === 'function') { + config.call(this, io); + } + + return result; + } + }, app); + }; +}; diff --git a/lib/providers/socketio.js b/lib/providers/socketio.js deleted file mode 100644 index 74e2368bbd..0000000000 --- a/lib/providers/socketio.js +++ /dev/null @@ -1,60 +0,0 @@ -'use strict'; - -var _ = require('lodash'); -var socketio = require('socket.io'); -var Proto = require('uberproto'); - -module.exports = function(config) { - return function() { - var app = this; - var services = {}; - - app.enable('feathers socketio'); - - // Monkey patch app.setup(server) - Proto.mixin({ - setup: function(server) { - var self = this; - var result = this._super.apply(this, arguments); - - if (this.disabled('feathers socketio')) { - return result; - } - - var io = this.io = socketio.listen(server); - - _.each(services, function(service, path) { - // If the service emits events that we want to listen to (Event mixin) - if (typeof service.on === 'function' && service._serviceEvents) { - _.each(service._serviceEvents, function(ev) { - service.on(ev, function(data) { - io.sockets.emit(path + ' ' + ev, data); - }); - }); - } - }); - - io.sockets.on('connection', function(socket) { - _.each(services, function(service, path) { - _.each(self.methods, function(method) { - var name = path + '::' + method; - if (service[method]) { - socket.on(name, service[method].bind(service)); - } - }); - }); - }); - - if (typeof config === 'function') { - config.call(this, io); - } - - return result; - } - }, app); - - app.providers.push(function(path, service) { - services[path] = service; - }); - }; -}; diff --git a/package.json b/package.json index ff2cd1cd4c..bf1f587120 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "feathers", "description": "An ultra scalable, feather weight, data oriented framework", - "version": "0.3.2", + "version": "0.4.0", "homepage": "http://feathersjs.com", "repository": { "type": "git", diff --git a/readme.md b/readme.md index bbe0840153..1abd497e54 100644 --- a/readme.md +++ b/readme.md @@ -29,6 +29,32 @@ app.use('/todos', { }); ``` +The default REST handler is a middleware that formats the data retrieved by the service as JSON. REST handling will be set up automatically when calling `var app = feathers()`. If you would like to configure the REST provider yourself, call `var app = feathers({ rest: false });`. + +Then you can configure it manually and add your own `handler` middleware that, for example just renders plain text with the todo description (`res.data` contains the data returned by the service): + +```js +var app = feathers({ rest: false }); + +app.use(feathers.urlencoded()).use(feathers.json()) + .configure(feathers.rest(function restFormatter(req, res) { + res.format({ + 'text/plain': function() { + res.end('The todo is: ' + res.data.description); + } + }); + })) + .use('/todo', { + get: function (id, params, callback) { + callback(null, { description: 'You have to do ' + id }); + } + }); +``` + +__Note:__ When configuring REST this way, you *have* to add `app.use(feathers.urlencoded()).use(feathers.json())` to support request body parsing. + +If you want to add other middleware *before* the REST handler, simply call `app.use(middleware)` before configuring the handler. + ### SocketIO To expose services via [SocketIO](http://socket.io/) call `app.configure(feathers.socketio())`. It is also possible pass a `function(io) {}` when initializing the provider where `io` is the main SocketIO object. Since Feathers is only using the SocketIO default configuration, this is a good spot to initialize the [recommended production settings](https://github.com/LearnBoost/Socket.IO/wiki/Configuring-Socket.IO#recommended-production-settings): @@ -74,6 +100,24 @@ app.configure(feathers.socketio(function(io) { })); ``` +Similar than the REST middleware, the SocketIO handshakes `feathers` property will be extended +for service parameters: + +```js +app.configure(feathers.socketio(function(io) { + io.set('authorization', function (handshake, callback) { + handshake.feathers.user = { name: 'David' }; + }); +})); + +app.use('todos', { + create: function(data, params, callback) { + // When called via SocketIO: + params.user // -> { name: 'David' } + } +}); +``` + Once the server has been started with `app.listen()` the SocketIO object is available as `app.io`. ### Primus @@ -117,6 +161,23 @@ In the Browser you can connect like this: ``` +Just like REST and SocketIO, the Primus request object can be extended with a `feathers` parameter during authorization which will extend the `params` for any service request: + +```js +app.configure(feathers.primus({ + transformer: 'sockjs' +}, function(primus) { + // Set up Primus authorization here + primus.authorize(function (req, done) { + req.feathers = { + user: { name: 'David' } + } + + done(); + }); +})); +``` + ## API ### listen @@ -221,6 +282,7 @@ var myService = { get: function(id, params, callback) {}, create: function(data, params, callback) {}, update: function(id, data, params, callback) {}, + patch: function(id, data, params, callback) {}, remove: function(id, params, callback) {}, setup: function(app) {} } @@ -305,6 +367,25 @@ socket.emit('todo::update', 2, { }); ``` +### patch + +`patch(id, data, params, callback)` patches the resource identified by `id` using `data`. The callback should be called with the updated resource data. Implement `patch` additionally to `update` if you want to separate between partial and full updates and support the `PATCH` HTTP method. + +__REST__ + + PATCH todo/2 + { "description": "I really have to do laundry" } + +__SocketIO__ + +```js +socket.emit('todo::patch', 2, { + description: 'I really have to do laundry' +}, {}, function(error, data) { + // data -> { id: 2, description: "I really have to do laundry" } +}); +``` + ### remove `remove(id, params, callback)` removes the resource with `id`. The callback should be called with the removed resource. @@ -419,9 +500,9 @@ __SocketIO__ ``` -### updated +### updated, patched -The `updated` event will be published with the callback data when a service `update` calls back successfully. +The `updated` and `patched` events will be published with the callback data when a service `update` or `patch` method calls back successfully. ```js app.use('/my/todos/', { @@ -484,6 +565,74 @@ __SocketIO__ ``` +### Event filtering + +By default all service events will be dispatched to all connected clients. +In many cases you probably want to be able to only dispatch events for certain clients. +This can be done by implementing the `created`, `updated`, `patched` and `removed` methods as `function(data, params, callback) {}` with `params` being the parameters set when the client connected, in SocketIO when authorizing and setting `handshake.feathers` and Primus with `req.feathers`. + +```js +var myService = { + created: function(data, params, callback) {}, + updated: function(data, params, callback) {}, + patched: function(data, params, callback) {}, + removed: function(data, params, callback) {} +} +``` + +The event dispatching service methods will be run for every connected client. Calling the callback with data (that you also may modify) will dispatch the according event. Callling back with a falsy value will prevent the event being dispatched to this client. + +The following example only dispatches the Todo `updated` event if the authorized user belongs to the same company: + +```js +app.configure(feathers.socketio(function(io) { + io.set('authorization', function (handshake, callback) { + // Authorize using the /users service + app.lookup('users').find({ + username: handshake.username, + password: handshake.password + }, function(error, user) { + if(!error || !user) { + return callback(error, false); + } + + handshake.feathers = { + user: user + }; + + callback(null, true); + }); + }); +})); + +app.use('todos', { + update: function(id, data, params, callback) { + // Update + callback(null, data); + }, + + updated: function(todo, params, callback) { + // params === handshake.feathers + if(todo.companyId === params.user.companyId) { + // Dispatch the todo data to this client + return callback(null, todo); + } + + // Call back with a falsy value to prevent dispatching + callback(null, false); + } +}); +``` + +On the client: + +```js +socket.on('todo updated', function(data) { + // The client will only get this event + // if authorized and in the same company +}); +``` + ## Why? We know! Oh God another NodeJS framework! We really didn't want to add another name to the long list of NodeJS web frameworks but also wanted to explore a different approach than any other framework we have seen. We strongly believe that data is the core of the web and should be the focus of web applications. @@ -494,6 +643,13 @@ With that being said there are some amazing frameworks already out there and we ## Changelog +__0.4.0__ + +- Allow socket provider event filtering and params passthrough ([#49](https://github.com/feathersjs/feathers/pull/49), [#50](https://github.com/feathersjs/feathers/pull/50), [#51](https://github.com/feathersjs/feathers/pull/51)) +- Added `patch` support ([#47](https://github.com/feathersjs/feathers/pull/47)) +- Allow to configure REST handler manually ([#40](https://github.com/feathersjs/feathers/issues/40), [#52](https://github.com/feathersjs/feathers/pull/52)) + + __0.3.2__ - Allows Feathers to use other Express apps ([#46](https://github.com/feathersjs/feathers/pull/46)) diff --git a/test/providers/primus.test.js b/test/providers/primus.test.js index 457656e979..199c625ed5 100644 --- a/test/providers/primus.test.js +++ b/test/providers/primus.test.js @@ -1,22 +1,35 @@ 'use strict'; +var assert = require('assert'); +var _ = require('lodash'); + var feathers = require('../../lib/feathers'); var fixture = require('./service-fixture'); var todoService = fixture.Service; var verify = fixture.verify; describe('Primus provider', function () { - var server, socket; + var server, socket, app, + socketParams = { + user: { name: 'David' }, + provider: 'socketio' + }; before(function () { - server = feathers() + app = feathers() .configure(feathers.primus({ transformer: 'socket.io' }, function(primus) { socket = new primus.Socket('http://localhost:7888'); + + primus.authorize(function (req, done) { + req.feathers = socketParams; + done(); + }); })) - .use('todo', todoService) - .listen(7888); + .use('todo', todoService); + + server = app.listen(7888); }); after(function (done) { @@ -24,6 +37,47 @@ describe('Primus provider', function () { server.close(done); }); + it('passes handshake as service parameters', function(done) { + var service = app.lookup('todo'); + var old = { + find: service.find, + create: service.create, + update: service.update, + remove: service.remove + }; + + service.find = function(params) { + assert.deepEqual(params, socketParams, 'Handshake parameters passed on proper position'); + old.find.apply(this, arguments); + }; + + service.create = function(data, params) { + assert.deepEqual(params, socketParams, 'Passed handshake parameters'); + old.create.apply(this, arguments); + }; + + service.update = function(id, data, params) { + assert.deepEqual(params, _.extend(socketParams, { + test: 'param' + }), 'Extended handshake paramters with original'); + old.update.apply(this, arguments); + }; + + service.remove = function(id, params) { + assert.equal(params.provider, 'socketio', 'Handshake parameters have priority'); + old.remove.apply(this, arguments); + }; + + socket.send('todo::create', {}, {}, function () { + socket.send('todo::update', 1, {}, { test: 'param' }, function() { + socket.send('todo::remove', 1, { provider: 'something' }, function() { + _.extend(service, old); + done(); + }); + }); + }); + }); + describe('CRUD', function () { it('::find', function (done) { socket.send('todo::find', {}, function (error, data) { @@ -65,6 +119,18 @@ describe('Primus provider', function () { }); }); + it('::patch', function (done) { + var original = { + name: 'patching' + }; + + socket.send('todo::patch', 25, original, {}, function (error, data) { + verify.patch(25, original, data); + + done(error); + }); + }); + it('::remove', function (done) { socket.send('todo::remove', 11, {}, function (error, data) { verify.remove(11, data); @@ -80,7 +146,7 @@ describe('Primus provider', function () { name: 'created event' }; - socket.on('todo created', function (data) { + socket.once('todo created', function (data) { verify.create(original, data); done(); }); @@ -93,7 +159,7 @@ describe('Primus provider', function () { name: 'updated event' }; - socket.on('todo updated', function (data) { + socket.once('todo updated', function (data) { verify.update(10, original, data); done(); }); @@ -101,8 +167,21 @@ describe('Primus provider', function () { socket.send('todo::update', 10, original, {}, function () {}); }); + it('patched', function(done) { + var original = { + name: 'patched event' + }; + + socket.once('todo patched', function (data) { + verify.patch(12, original, data); + done(); + }); + + socket.send('todo::patch', 12, original, {}, function () {}); + }); + it('removed', function (done) { - socket.on('todo removed', function (data) { + socket.once('todo removed', function (data) { verify.remove(333, data); done(); }); @@ -110,4 +189,66 @@ describe('Primus provider', function () { socket.send('todo::remove', 333, {}, function () {}); }); }); + + describe('Event filtering', function() { + it('.created', function (done) { + var service = app.lookup('todo'); + var original = { description: 'created event test' }; + var oldCreated = service.created; + + service.created = function(data, params, callback) { + assert.deepEqual(params, socketParams); + verify.create(original, data); + + callback(null, _.extend({ processed: true }, data)); + }; + + socket.send('todo::create', original, {}, function() {}); + + socket.once('todo created', function (data) { + service.created = oldCreated; + // Make sure Todo got processed + verify.create(_.extend({ processed: true }, original), data); + done(); + }); + }); + + it('.updated', function (done) { + var original = { + name: 'updated event' + }; + + socket.once('todo updated', function (data) { + verify.update(10, original, data); + done(); + }); + + socket.send('todo::update', 10, original, {}, function () {}); + }); + + it('.removed', function (done) { + var service = app.lookup('todo'); + var oldRemoved = service.removed; + + service.removed = function(data, params, callback) { + assert.deepEqual(params, socketParams); + + if(data.id === 23) { + // Only dispatch with given id + return callback(null, data); + } + + callback(); + }; + + socket.send('todo::remove', 1, {}, function() {}); + socket.send('todo::remove', 23, {}, function() {}); + + socket.on('todo removed', function (data) { + service.removed = oldRemoved; + assert.equal(data.id, 23); + done(); + }); + }); + }); }); diff --git a/test/providers/rest.test.js b/test/providers/rest.test.js index bcec86d708..507153b9c9 100644 --- a/test/providers/rest.test.js +++ b/test/providers/rest.test.js @@ -77,6 +77,26 @@ describe('REST provider', function () { }); }); + it('PATCH .patch', function (done) { + var original = { + description: 'PATCH .patch' + }; + + request({ + url: 'http://localhost:4777/todo/544', + method: 'patch', + body: JSON.stringify(original), + headers: { + 'Content-Type': 'application/json' + } + }, function (error, response, body) { + assert.ok(response.statusCode === 200, 'Got OK status code'); + verify.patch(544, original, JSON.parse(body)); + + done(error); + }); + }); + it('DELETE .remove', function (done) { request({ url: 'http://localhost:4777/todo/233', @@ -121,16 +141,16 @@ describe('REST provider', function () { }); }); - it('throws a 405 for undefined service methods', function(done) { + it('throws a 405 for undefined service methods', function (done) { var app = feathers().use('todo', { - get: function(id, params, callback) { - callback(null, { description: 'You have to do ' + id }); - } - }); + get: function (id, params, callback) { + callback(null, { description: 'You have to do ' + id }); + } + }); /* jshint ignore:start */ // Error handler - app.use(function(error, req, res, next) { + app.use(function (error, req, res, next) { assert.equal(error.message, 'Can not call service method .find'); res.json({ message: error.message }); }); @@ -148,4 +168,56 @@ describe('REST provider', function () { }); }); }); + + it('disables REST and lets you set the handler manually', function(done) { + var app = feathers({ rest: false }); + + app.configure(feathers.rest(function restFormatter(req, res) { + res.format({ + 'text/plain': function() { + res.end('The todo is: ' + res.data.description); + } + }); + })) + .use('/todo', { + get: function (id, params, callback) { + callback(null, { description: 'You have to do ' + id }); + } + }); + + var server = app.listen(4776); + request('http://localhost:4776/todo/dishes', function (error, response, body) { + assert.equal(body, 'The todo is: You have to do dishes'); + server.close(done); + }); + }); + + it('Lets you configure your own middleware before the handler (#40)', function(done) { + var data = { description: 'Do dishes!', id: 'dishes' }; + var app = feathers({ rest: false }); + + app.use(function defaultContentTypeMiddleware (req, res, next) { + req.headers['content-type'] = req.headers['content-type'] || 'application/json'; + next(); + }) + .use(feathers.urlencoded()) + .use(feathers.json()) + .configure(feathers.rest()) + .use('/todo', { + create: function (data, params, callback) { + callback(null, data); + } + }); + + var server = app.listen(4775); + request({ + method: 'POST', + url: 'http://localhost:4775/todo', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }, function (error, response, body) { + assert.deepEqual(JSON.parse(body), data); + server.close(done); + }); + }); }); diff --git a/test/providers/service-fixture.js b/test/providers/service-fixture.js index 945fcbe5d4..248dad9cb2 100644 --- a/test/providers/service-fixture.js +++ b/test/providers/service-fixture.js @@ -37,6 +37,13 @@ exports.Service = { callback(null, result); }, + patch: function (id, data, params, callback) { + var result = _.clone(data); + result.id = id; + result.status = 'patched'; + callback(null, result); + }, + remove: function (id, params, callback) { callback(null, { id: id @@ -70,6 +77,14 @@ exports.verify = { assert.deepEqual(expected, current, 'Data ran through .update as expected'); }, + patch: function (id, original, current) { + var expected = _.extend({}, original, { + id: id, + status: 'patched' + }); + assert.deepEqual(expected, current, 'Data ran through .patch as expected'); + }, + remove: function (id, data) { assert.deepEqual({ id: id diff --git a/test/providers/socketio.test.js b/test/providers/socketio.test.js index c1c708f287..f0af8bfc70 100644 --- a/test/providers/socketio.test.js +++ b/test/providers/socketio.test.js @@ -1,14 +1,20 @@ 'use strict'; +var _ = require('lodash'); var feathers = require('../../lib/feathers'); var io = require('socket.io-client'); +var assert = require('assert'); var fixture = require('./service-fixture'); var todoService = fixture.Service; var verify = fixture.verify; describe('SocketIO provider', function () { - var server, socket; + var server, socket, app, + socketParams = { + user: { name: 'David' }, + provider: 'socketio' + }; before(function () { // This seems to be the only way to not get the @@ -16,12 +22,17 @@ describe('SocketIO provider', function () { var oldlog = console.log; console.log = function () {}; - server = feathers() + app = feathers() .configure(feathers.socketio(function(io) { io.set('log level', 0); + io.set('authorization', function (handshake, callback) { + handshake.feathers = socketParams; + callback(null, true); + }); })) - .use('todo', todoService) - .listen(7886); + .use('todo', todoService); + + server = app.listen(7886); console.log = oldlog; @@ -33,6 +44,47 @@ describe('SocketIO provider', function () { server.close(done); }); + it('passes handshake as service parameters', function(done) { + var service = app.lookup('todo'); + var old = { + find: service.find, + create: service.create, + update: service.update, + remove: service.remove + }; + + service.find = function(params) { + assert.deepEqual(params, socketParams, 'Handshake parameters passed on proper position'); + old.find.apply(this, arguments); + }; + + service.create = function(data, params) { + assert.deepEqual(params, socketParams, 'Passed handshake parameters'); + old.create.apply(this, arguments); + }; + + service.update = function(id, data, params) { + assert.deepEqual(params, _.extend(socketParams, { + test: 'param' + }), 'Extended handshake paramters with original'); + old.update.apply(this, arguments); + }; + + service.remove = function(id, params) { + assert.equal(params.provider, 'socketio', 'Handshake parameters have priority'); + old.remove.apply(this, arguments); + }; + + socket.emit('todo::create', {}, {}, function () { + socket.emit('todo::update', 1, {}, { test: 'param' }, function() { + socket.emit('todo::remove', 1, { provider: 'something' }, function() { + _.extend(service, old); + done(); + }); + }); + }); + }); + describe('CRUD', function () { it('::find', function (done) { socket.emit('todo::find', {}, function (error, data) { @@ -74,6 +126,18 @@ describe('SocketIO provider', function () { }); }); + it('::patch', function (done) { + var original = { + name: 'patching' + }; + + socket.emit('todo::patch', 25, original, {}, function (error, data) { + verify.patch(25, original, data); + + done(error); + }); + }); + it('::remove', function (done) { socket.emit('todo::remove', 11, {}, function (error, data) { verify.remove(11, data); @@ -89,7 +153,7 @@ describe('SocketIO provider', function () { name: 'created event' }; - socket.on('todo created', function (data) { + socket.once('todo created', function (data) { verify.create(original, data); done(); }); @@ -102,7 +166,7 @@ describe('SocketIO provider', function () { name: 'updated event' }; - socket.on('todo updated', function (data) { + socket.once('todo updated', function (data) { verify.update(10, original, data); done(); }); @@ -110,8 +174,21 @@ describe('SocketIO provider', function () { socket.emit('todo::update', 10, original, {}, function () {}); }); + it('patched', function(done) { + var original = { + name: 'patched event' + }; + + socket.once('todo patched', function (data) { + verify.patch(12, original, data); + done(); + }); + + socket.emit('todo::patch', 12, original, {}, function () {}); + }); + it('removed', function (done) { - socket.on('todo removed', function (data) { + socket.once('todo removed', function (data) { verify.remove(333, data); done(); }); @@ -119,4 +196,66 @@ describe('SocketIO provider', function () { socket.emit('todo::remove', 333, {}, function () {}); }); }); + + describe('Event filtering', function() { + it('.created', function (done) { + var service = app.lookup('todo'); + var original = { description: 'created event test' }; + var oldCreated = service.created; + + service.created = function(data, params, callback) { + assert.deepEqual(params, socketParams); + verify.create(original, data); + + callback(null, _.extend({ processed: true }, data)); + }; + + socket.emit('todo::create', original, {}, function() {}); + + socket.once('todo created', function (data) { + service.created = oldCreated; + // Make sure Todo got processed + verify.create(_.extend({ processed: true }, original), data); + done(); + }); + }); + + it('.updated', function (done) { + var original = { + name: 'updated event' + }; + + socket.once('todo updated', function (data) { + verify.update(10, original, data); + done(); + }); + + socket.emit('todo::update', 10, original, {}, function () {}); + }); + + it('.removed', function (done) { + var service = app.lookup('todo'); + var oldRemoved = service.removed; + + service.removed = function(data, params, callback) { + assert.deepEqual(params, socketParams); + + if(data.id === 23) { + // Only dispatch with given id + return callback(null, data); + } + + callback(); + }; + + socket.emit('todo::remove', 1, {}, function() {}); + socket.emit('todo::remove', 23, {}, function() {}); + + socket.on('todo removed', function (data) { + service.removed = oldRemoved; + assert.equal(data.id, 23); + done(); + }); + }); + }); });