curl 'http://localhost:3000/todos/' -H 'Content-Type: application/json' --data-binary '{ "text": "You have to do dishes!" }'
-
-## Getting real-time
-
-As previously mentioned, a Feathers service can also be exposed through websockets. You can either use [SocketIO](http://socket.io) or [Primus](https://github.com/primus/primus) - an abstraction layer for differentNode websocket libraries. In the following examples we will use SocketIO.
-
-SocketIO can be enabled by calling `app.configure(feathers.socketio())`. Once set up, it is possible to call service methods by emitting events like `curl 'http://localhost:3000/todos/' -H 'Content-Type: application/json' --data-binary '{ "text": "Do something" }'
-
-will also log `Someone created a new Todo`. This is how you can implement real-time functionality in any web page by using standardized websocket messages instead of having to make up your own.
-
-## Persisting to MongoDB
-
-Our CRUD Todo functionality implemented in the service is very common and doesn't have to be re-done from scratch every time. In fact, this is almost exactly what is being provided already in the [feathers-memory](https://github.com/feathersjs/feathers-memory) module. Luckily we don't have to stop at storing everything in-memory. For the popular NoSQL database [MongoDB](http://mongodb.org) , for example, there already is the [feathers-mongodb](https://github.com/feathersjs/feathers-mongodb) module and if you need more ORM-like functionality through [Mongoose](http://mongoosejs.com/) you can also use [feathers-mongoose](https://github.com/feathersjs/feathers-mongoose).
-
-> `npm install feathers-mongodb`
-
-With a MongoDB instance running locally, we can replace our `todoService` in `app.js` with a MongoDB storage on the `feathers-demo` database and the `todos` collection like this:
-
-```js
-// app.js
-var feathers = require('feathers');
-var mongodb = require('feathers-mongodb');
-var bodyParser = require('body-parser');
-
-var app = feathers();
-var todoService = mongodb({
- db: 'feathers-demo',
- collection: 'todos'
-});
-
-app.configure(feathers.rest())
- .configure(feathers.socketio())
- .use(bodyParser.json())
- .use('/todos', todoService)
- .use('/', feathers.static(__dirname))
- .listen(3000);
-```
-
-And just like this we have a full REST and real-time Todo API that stores its data into MongoDB in just 16 lines of code! We will continue using MongoDB so we don't need our example `todos.js` service anymore.
-
-## Validation and processing
-
-The next step is validating and processing our data. With the MongoDB service already implemented we have two options to extend its functionality.
-
-### Service Extension
-
-*feathers-mongodb* uses the ES5 inheritance library [Uberproto](https://github.com/daffl/uberproto). This allows us to `extend` the original object returned by the call to `mongodb(options)` and overwrite the existing implementation of `create` to process the Todo data and then pass it to the original (`_super`) method. This way we can also easily add our own methods to the service.
-
-```js
-var todoService = mongodb({
- db: 'feathers-demo',
- collection: 'todos'
-}).extend({
- create: function(data, params, callback) {
- // We want to make sure that `complete` is always set
- // and also only use the `text` and `complete` properties
- var newData = {
- text: data.text,
- complete: data.complete === 'true' || !!data.complete
- };
- // Call the original method with the new data
- this._super(newData, params, callback);
- },
-
- // Add another method
- addDefaultTodo: function(callback) {
- this.create({
- text: 'The default todo',
- complete: false
- }, {}, callback);
- }
-});
-```
-
-### Hooks
-
-Another option is the [feathers-hooks](https://github.com/feathersjs/feathers-hooks) plugin which allows us to add asynchronous hooks before or after a service method call. Hooks work similar to Express middleware. The following example adds a hook that converts our Todo data and makes sure that nobody submits anything that we don't want to put into MongoDB:
-
-> `npm install feathers-hooks`
-
-```js
-// app.js
-var feathers = require('feathers');
-var mongodb = require('feathers-mongodb');
-var hooks = require('feathers-hooks');
-var bodyParser = require('body-parser');
-
-var app = feathers();
-var todoService = mongodb({
- db: 'feathers-demo',
- collection: 'todos'
-});
-
-app.configure(feathers.rest())
- .configure(feathers.socketio())
- // Configure hooks
- .configure(hooks())
- .use(bodyParser.json())
- .use('/todos', todoService)
- .use('/', feathers.static(__dirname))
- .listen(3000);
-
-// Get the wrapped todos service object and
-// add a `before` create hook modifying the data
-app.service('todos').before({
- create: function(hook, next) {
- var oldData = hook.data;
- // Replace the old data by creating a new object
- hook.data = {
- text: oldData.text,
- complete: oldData.complete === 'true' || !!oldData.complete
- };
- next();
- }
-});
-```
-
-You might have noticed the call to [.service](/api/#toc9) in `app.service('todos')`. This will basically return the original service object (`todoService` in our case) *but* contain some functionality added by Feathers. Most notably, the returned service object will be an [EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter) that emits `created`, `updated` etc. events.
-
-The *feathers-hooks* plugin also adds a `.before` and `.after` method that allows to add hooks to that service. When you need to access services, *always* use `app.service(name)` and not the original service object otherwise things will not work as expected.
-
-## Authentication
-
-Since Feathers directly extends Express you can use any of its authentication mechanism. [Passport](http://passportjs.org/) is one that is used quite often and also really flexible. Manually setting up shared authentication between websockets and an HTTP REST API can be tricky. This is what the [feathers-passport](https://github.com/feathersjs/feathers-passport) module aims to make easier. The following examples show how to add local authentication that uses a Feathers service for storing and retrieving user information.
-
-### Configuring Passport
-
-The first step is to add the Passport, local strategy and feathers-passport modules to our application. Since we are using MongoDB already we will also use it as the session store through the [connect-mongo](https://github.com/kcbanner/connect-mongo) module:
-
-> `npm install passport passport-local connect-mongo feathers-passport`
-
-```js
-// app.js
-var feathers = require('feathers');
-var mongodb = require('feathers-mongodb');
-var bodyParser = require('body-parser');
-var hooks = require('feathers-hooks');
-
-var passport = require('passport');
-var connectMongo = require('connect-mongo');
-var feathersPassport = require('feathers-passport');
-
-var app = feathers();
-var todoService = mongodb({
- db: 'feathers-demo',
- collection: 'todos'
-});
-
-app.configure(feathers.rest())
- .configure(feathers.socketio())
- .configure(hooks())
- .configure(feathersPassport(function(result) {
- // MongoStore needs the session function
- var MongoStore = connectMongo(result.createSession);
-
- result.secret = 'feathers-rocks';
- result.store = new MongoStore({
- db: 'feathers-demo'
- });
-
- return result;
- }))
- .use(bodyParser.json())
- // Now we also need to parse HTML form submissions
- .use(bodyParser.urlencoded({ extended: true }))
- .use('/todos', todoService)
- .use('/', feathers.static(__dirname));
-```
-
-### User storage
-
-Next, we create a MongoDB service for storing user information. It is always a good idea to not store plain text passwords in the database so we add a `.before` hook that salts and then hashes the password when creating a new user. This can be done in the service `.setup` which is called when the application is ready to start up. We also add an `.authenticate` method that we can use to look up a user by username and compare the hashed and salted passwords.
-
-```js
-var crypto = require('crypto');
-// One-way hashes a string
-var hash = function(string, salt) {
- var shasum = crypto.createHash('sha256');
- shasum.update(string + salt);
- return shasum.digest('hex');
-};
-
-var userService = mongodb({
- db: 'feathers-demo',
- collection: 'users'
-}).extend({
- authenticate: function(username, password, callback) {
- // This will be used as the MongoDB query
- var query = {
- username: username
- };
-
- this.find({ query: query }, function(error, users) {
- if(error) {
- return callback(error);
- }
-
- var user = users[0];
-
- if(!user) {
- return callback(new Error('No user found'));
- }
-
- // Compare the hashed and salted passwords
- if(user.password !== hash(password, user.salt)) {
- return callback(new Error('User password does not match'));
- }
-
- // If we got to here, we call the callback with the user information
- return callback(null, user);
- });
- },
-
- setup: function() {
- // Adds the hook during service setup
- this.before({
- // Hash the password before sending it to MongoDB
- create: function(hook, next) {
- // Create a random salt string
- var salt = crypto.randomBytes(128).toString('base64');
- // Change the password to a hashed and salted password
- hook.data.password = hash(hook.data.password, salt);
- // Add the salt to the user data
- hook.data.salt = salt;
- next();
- }
- });
- }
-});
-
-app.use('/users', userService);
-```
-
-Now we need to set up Passport to use that service and tell it how to deserialize and serialize our user information. For us, the serialized form is the `_id` generated by MongoDB. To deserialize by `_id` we can simply call the user services `.get` method. Then we add the local strategy which simply calls the `.authenticate` method that we implemented in the user service.
-
-```js
-var LocalStrategy = require('passport-local').Strategy;
-
-passport.serializeUser(function(user, done) {
- // Use the `_id` property to serialize the user
- done(null, user._id);
-});
-
-passport.deserializeUser(function(id, done) {
- // Get the user information from the service
- app.service('users').get(id, {}, done);
-});
-
-passport.use(new LocalStrategy(function(username, password, done) {
- app.service('users').authenticate(username, password, done);
-}));
-```
-
-### Login
-
-The last step is to add the authentication route that we can POST the login to:
-
-```js
-app.post('/login', passport.authenticate('local', {
- successRedirect: '/',
- failureRedirect: '/login.html',
- failureFlash: false
-}));
-
-app.listen(3000);
-```
-
-And to add a `login.html` page:
-
-```html
-
-
-
-
- curl 'http://localhost:3000/users/' -H 'Content-Type: application/json' --data-binary '{ "username": "feathers", "password": "supersecret" }'
-
-Now it should be possible to log in with the `feathers` username and `supersecret` password and you will get the logged in user information in every service call in `params.user`.
-
-## Authorization
-
-Authorization is the process of determining after successful authentication if the user is allowed to perform the requested action. This is again where hooks come in handy.
-
-### User authorization
-
-Since *feathers-passport* adds the authenticated user information to the service call parameters we can just check those in the hook and return with an error if the user is not authorized:
-
-```js
-app.service('todos').before({
- create: function(hook, next) {
- // We only allow creating todos with an authenticated user
- if(!hook.params.user) {
- return next(new Error('You need to be authenticated'));
- }
-
- // Check if the user belongs the `admin` group
- var groups = hook.params.user.groups;
- if(groups.indexOf('admin') === -1) {
- // Return with an error if not
- return next(new Error('User is not allowed to create a new Todo'));
- }
-
- // Otherwise just continue on to the
- // next hook or the service method
- next();
- }
-});
-```
-
-### Event filtering
-
-This is also a good time to talk a little about [filtering events](/api/#event-filtering). It is very likely that you eventually only want to send certain events to specific users instead of everybody. Following up on the group authorization example from above, we might only want to dispatch a `todos created` event to users that are in the admin group. This can be done by adding a `created(data, params, callback)` method to the Todo MongoDB service:
-
-```js
-var todoService = mongodb({
- db: 'feathers-demo',
- collection: 'todos'
-}).extend({
- created: function(data, params, callback) {
- // Only dispatch if we have a user and user belongs to the admin group
- if(params.user && params.user.groups.indexOf('admin') !== -1) {
- // Call back with the data we want to dispatch
- return callback(null, data);
- }
-
- // Call back with falsy value to not dispatch the event
- callback(null, false);
- }
-});
-```
-
-The `created` method is being called for every connected user with the `params` set in the `request.feathers` object and the data from the event. You can either call back with the original or modified data (which will then be dispatched to that user) or a falsy value which will prevent the event from being dispatched to that connection.
-
-## What's next?
-
-This guide hopefully gave you an overview of how Feathers works. We created a Todo service and made it available through a REST API and SocketIO. Then we moved to using MongoDB as the backend storage and learned how to process and validate our data. After that we added PassportJS authentication for both, the REST API and websockets and then briefly discussed how you might authorize that authenticated user and make sure that websocket events only get dispatched to where we want them to.
-
-The next step is definitely reading through the [API documentation](/api/) for a more detailed information on how to configure and use certain parts of Feathers. The [FAQ](/faq/) also has some answers to questions that come up regularly. For a growing list of official plugins, have a look at the [Feathersjs GitHub organization](https://github.com/feathersjs).
-
-If you have any other questions, feel free to submit them as a [GitHub issue](https://github.com/feathersjs/feathers/issues) or on [Stackoverflow](http://stackoverflow.com) using the `feathers` or `feathersjs` tag or join [#feathersjs](http://webchat.freenode.net/?channels=feathersjs) on Freenode IRC.
diff --git a/lib/mixins/event.js b/lib/mixins/event.js
index c8febff67d..eb25c391ee 100644
--- a/lib/mixins/event.js
+++ b/lib/mixins/event.js
@@ -29,7 +29,9 @@ var EventMixin = {
});
_.each(eventMappings, function (event, method) {
- if (typeof self[method] === 'function') {
+ var alreadyEmits = self._serviceEvents.indexOf(event) !== -1;
+
+ if (typeof self[method] === 'function' && !alreadyEmits) {
// The Rubberduck event name (e.g. afterCreate, afterUpdate or afterDestroy)
var eventName = 'after' + method.charAt(0).toUpperCase() + method.substring(1);
self._serviceEvents.push(event);
@@ -50,10 +52,15 @@ var EventMixin = {
}
};
-_.extend(EventMixin, EventEmitter.prototype);
-
module.exports = function (service) {
+ var isEmitter = typeof service.on === 'function' &&
+ typeof service.emit === 'function';
+
if (typeof service.mixin === 'function') {
+ if(!isEmitter) {
+ service.mixin(EventEmitter.prototype);
+ }
+
service.mixin(EventMixin);
}
};
diff --git a/package.json b/package.json
index d4906652bc..5cc64ecdd6 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "feathers",
- "description": "Shared REST and real-time APIs with Express.",
- "version": "1.1.0-pre.0",
+ "description": "Build Better APIs, Faster than Ever.",
+ "version": "1.1.0",
"homepage": "http://feathersjs.com",
"repository": {
"type": "git",
@@ -40,7 +40,6 @@
"npm": ">= 1.3.0"
},
"dependencies": {
- "body-parser": "^1.0.2",
"debug": "^2.1.1",
"express": "^4.12.3",
"feathers-commons": "^0.2.0",
@@ -53,6 +52,8 @@
"uberproto": "^1.1.0"
},
"devDependencies": {
+ "body-parser": "^1.13.2",
+ "feathers-client": "^0.1.1",
"jshint": "^2.6.3",
"mocha": "^2.2.0",
"q": "^1.0.1",
diff --git a/readme.md b/readme.md
index ac92e6e9f7..e7a969ded4 100644
--- a/readme.md
+++ b/readme.md
@@ -1,269 +1,21 @@
-# Feathers - Let your applications fly!
+
+ curl 'http://localhost:3000/todos/' -H 'Content-Type: application/json' --data-binary '{ "text": "You have to do dishes!" }'
-
-## Getting real-time
-
-As previously mentioned, a Feathers service can also be exposed through websockets. You can either use [SocketIO](http://socket.io) or [Primus](https://github.com/primus/primus) - an abstraction layer for differentNode websocket libraries. In the following examples we will use SocketIO.
-
-SocketIO can be enabled by calling `app.configure(feathers.socketio())`. Once set up, it is possible to call service methods by emitting events like `curl 'http://localhost:3000/todos/' -H 'Content-Type: application/json' --data-binary '{ "text": "Do something" }'
-
-will also log `Someone created a new Todo`. This is how you can implement real-time functionality in any web page without a lot of magic using standardized websocket messages instead of having to re-invent your own.
-
-## Persisting to MongoDB
-
-Our CRUD Todo functionality implemented in the service is very common and doesn't have to be implemented form scratch every time. In fact, this is almost exactly what is being provided already in the [feathers-memory](https://github.com/feathersjs/feathers-memory) module. Luckily we don't have to stop at storing everything in-memory. For the popular NoSQL database [MongoDB](http://mongodb.org) , for example, there already is the [feathers-mongodb](https://github.com/feathersjs/feathers-mongodb) module and if you need more ORM-like functionality through [Mongoose](http://mongoosejs.com/) you can also use [feathers-mongoose](https://github.com/feathersjs/feathers-mongoose).
-
-> `npm install feathers-mongodb`
-
-With a MongoDB instance running locally, we can replace our `todoService` in `app.js` with a MongoDB storage on the `feathers-demo` database and the `todos` collection like this:
+Curious how it looks? Here is a full REST and real-time todo API that uses MongoDB:
```js
// app.js
@@ -285,8 +37,11 @@ app.configure(feathers.rest())
.listen(3000);
```
-And just like this we have a full REST and real-time Todo API that stores its data into MongoDB in just 16 lines of code!
+Then run
-## Next steps
+```
+npm install feathers feathers-mongodb body-parser
+node app
+```
-To learn more about Feathers go to the [feathersjs.com](http://feathersjs.com) homepage and continue reading this guide.
+and go to [http://localhost:3000/todos](http://localhost:3000/todos). Don't want to use MongoDB? Feathers has plugins for [many other databases](http://feathersjs.com/learn/) and you can easily [write your own adapters](http://feathersjs.com/quick-start/).
diff --git a/test/distributed.test.js b/test/distributed.test.js
new file mode 100644
index 0000000000..e9e0bce8fd
--- /dev/null
+++ b/test/distributed.test.js
@@ -0,0 +1,41 @@
+var assert = require('assert');
+var client = require('feathers-client');
+var io = require('socket.io-client');
+var feathers = require('../lib/feathers');
+
+describe('Distributed Feathers applications test', function () {
+ before(function(done) {
+ var app = feathers()
+ .configure(feathers.socketio())
+ .use('todos', {
+ create: function(data, params, callback) {
+ data.id = 42;
+ callback(null, data);
+ }
+ });
+
+ app.listen(8888, done);
+ });
+
+ it('passes created event between servers', function (done) {
+ var socket = io('http://localhost:8888');
+ var remoteApp = client().configure(client.socketio(socket));
+ var todo = { text: 'Created on alpha server', complete: false };
+ var beta = feathers()
+ .configure(feathers.rest())
+ .use('todos', remoteApp.service('todos'));
+
+ beta.listen(9999, function() {
+ beta.service('todos').on('created', function(newTodo) {
+ assert.deepEqual(newTodo, {
+ id: 42,
+ text: 'Created on alpha server',
+ complete: false
+ });
+ done();
+ });
+
+ socket.emit('todos::create', todo);
+ });
+ });
+});
diff --git a/test/mixins/event.test.js b/test/mixins/event.test.js
index dbf20dc7a0..bb64a9ba17 100644
--- a/test/mixins/event.test.js
+++ b/test/mixins/event.test.js
@@ -3,6 +3,8 @@
var assert = require('assert');
var _ = require('lodash');
var Proto = require('uberproto');
+var EventEmitter = require('events').EventEmitter;
+
var mixinEvent = require('../../lib/mixins/event');
var EventMixin = mixinEvent.Mixin;
var create = Proto.create;
@@ -23,7 +25,7 @@ describe('Event mixin', function () {
var instance = create.call(FixtureService);
assert.equal('Original setup: Test', instance.setup('Test'));
- assert.ok(instance._rubberDuck instanceof require('events').EventEmitter);
+ assert.ok(instance._rubberDuck instanceof EventEmitter);
var existingMethodsService = {
setup: function (arg) {
@@ -32,6 +34,7 @@ describe('Event mixin', function () {
};
Proto.mixin(EventMixin, existingMethodsService);
+ Proto.mixin(EventEmitter.prototype, existingMethodsService);
assert.equal('Original setup from object: Test', existingMethodsService.setup('Test'));
assert.equal(typeof existingMethodsService.on, 'function');
@@ -149,4 +152,36 @@ describe('Event mixin', function () {
assert.equal(data.id, 27);
});
});
+
+ it('does not punch when service has an events list (#118)', function(done) {
+ var FixtureService = Proto.extend({
+ events: [ 'created' ],
+ create: function (data, params, cb) {
+ _.defer(function () {
+ cb(null, {
+ id: 10,
+ name: data.name
+ });
+ });
+ }
+ });
+
+ FixtureService.mixin(EventEmitter.prototype);
+ mixinEvent(FixtureService);
+
+ var instance = create.call(FixtureService);
+ instance.setup();
+
+ instance.on('created', function (data) {
+ assert.deepEqual(data, { custom: 'event' });
+ done();
+ });
+
+ instance.create({
+ name: 'Tester'
+ }, {}, function (error, data) {
+ assert.equal(data.id, 10);
+ instance.emit('created', { custom: 'event' });
+ });
+ });
});