From f1651a911cf87edc97791151ac688d3b23be835b Mon Sep 17 00:00:00 2001 From: Mathias Laug Date: Fri, 28 Mar 2014 14:52:52 +0100 Subject: [PATCH 01/11] added patch support --- lib/application.js | 2 +- lib/providers/rest/index.js | 12 +++++++----- lib/providers/rest/wrappers.js | 11 +++++++++++ 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/lib/application.js b/lib/application.js index 5b87c54c21..0b5aa156de 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/providers/rest/index.js b/lib/providers/rest/index.js index a0042ddbc1..77d8d44e4e 100644 --- a/lib/providers/rest/index.js +++ b/lib/providers/rest/index.js @@ -36,14 +36,16 @@ 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); }); diff --git a/lib/providers/rest/wrappers.js b/lib/providers/rest/wrappers.js index d5b7442f3a..fdd37c7f3e 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'); From 0f059583e950aa1a049d9fdc99f1f5c09973dfff Mon Sep 17 00:00:00 2001 From: Mathias Laug Date: Tue, 1 Apr 2014 15:37:25 +0200 Subject: [PATCH 02/11] adding testcases for patch support --- test/providers/rest.test.js | 20 ++++++++++++++++++++ test/providers/service-fixture.js | 15 +++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/test/providers/rest.test.js b/test/providers/rest.test.js index bcec86d708..d3e9c074ce 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', diff --git a/test/providers/service-fixture.js b/test/providers/service-fixture.js index 49a7628de7..eaec52ae10 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 From e34d02d6438929f6e568e6919b12e9e4fa2fa672 Mon Sep 17 00:00:00 2001 From: Mathias Laug Date: Tue, 1 Apr 2014 15:47:06 +0200 Subject: [PATCH 03/11] adding testcases for patch support --- test/providers/primus.test.js | 12 ++++++++++++ test/providers/socketio.test.js | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/test/providers/primus.test.js b/test/providers/primus.test.js index 457656e979..d8603695bd 100644 --- a/test/providers/primus.test.js +++ b/test/providers/primus.test.js @@ -65,6 +65,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); diff --git a/test/providers/socketio.test.js b/test/providers/socketio.test.js index c1c708f287..3623b9efa2 100644 --- a/test/providers/socketio.test.js +++ b/test/providers/socketio.test.js @@ -74,6 +74,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); From 817e6f8ca86c6ad55b76131ffaaa5626526ac573 Mon Sep 17 00:00:00 2001 From: David Luecke Date: Wed, 2 Apr 2014 20:17:36 -0600 Subject: [PATCH 04/11] Initial implementation for SocketIO dispatching mechanism. --- .jshintrc | 2 ++ lib/providers/socketio.js | 17 ++++++++++++++-- test/providers/service-fixture.js | 16 +++++++++++++++ test/providers/socketio.test.js | 33 ++++++++++++++++++++++++++++--- 4 files changed, 63 insertions(+), 5 deletions(-) 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/providers/socketio.js b/lib/providers/socketio.js index 74e2368bbd..76fc7d9e23 100644 --- a/lib/providers/socketio.js +++ b/lib/providers/socketio.js @@ -3,6 +3,9 @@ var _ = require('lodash'); var socketio = require('socket.io'); var Proto = require('uberproto'); +var defaultDispatcher = function(data, params, callback) { + callback(null, data); +}; module.exports = function(config) { return function() { @@ -27,8 +30,18 @@ module.exports = function(config) { // 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); + service.on(ev, function serviceEvent(data) { + // Check for a `removed`, `created` or `updated` filter on the service + var dispatcher = typeof service[ev] === 'function' ? service[ev] : defaultDispatcher; + var eventName = path + ' ' + ev; + + io.sockets.clients().forEach(function(socket) { + dispatcher(data, socket.handshake, function(error, dispatchData) { + if(!error && dispatchData) { + socket.emit(eventName, dispatchData); + } + }); + }); }); }); } diff --git a/test/providers/service-fixture.js b/test/providers/service-fixture.js index 248dad9cb2..2f9d64c24f 100644 --- a/test/providers/service-fixture.js +++ b/test/providers/service-fixture.js @@ -48,6 +48,22 @@ exports.Service = { callback(null, { id: id }); + }, + + removed: function(data, params, callback) { + if(params.process) { + return callback(null, _.extend({}, data, params)); + } + + callback(null, data); + }, + + created: function(data, params, callback) { + if(params.process) { + return callback(null, _.extend({}, data, params)); + } + + callback(null, data); } }; diff --git a/test/providers/socketio.test.js b/test/providers/socketio.test.js index 3623b9efa2..ff009efdaa 100644 --- a/test/providers/socketio.test.js +++ b/test/providers/socketio.test.js @@ -19,6 +19,10 @@ describe('SocketIO provider', function () { server = feathers() .configure(feathers.socketio(function(io) { io.set('log level', 0); + io.set('authorization', function (handshakeData, callback) { + handshakeData.user = { name: 'David' }; + callback(null, true); + }); })) .use('todo', todoService) .listen(7886); @@ -101,7 +105,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(); }); @@ -114,7 +118,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(); }); @@ -123,7 +127,7 @@ describe('SocketIO provider', function () { }); it('removed', function (done) { - socket.on('todo removed', function (data) { + socket.once('todo removed', function (data) { verify.remove(333, data); done(); }); @@ -131,4 +135,27 @@ describe('SocketIO provider', function () { socket.emit('todo::remove', 333, {}, function () {}); }); }); + + describe('Event filtering', function() { + it.skip('.created', function (done) { + 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.skip('.removed', function (done) { + done(); + }); + }); }); From 001b95fb741d0fd049b9f814f29ed0cbf2a61cfb Mon Sep 17 00:00:00 2001 From: David Luecke Date: Wed, 2 Apr 2014 21:31:49 -0600 Subject: [PATCH 05/11] Adding SocketIO handshake data to service call parameters. --- lib/providers/socketio.js | 18 ++++++++-- readme.md | 18 ++++++++++ test/providers/socketio.test.js | 60 ++++++++++++++++++++++++++++++--- 3 files changed, 90 insertions(+), 6 deletions(-) diff --git a/lib/providers/socketio.js b/lib/providers/socketio.js index 74e2368bbd..d0f798d3d8 100644 --- a/lib/providers/socketio.js +++ b/lib/providers/socketio.js @@ -4,6 +4,14 @@ var _ = require('lodash'); var socketio = require('socket.io'); var Proto = require('uberproto'); +// 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 +}; + module.exports = function(config) { return function() { var app = this; @@ -38,8 +46,14 @@ module.exports = function(config) { _.each(services, function(service, path) { _.each(self.methods, function(method) { var name = path + '::' + method; - if (service[method]) { - socket.on(name, service[method].bind(service)); + var position = typeof paramsPositions[method] !== 'undefined' ? paramsPositions[method] : 1; + + if (typeof service[method] === 'function') { + socket.on(name, function() { + var args = _.toArray(arguments); + args[position] = _.extend({}, args[position], socket.handshake.feathers); + service[method].apply(service, args); + }); } }); }); diff --git a/readme.md b/readme.md index bbe0840153..567f7b4349 100644 --- a/readme.md +++ b/readme.md @@ -74,6 +74,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 diff --git a/test/providers/socketio.test.js b/test/providers/socketio.test.js index 3623b9efa2..4d2fd42e62 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) { From 9c7c279ef4bdaec9492e419e4189a526fe30515c Mon Sep 17 00:00:00 2001 From: David Luecke Date: Sat, 5 Apr 2014 19:22:05 -0600 Subject: [PATCH 06/11] Adding event dispatching tests for SocketIO. --- .jshintrc | 2 + lib/providers/socketio.js | 86 ++++++++++++++++++++----------- test/providers/service-fixture.js | 16 ++++++ test/providers/socketio.test.js | 68 ++++++++++++++++++++++-- 4 files changed, 139 insertions(+), 33 deletions(-) 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/providers/socketio.js b/lib/providers/socketio.js index d0f798d3d8..6197326bb7 100644 --- a/lib/providers/socketio.js +++ b/lib/providers/socketio.js @@ -12,16 +12,55 @@ var paramsPositions = { patch: 2 }; -module.exports = function(config) { - return function() { +// Set up the service method handlers for a service and socket. +function setupMethodHandler (socket, service, path, method) { + var name = path + '::' + method; + var position = typeof paramsPositions[method] !== 'undefined' ? paramsPositions[method] : 1; + + if (typeof service[method] === 'function') { + socket.on(name, function () { + var args = _.toArray(arguments); + args[position] = _.extend({}, args[position], socket.handshake.feathers); + 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. +function setupEventHandler (sockets, 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; + + sockets.clients().forEach(function (socket) { + dispatcher(data, socket.handshake.feathers, function (error, dispatchData) { + if (error) { + socket.emit('error', error); + } else if (dispatchData) { + socket.emit(eventName, dispatchData); + } + }); + }); + }); +} + +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) { + setup: function (server) { var self = this; var result = this._super.apply(this, arguments); @@ -31,32 +70,23 @@ module.exports = function(config) { 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); - }); + // 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) { + setupMethodHandler(socket, service, path, method); }); - } + }); }); - io.sockets.on('connection', function(socket) { - _.each(services, function(service, path) { - _.each(self.methods, function(method) { - var name = path + '::' + method; - var position = typeof paramsPositions[method] !== 'undefined' ? paramsPositions[method] : 1; - - if (typeof service[method] === 'function') { - socket.on(name, function() { - var args = _.toArray(arguments); - args[position] = _.extend({}, args[position], socket.handshake.feathers); - service[method].apply(service, args); - }); - } + // 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) { + setupEventHandler(io.sockets, service, path, ev); }); - }); + } }); if (typeof config === 'function') { @@ -66,9 +96,5 @@ module.exports = function(config) { return result; } }, app); - - app.providers.push(function(path, service) { - services[path] = service; - }); }; }; diff --git a/test/providers/service-fixture.js b/test/providers/service-fixture.js index 248dad9cb2..2f9d64c24f 100644 --- a/test/providers/service-fixture.js +++ b/test/providers/service-fixture.js @@ -48,6 +48,22 @@ exports.Service = { callback(null, { id: id }); + }, + + removed: function(data, params, callback) { + if(params.process) { + return callback(null, _.extend({}, data, params)); + } + + callback(null, data); + }, + + created: function(data, params, callback) { + if(params.process) { + return callback(null, _.extend({}, data, params)); + } + + callback(null, data); } }; diff --git a/test/providers/socketio.test.js b/test/providers/socketio.test.js index 4d2fd42e62..7fd74207d7 100644 --- a/test/providers/socketio.test.js +++ b/test/providers/socketio.test.js @@ -153,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(); }); @@ -166,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(); }); @@ -175,7 +175,7 @@ describe('SocketIO provider', function () { }); it('removed', function (done) { - socket.on('todo removed', function (data) { + socket.once('todo removed', function (data) { verify.remove(333, data); done(); }); @@ -183,4 +183,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(); + }); + }); + }); }); From 9f0fdf1ac38d1dde71c43a9fa6f2f3a136316584 Mon Sep 17 00:00:00 2001 From: David Luecke Date: Sat, 5 Apr 2014 20:16:50 -0600 Subject: [PATCH 07/11] Documentation for service event dispatching and some cleanup. --- lib/mixins/event.js | 3 +- readme.md | 92 ++++++++++++++++++++++++++++++- test/providers/service-fixture.js | 16 ------ test/providers/socketio.test.js | 13 +++++ 4 files changed, 105 insertions(+), 19 deletions(-) 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/readme.md b/readme.md index 567f7b4349..c7f8720eba 100644 --- a/readme.md +++ b/readme.md @@ -239,6 +239,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) {} } @@ -323,6 +324,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. @@ -437,9 +457,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/', { @@ -502,6 +522,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. diff --git a/test/providers/service-fixture.js b/test/providers/service-fixture.js index 2f9d64c24f..248dad9cb2 100644 --- a/test/providers/service-fixture.js +++ b/test/providers/service-fixture.js @@ -48,22 +48,6 @@ exports.Service = { callback(null, { id: id }); - }, - - removed: function(data, params, callback) { - if(params.process) { - return callback(null, _.extend({}, data, params)); - } - - callback(null, data); - }, - - created: function(data, params, callback) { - if(params.process) { - return callback(null, _.extend({}, data, params)); - } - - callback(null, data); } }; diff --git a/test/providers/socketio.test.js b/test/providers/socketio.test.js index 7fd74207d7..f0af8bfc70 100644 --- a/test/providers/socketio.test.js +++ b/test/providers/socketio.test.js @@ -174,6 +174,19 @@ 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.once('todo removed', function (data) { verify.remove(333, data); From 5d35ef18c245dc405fa7662b463e64b7fdf75846 Mon Sep 17 00:00:00 2001 From: David Luecke Date: Sun, 6 Apr 2014 10:54:44 -0600 Subject: [PATCH 08/11] Implementing event filtering and default params for Primus. Extracted common Socket functionality. --- lib/providers/index.js | 4 +- lib/providers/socket/commons.js | 50 ++++++++++ lib/providers/{ => socket}/primus.js | 47 ++++----- lib/providers/socket/socketio.js | 63 ++++++++++++ lib/providers/socketio.js | 100 ------------------- readme.md | 17 ++++ test/providers/primus.test.js | 143 +++++++++++++++++++++++++-- 7 files changed, 292 insertions(+), 132 deletions(-) create mode 100644 lib/providers/socket/commons.js rename lib/providers/{ => socket}/primus.js (55%) create mode 100644 lib/providers/socket/socketio.js delete mode 100644 lib/providers/socketio.js 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/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 6197326bb7..0000000000 --- a/lib/providers/socketio.js +++ /dev/null @@ -1,100 +0,0 @@ -'use strict'; - -var _ = require('lodash'); -var socketio = require('socket.io'); -var Proto = require('uberproto'); - -// 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. -function setupMethodHandler (socket, service, path, method) { - var name = path + '::' + method; - var position = typeof paramsPositions[method] !== 'undefined' ? paramsPositions[method] : 1; - - if (typeof service[method] === 'function') { - socket.on(name, function () { - var args = _.toArray(arguments); - args[position] = _.extend({}, args[position], socket.handshake.feathers); - 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. -function setupEventHandler (sockets, 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; - - sockets.clients().forEach(function (socket) { - dispatcher(data, socket.handshake.feathers, function (error, dispatchData) { - if (error) { - socket.emit('error', error); - } else if (dispatchData) { - socket.emit(eventName, dispatchData); - } - }); - }); - }); -} - -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); - - // 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) { - setupMethodHandler(socket, 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) { - setupEventHandler(io.sockets, service, path, ev); - }); - } - }); - - if (typeof config === 'function') { - config.call(this, io); - } - - return result; - } - }, app); - }; -}; diff --git a/readme.md b/readme.md index c7f8720eba..d179701d28 100644 --- a/readme.md +++ b/readme.md @@ -135,6 +135,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 diff --git a/test/providers/primus.test.js b/test/providers/primus.test.js index d8603695bd..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) { @@ -92,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(); }); @@ -105,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(); }); @@ -113,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(); }); @@ -122,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(); + }); + }); + }); }); From eda0e251d593ac3b7e1928d1b2021034fe76a56f Mon Sep 17 00:00:00 2001 From: David Luecke Date: Tue, 8 Apr 2014 08:51:21 -0600 Subject: [PATCH 09/11] Allow to configure REST handler manually (#40) --- lib/feathers.js | 8 +++-- lib/providers/rest/index.js | 13 +++++--- readme.md | 26 +++++++++++++++ test/providers/rest.test.js | 64 +++++++++++++++++++++++++++++++++---- 4 files changed, 98 insertions(+), 13 deletions(-) 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/providers/rest/index.js b/lib/providers/rest/index.js index 12b13b07c4..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; @@ -47,7 +50,7 @@ module.exports = function (config) { // 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/readme.md b/readme.md index d179701d28..33d005b605 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): diff --git a/test/providers/rest.test.js b/test/providers/rest.test.js index d3e9c074ce..507153b9c9 100644 --- a/test/providers/rest.test.js +++ b/test/providers/rest.test.js @@ -141,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 }); }); @@ -168,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); + }); + }); }); From 53c961b2634c15ddcb50b927088289094e4a9ba1 Mon Sep 17 00:00:00 2001 From: David Luecke Date: Tue, 8 Apr 2014 16:31:28 -0600 Subject: [PATCH 10/11] Updating readme with 0.4.0 changelog. --- readme.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/readme.md b/readme.md index 33d005b605..1abd497e54 100644 --- a/readme.md +++ b/readme.md @@ -643,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)) From 0a3e94b43614f9bd9f2bf7a149f092a841ae9f81 Mon Sep 17 00:00:00 2001 From: David Luecke Date: Tue, 8 Apr 2014 16:32:55 -0600 Subject: [PATCH 11/11] release 0.4.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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",