From 387b2ae9abb7d03445039c6a6021ab973a4e445a Mon Sep 17 00:00:00 2001 From: David Luecke Date: Tue, 7 Jan 2014 15:28:44 -0700 Subject: [PATCH 1/3] Refactoring REST provider to allows external wrapping of service methods as middleware. Throw HTTP error 405 when acessing unavailable service methods. --- documentation.md | 2 +- lib/providers/rest.js | 82 ----------------------- lib/providers/rest/index.js | 51 +++++++++++++++ lib/providers/rest/wrappers.js | 115 +++++++++++++++++++++++++++++++++ package.json | 2 +- test/providers/rest.test.js | 28 ++++++++ 6 files changed, 196 insertions(+), 84 deletions(-) delete mode 100644 lib/providers/rest.js create mode 100644 lib/providers/rest/index.js create mode 100644 lib/providers/rest/wrappers.js diff --git a/documentation.md b/documentation.md index c3b728ca43..33a858e91b 100644 --- a/documentation.md +++ b/documentation.md @@ -107,7 +107,7 @@ In the Browser you can connect like this: console.log('Someone created a Todo', todo); }); - primus.send('todos::create', { description: 'Do something', {}, function() { + primus.send('todos::create', { description: 'Do something' }, {}, function() { primus.send('todos::find', {}, function(error, todos) { console.log(todos); }); diff --git a/lib/providers/rest.js b/lib/providers/rest.js deleted file mode 100644 index f776920b0f..0000000000 --- a/lib/providers/rest.js +++ /dev/null @@ -1,82 +0,0 @@ -'use strict'; - -var _ = require('underscore'); - -var _wrapper = function (req, res, next) { - return function (error, data) { - if (error) { - return next(error); - } - res.data = data; - return next(); - }; -}; -var _getParams = function (req) { - var query = req.query || {}; - return _.extend({ - query: query - }, req.feathers); -}; -var toUri = function (name) { - // TODO - return '/' + name; -}; - -module.exports = function (config) { - config = config || {}; - - return function () { - var app = this; - var responder = config.responder || function (req, res) { - res.format(_.extend({ - 'application/json': function () { - res.json(res.data); - } - }, config.formatters)); - }; - - app.enable('feathers rest'); - - app.use(function (req, res, next) { - req.feathers = {}; - next(); - }); - - // Register the REST provider - app.providers.push(function (path, service) { - if (app.disabled('feathers rest')) { - return; - } - - var uri = toUri(path); - // TODO throw 405 Method Not Allowed with allowed methods - - // GET / -> resource.index(cb, params) - app.get(uri, function (req, res, next) { - service.find(_getParams(req), _wrapper(req, res, next)); - }); - - // GET /:id -> resource.get(cb, id, params) - app.get(uri + '/:id', function (req, res, next) { - service.get(req.params.id, _getParams(req), _wrapper(req, res, next)); - }); - - // POST -> resource.create(cb, data, params) - app.post(uri, function (req, res, next) { - service.create(req.body, _getParams(req), _wrapper(req, res, next)); - }); - - // PUT /:id -> resource.update(cb, id, data, params) - app.put(uri + '/:id', function (req, res, next) { - service.update(req.params.id, req.body, _getParams(req), _wrapper(req, res, next)); - }); - - // DELETE /:id -> resource.destroy(cb, id, params) - app.del(uri + '/:id', function (req, res, next) { - service.remove(req.params.id, _getParams(req), _wrapper(req, res, next)); - }); - - app.use(uri, responder); - }); - }; -}; diff --git a/lib/providers/rest/index.js b/lib/providers/rest/index.js new file mode 100644 index 0000000000..a0042ddbc1 --- /dev/null +++ b/lib/providers/rest/index.js @@ -0,0 +1,51 @@ +'use strict'; + +var _ = require('underscore'); +var wrappers = require('./wrappers'); + +module.exports = function (config) { + config = config || {}; + + var responder = config.responder || function (req, res) { + res.format(_.extend({ + 'application/json': function () { + res.json(res.data); + } + }, config.formatters)); + }; + + return function () { + var app = this; + + app.enable('feathers rest'); + + app.use(function (req, res, next) { + req.feathers = {}; + next(); + }); + + app.rest = wrappers; + + // Register the REST provider + app.providers.push(function (path, service) { + if (app.disabled('feathers rest')) { + return; + } + + var uri = path.indexOf('/') === 0 ? path : '/' + path; + + // GET / -> service.find(cb, params) + app.get(uri, app.rest.find(service)) + // GET /:id -> service.get(cb, id, params) + .get(uri + '/:id', app.rest.get(service)) + // POST -> service.create(cb, data, params) + .post(uri, app.rest.create(service)) + // PUT /:id -> service.update(cb, id, data, params) + .put(uri + '/:id', app.rest.update(service)) + // DELETE /:id -> service.remove(cb, id, params) + .del(uri + '/:id', app.rest.remove(service)); + + app.use(uri, responder); + }); + }; +}; diff --git a/lib/providers/rest/wrappers.js b/lib/providers/rest/wrappers.js new file mode 100644 index 0000000000..d5b7442f3a --- /dev/null +++ b/lib/providers/rest/wrappers.js @@ -0,0 +1,115 @@ +var _ = require('underscore'); + +/** + * Return a service callback that sets the data in the given + * response and calls next on errors. + * + * @param req + * @param res + * @param next + * @returns {Function} + */ +function wrap(req, res, next) { + return function(error, data) { + if (error) { + return next(error); + } + res.data = data; + return next(); + }; +} + +/** + * Returns the service params, setting params.query + * to an empty object (if not set) and grabbing anything set + * in req.feathers. + * + * @param req The request + * @returns {Object} The service parameters + */ +function getParams(req) { + var query = req.query || {}; + return _.extend({ + query: query + }, req.feathers); +} + +/** + * Checks if the service method is available. If not, sets the response HTTP status to + * 405 (Method not allowed) and returns an error. + * + * @param res The HTTP response + * @param service The wrapped service object + * @param name The method name to check for + * @returns {Error|false} `false` or an error object with the description + */ +function checkMethod(res, service, name) { + if (typeof service[name] !== 'function') { + res.status(405); + return new Error('Can not call service method .' + name); + } + + return false; +} + +/** + * Returns wrapped middleware for a service method. + * + * @type {{find: find, get: get, create: create, update: update, remove: remove}} + */ +module.exports = { + find: function(service) { + return function(req, res, next) { + var error = checkMethod(res, service, 'find'); + if (error) { + return next(error); + } + + service.find(getParams(req), wrap(req, res, next)); + }; + }, + + get: function(service) { + return function(req, res, next) { + var error = checkMethod(res, service, 'get'); + if (error) { + return next(error); + } + + service.get(req.params.id, getParams(req), wrap(req, res, next)); + }; + }, + + create: function(service) { + return function(req, res, next) { + var error = checkMethod(res, service, 'create'); + if (error) { + return next(error); + } + + service.create(req.body, getParams(req), wrap(req, res, next)); + }; + }, + + update: function(service) { + return function(req, res, next) { + var error = checkMethod(res, service, 'update'); + if (error) { + return next(error); + } + + service.update(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'); + if (error) { + return next(error); + } + + service.remove(req.params.id, getParams(req), wrap(req, res, next)); + }; + } +}; diff --git a/package.json b/package.json index fa0a0c5132..d54277ed95 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "grunt": "~0.4.1", "grunt-release": "~0.5.1", "mocha": "~1.11.0", - "grunt-contrib-jshint": "~0.6.3", + "grunt-contrib-jshint": "~0.x", "grunt-simple-mocha": "~0.4.0", "grunt-jsbeautifier": "~0.2.2" } diff --git a/test/providers/rest.test.js b/test/providers/rest.test.js index dd92750eee..bcec86d708 100644 --- a/test/providers/rest.test.js +++ b/test/providers/rest.test.js @@ -120,4 +120,32 @@ describe('REST provider', function () { server.close(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 }); + } + }); + + /* jshint ignore:start */ + // Error handler + app.use(function(error, req, res, next) { + assert.equal(error.message, 'Can not call service method .find'); + res.json({ message: error.message }); + }); + /* jshint ignore:end */ + + var server = app.listen(4777); + + request('http://localhost:4777/todo/dishes', function (error, response, body) { + assert.ok(response.statusCode === 200, 'Got OK status code for .get'); + assert.deepEqual(JSON.parse(body), { description: 'You have to do dishes' }, 'Got expected object'); + request('http://localhost:4777/todo', function (error, response, body) { + assert.ok(response.statusCode === 405, 'Got 405 for .find'); + assert.deepEqual(JSON.parse(body), { message: 'Can not call service method .find' }, 'Error serialized as expected'); + server.close(done); + }); + }); + }); }); From db6ac83108d647b5eea91ab2ec072f5132bf6bb3 Mon Sep 17 00:00:00 2001 From: David Luecke Date: Tue, 7 Jan 2014 16:19:56 -0700 Subject: [PATCH 2/3] We only need one documentation and license file. --- documentation.md | 540 ----------------------------------------------- license.md | 20 -- readme.md | 535 +++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 505 insertions(+), 590 deletions(-) delete mode 100644 documentation.md delete mode 100644 license.md diff --git a/documentation.md b/documentation.md deleted file mode 100644 index c3b728ca43..0000000000 --- a/documentation.md +++ /dev/null @@ -1,540 +0,0 @@ -## Introduction - -Feathers sits right on top of Express, one of the most popular web frameworks for [NodeJS](http://nodejs.org/). If you are not familiar with Express head over to the [Express Guides](http://expressjs.com/guide.html) to get an idea. Feathers works the exact same way except that `var app = require('express')();` is replaced with `var app = require('feathers')()`. The small differences and additional functionality available is outline in the following documentation. - -## Configuration - -### REST - -Exposing services through a RESTful JSON interface is enabled by default. If you only want to use SocketIO call `app.disabled('feathers rest')` _before_ registering any services. - -To set service parameters in a middleware, just attach it to the `req.feathers` object which will become the params for any resulting service call: - -```js -app.use(function(req, res) { - req.feathers.data = 'Hello world'; -}); - -app.use('/todos', { - get: function(name, params, callback) { - console.log(params.data); // -> 'Hello world' - callback(null, { - id: name, - params: params, - description: "You have to do " + name + "!" - }); - } -}); -``` - -### 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): - -```js -app.configure(feathers.socketio(function(io) { - io.enable('browser client minification'); // send minified client - io.enable('browser client etag'); // apply etag caching logic based on version number - io.enable('browser client gzip'); // gzip the file - io.set('log level', 1); // reduce logging - - // enable all transports (optional if you want flashsocket support, please note that some hosting - // providers do not allow you to create servers that listen on a port different than 80 or their - // default port) - io.set('transports', [ - 'websocket' - , 'flashsocket' - , 'htmlfile' - , 'xhr-polling' - , 'jsonp-polling' - ]); -})); -``` - -This is also the place to listen to custom events or add [authorization](https://github.com/LearnBoost/socket.io/wiki/Authorizing): - -```js -app.configure(feathers.socketio(function(io) { - io.on('connection', function(socket) { - socket.emit('news', { hello: 'world' }); - socket.on('my other event', function (data) { - console.log(data); - }); - }); - - io.set('authorization', function (handshakeData, callback) { - // Authorize using the /users service - app.lookup('users').find({ - username: handshakeData.username, - password: handshakeData.password - }, callback); - }); -})); -``` - -Once the server has been started with `app.listen()` the SocketIO object is available as `app.io`. - -### Primus - -[Primus](https://github.com/primus/primus) is a universal wrapper for real-time frameworks and allows you to transparently use Engine.IO, WebSockets, BrowserChannel, SockJS and Socket.IO. Set it up with `feathers.primus(configuration [, fn])` where `configuration` is the [Primus server configuration](https://github.com/primus/primus#getting-started) and `fn` an optional callback with the Primus server instance that can e.g. be used for setting up [authorization](https://github.com/primus/primus#authorization): - -```js -// Set up Primus with SockJS -app.configure(feathers.primus({ - transformer: 'sockjs' -}, function(primus) { - // Set up Primus authorization here - primus.authorize(function (req, done) { - var auth; - - try { auth = authParser(req.headers['authorization']) } - catch (ex) { return done(ex) } - - // Do some async auth check - authCheck(auth, done); - }); -})); -``` - -In the Browser you can connect like this: - -```html - - -``` - -## API - -### listen - -`app.listen([port])` starts the application on the given port. It will first call the original [Express app.listen([port])](http://expressjs.com/api.html#app.listen), then run `app.setup(server)` (see below) with the server object and then return the server object. - -### setup - -`app.setup(server)` is used initialize all services by calling each services `.setup(app, path)` method (if available). -It will also use the `server` instance passed (e.g. through `http.createServer`) to set up SocketIO (if enabled) and any other provider that might require the server instance. - -Normally `app.setup` will be called automatically when starting the application via `app.listen([port])` but there are cases when you need to initialize the server separately: - -__HTTPS__ - -With your Feathers application initialized it is easy to set up an HTTPS REST and SocketIO server: - -```js -app.configure(feathers.socketio()).use('/todos', todoService); - -var https = require('https'); -var server = https.createServer({ - key: fs.readFileSync('privatekey.pem'), - cert: fs.readFileSync('certificate.pem') -}, app).listen(443); - -// Call app.setup to initialize all services and SocketIO -app.setup(server); -``` - -__Virtual Hosts__ - -You can use `feathers.vhost` (which is the same as [Express and Connect .vhost](http://www.senchalabs.org/connect/vhost.html)) to run your Feathers app on a virtual host: - -```js -app.use('/todos', todoService); - -var host = feathers().use(feathers.vhost('foo.com', app)); -var server = host.listen(8080); - -// Here we need to call app.setup because .listen on our virtal hosted -// app is never called -app.setup(server); -``` - -### lookup - -`app.lookup(path)` returns the wrapped service object for the given path. Note that Feathers internally creates a new object from each registered service. This means that the object returned by `lookup` will provide the same methods and functionality as the original service but also functionality added by Feathers (most notably it is possible to listen to service events). `path` can be the service name with or without leading and trailing slashes. - -```js -app.use('/my/todos', { - create: function(data, params, callback) { - callback(null, data); - } -}); - -var todoService = app.lookup('my/todos'); -// todoService is an event emitter -todoService.on('created', function(todo) { - console.log('Created todo', todo); -}); -``` - -### use - -`app.use([path], service)` works just like [Express app.use([path], middleware)](http://expressjs.com/api.html#app.use) but additionally allows to register a service object (an object which at least provides one of the service methods as outlined in the Services section) instead of the middleware function. Note that REST services are registered in the same order as any other middleware so the below example will allow the `/todos` service only to [Passport](http://passportjs.org/) authenticated users. - -```js -// Serve public folder for everybody -app.use(feathers.static(__dirname + '/public'); -// Make sure that everything else only works with authentication -app.use(function(req,res,next){ - if(req.isAuthenticated()){ - next(); - } else { - // 401 Not Authorized - next(new Error(401)); - } -}); -// Add a service. -app.use('/todos', { - get: function(name, params, callback) { - callback(null, { - id: name, - description: "You have to do " + name + "!" - }); - } -}); -``` - -### service - -`app.service([path], service)` is what is called internally by `app.use([path], service)` if a service object is being passed. Use it instead of `app.use([path], service)` if you want to be more explicit that you are registering a service. `app.service` does __not__ provide the Express `app.use` functionality and doesn't check the service object for valid methods. - -## Services - -A service can be any JavaScript object that offers one or more of the `find`, `get`, `create`, `update`, `remove` and `setup` service methods with the following signatures: - -```js -var myService = { - find: function(params, callback) {}, - get: function(id, params, callback) {}, - create: function(data, params, callback) {}, - update: function(id, data, params, callback) {}, - remove: function(id, params, callback) {}, - setup: function(app) {} -} -``` - -All callbacks follow the `function(error, data)` NodeJS convention. `params` can contain any additional parameters, for example the currently authenticated user. REST service calls set `params.query` with the query parameters (e.g. a query string like `?status=active&type=user` becomes `{ query: { status: "active", type: "user" } }`). - -### find - -`find(params, callback)` retrieves a list of all resources from the service. Ideally use `params.query` for things like filtering and paging so that REST calls like `todo?status=completed&user=10` work right out of the box. - -__REST__ - - GET todo?status=completed&user=10 - -__SocketIO__ - -```js -socket.emit('todo::find', { - query: { - status: 'completed' - user: 10 - } -}, function(error, data) { -}); -``` - -### get - -`get(id, params, callback)` retrieves a single resource with the given `id` from the service. - -__REST__ - - GET todo/1 - -__SocketIO__ - -```js -socket.emit('todo::get', 1, {}, function(error, data) { - -}); -``` - -### create - -`create(data, params, callback)` creates a new resource with `data`. The callback should be called with the newly -created resource data. - -__REST__ - - POST todo - { "description": "I really have to iron" } - -By default the body can be eihter JSON or form encoded as long as the content type is set accordingly. - -__SocketIO__ - -```js -socket.emit('todo::create', { - description: 'I really have to iron' -}, {}, function(error, data) { -}); -``` - -### update - -`update(id, data, params, callback)` updates the resource identified by `id` using `data`. The callback should -be called with the updated resource data. - -__REST__ - - PUT todo/2 - { "description": "I really have to do laundry" } - -__SocketIO__ - -```js -socket.emit('todo::update', 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. - -__REST__ - - DELETE todo/2 - -__SocketIO__ - -```js -socket.emit('todo::remove', 2, {}, function(error, data) { -}); -``` - -### setup - -`setup(app, path)` initializes the service passing an instance of the Feathers application and the path it has been registered on. The SocketIO server is available via `app.io`. `setup` is a great way to connect services: - -```js -var todoService = { - get: function(name, params, callback) { - callback(null, { - id: name, - description: 'You have to ' + name + '!' - }); - } -}; - -var myService = { - setup: function(app) { - this.todo = app.lookup('todo'); - }, - - get: function(name, params, callback) { - this.todo.get('take out trash', {}, function(error, todo) { - callback(error, { - name: name, - todo: todo - }); - }); - } -} - -feathers() - .use('todo', todoService) - .use('my', myService) - .listen(8000); -``` - -You can see the combination when going to `http://localhost:8000/my/test`. - -__Pro tip:__ - -Bind the apps `lookup` method to your service to always look your services up dynamically: - -``` -var myService = { - setup: function(app) { - this.lookup = app.lookup.bind(app); - }, - - get: function(name, params, callback) { - this.lookup('todos').get('take out trash', {}, function(error, todo) { - callback(null, { - name: name, - todo: todo - }); - }); - } -} -``` - -## Events - -Any registered service will be automatically turned into an event emitter that emits events when a resource has changed, that is a `create`, `update` or `remove` service call returned successfully. It is therefore possible to bind to the below events via `app.lookup(servicename).on()` and, if enabled, all events will also broadcast to all connected SocketIO clients in the form of ` `. Note that the service path will always be stripped of leading and trailing slashes regardless of how it has been registered (e.g. `/my/service/` will become `my/service`). - -### created - -The `created` event will be published with the callback data when a service `create` calls back successfully. - -```js -app.use('/todos', { - create: function(data, params, callback) { - callback(null, data); - } -}); - -app.lookup('/todos').on('created', function(todo) { - console.log('Created todo', todo); -}); - -app.lookup('/todos').create({ - description: 'We have to do something!' -}, {}, function(error, callback) { - // ... -}); - -app.listen(8000); -``` - -__SocketIO__ - -```html - - -``` - -### updated - -The `updated` event will be published with the callback data when a service `update` calls back successfully. - -```js -app.use('/my/todos/', { - update: function(id, data, params, callback) { - callback(null, data); - } -}); - -app.listen(8000); -``` - -__SocketIO__ - -```html - - -``` - -### removed - -The `removed` event will be published with the callback data when a service `remove` calls back successfully. - -```js -app.use('/todos', { - remove: function(id, params, callback) { - callback(null, { id: id }); - } -}); - -app.lookup('/todos').remove(1, {}, function(error, callback) { - // ... -}); - -app.listen(8000); -``` - -__SocketIO__ - -```html - - -``` - -## 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. - -We also think that your data resources can and should be encapsulated in such a way that they can be scalable, easily testable and self contained. The classic web MVC pattern used to work well but is becoming antiquated in today's web. - -With that being said there are some amazing frameworks already out there and we wanted to leverage the ideas that have been put into them, which is why Feathers is built on top of [Express](http://expressjs.com) and is inspired in part by [Sails](http://sailsjs.org), [Flatiron](http://flatironjs.org) and [Derby](http://derbyjs.com). - -## Changelog - -__0.3.0__ - -- Added [Primus](https://github.com/primus/primus) provider ([#34](https://github.com/feathersjs/feathers/pull/34)) -- `app.setup(server)` to support HTTPS (and other functionality that requires a custom server) ([#33](https://github.com/feathersjs/feathers/pull/33)) -- Removed bad SocketIO configuration ([#19](https://github.com/feathersjs/feathers/issues/19)) -- Add .npmignore to not publish .idea folder ([#30](https://github.com/feathersjs/feathers/issues/30)) -- Remove middleware: connect.bodyParser() ([#27](https://github.com/feathersjs/feathers/pull/27)) - -__0.2.0__ - -- Pre-initialize `req.feathers` in REST provider to set service parameters -- Allowing to initialize services with or without slashes to be more express-compatible - -__0.1.0__ - -- First beta release -- Directly extends Express -- Removed built in services and moved to [Legs](https://github.com/feathersjs/legs) -- Created [example repository](https://github.com/feathersjs/examples) - -__0.0.x__ - -- Initial test alpha releases - -## License - -Copyright (C) 2013 David Luecke daff@neyeon.com -Copyright (C) 2013 Eric Kryski e.kryski@gmail.com - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/license.md b/license.md deleted file mode 100644 index 1c3ec09843..0000000000 --- a/license.md +++ /dev/null @@ -1,20 +0,0 @@ -Copyright (C) 2013 David Luecke daff@neyeon.com -Copyright (C) 2013 Eric Kryski e.kryski@gmail.com - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. \ No newline at end of file diff --git a/readme.md b/readme.md index 14dfcd80a5..9053ada862 100644 --- a/readme.md +++ b/readme.md @@ -1,72 +1,547 @@ -# Feathers +## Introduction -> Let your web app fly. +Feathers is a light weight web application framework that rides on top of [Express](http://expressjs.com), one of the most popular web frameworks for [NodeJS](http://nodejs.org/). It makes it easy to create RESTful web services and real-time applications using SocketIO and several other NodeJS real-time libraries. -[![Build Status](https://travis-ci.org/feathersjs/feathers.png)](https://travis-ci.org/feathersjs/feathers) +If you are not familiar with Express head over to the [Express Guides](http://expressjs.com/guide.html) to get an idea. Feathers works the exact same way except that `var app = require('express')();` is replaced with `var app = require('feathers')()`. The most important concept that Feathers adds to Express middleware is data oriented **Services**. How services work and the API additional to the available [Express API](http://expressjs.com/api.html) is outlined in the following documentation. -Feathers is a light weight web application framework that rides on top of [Express](http://expressjs.com). It makes it easy to create RESTful web services and real-time applications using [socket.io](http://socket.io). +## Configuration -The core focus of Feathers is **your data**. We believe that ultimately your app's purpose is to manage data in some fashion and so that's all you should really need to deal with. Managing your data. +### REST + +Exposing services through a RESTful JSON interface is enabled by default. If you only want to use SocketIO call `app.disabled('feathers rest')` _before_ registering any services. -## Install +To set service parameters in a middleware, just attach it to the `req.feathers` object which will become the params for any resulting service call: -As with any NodeJS module, just install it as a dependency in your application: +```js +app.use(function(req, res) { + req.feathers.data = 'Hello world'; +}); -> npm install feathers --save +app.use('/todos', { + get: function(name, params, callback) { + console.log(params.data); // -> 'Hello world' + callback(null, { + id: name, + params: params, + description: "You have to do " + name + "!" + }); + } +}); +``` -## Getting Started Is Easy +### SocketIO -Building an app with Feathers is easy. There are only 4 things to worry about. A wrapped express server, providers, services & middleware. Services are just simple modules that expose certain methods to the providers in order to CRUD your data. We can easily initialize a service that say... provides a single Todo: +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): ```js -var feathers = require('feathers'); +app.configure(feathers.socketio(function(io) { + io.enable('browser client minification'); // send minified client + io.enable('browser client etag'); // apply etag caching logic based on version number + io.enable('browser client gzip'); // gzip the file + io.set('log level', 1); // reduce logging -var todoService = { + // enable all transports (optional if you want flashsocket support, please note that some hosting + // providers do not allow you to create servers that listen on a port different than 80 or their + // default port) + io.set('transports', [ + 'websocket' + , 'flashsocket' + , 'htmlfile' + , 'xhr-polling' + , 'jsonp-polling' + ]); +})); +``` + +This is also the place to listen to custom events or add [authorization](https://github.com/LearnBoost/socket.io/wiki/Authorizing): + +```js +app.configure(feathers.socketio(function(io) { + io.on('connection', function(socket) { + socket.emit('news', { hello: 'world' }); + socket.on('my other event', function (data) { + console.log(data); + }); + }); + + io.set('authorization', function (handshakeData, callback) { + // Authorize using the /users service + app.lookup('users').find({ + username: handshakeData.username, + password: handshakeData.password + }, callback); + }); +})); +``` + +Once the server has been started with `app.listen()` the SocketIO object is available as `app.io`. + +### Primus + +[Primus](https://github.com/primus/primus) is a universal wrapper for real-time frameworks and allows you to transparently use Engine.IO, WebSockets, BrowserChannel, SockJS and Socket.IO. Set it up with `feathers.primus(configuration [, fn])` where `configuration` is the [Primus server configuration](https://github.com/primus/primus#getting-started) and `fn` an optional callback with the Primus server instance that can e.g. be used for setting up [authorization](https://github.com/primus/primus#authorization): + +```js +// Set up Primus with SockJS +app.configure(feathers.primus({ + transformer: 'sockjs' +}, function(primus) { + // Set up Primus authorization here + primus.authorize(function (req, done) { + var auth; + + try { auth = authParser(req.headers['authorization']) } + catch (ex) { return done(ex) } + + // Do some async auth check + authCheck(auth, done); + }); +})); +``` + +In the Browser you can connect like this: + +```html + + +``` + +## API + +### listen + +`app.listen([port])` starts the application on the given port. It will first call the original [Express app.listen([port])](http://expressjs.com/api.html#app.listen), then run `app.setup(server)` (see below) with the server object and then return the server object. + +### setup + +`app.setup(server)` is used initialize all services by calling each services `.setup(app, path)` method (if available). +It will also use the `server` instance passed (e.g. through `http.createServer`) to set up SocketIO (if enabled) and any other provider that might require the server instance. + +Normally `app.setup` will be called automatically when starting the application via `app.listen([port])` but there are cases when you need to initialize the server separately: + +__HTTPS__ + +With your Feathers application initialized it is easy to set up an HTTPS REST and SocketIO server: + +```js +app.configure(feathers.socketio()).use('/todos', todoService); + +var https = require('https'); +var server = https.createServer({ + key: fs.readFileSync('privatekey.pem'), + cert: fs.readFileSync('certificate.pem') +}, app).listen(443); + +// Call app.setup to initialize all services and SocketIO +app.setup(server); +``` + +__Virtual Hosts__ + +You can use `feathers.vhost` (which is the same as [Express and Connect .vhost](http://www.senchalabs.org/connect/vhost.html)) to run your Feathers app on a virtual host: + +```js +app.use('/todos', todoService); + +var host = feathers().use(feathers.vhost('foo.com', app)); +var server = host.listen(8080); + +// Here we need to call app.setup because .listen on our virtal hosted +// app is never called +app.setup(server); +``` + +### lookup + +`app.lookup(path)` returns the wrapped service object for the given path. Note that Feathers internally creates a new object from each registered service. This means that the object returned by `lookup` will provide the same methods and functionality as the original service but also functionality added by Feathers (most notably it is possible to listen to service events). `path` can be the service name with or without leading and trailing slashes. + +```js +app.use('/my/todos', { + create: function(data, params, callback) { + callback(null, data); + } +}); + +var todoService = app.lookup('my/todos'); +// todoService is an event emitter +todoService.on('created', function(todo) { + console.log('Created todo', todo); +}); +``` + +### use + +`app.use([path], service)` works just like [Express app.use([path], middleware)](http://expressjs.com/api.html#app.use) but additionally allows to register a service object (an object which at least provides one of the service methods as outlined in the Services section) instead of the middleware function. Note that REST services are registered in the same order as any other middleware so the below example will allow the `/todos` service only to [Passport](http://passportjs.org/) authenticated users. + +```js +// Serve public folder for everybody +app.use(feathers.static(__dirname + '/public'); +// Make sure that everything else only works with authentication +app.use(function(req,res,next){ + if(req.isAuthenticated()){ + next(); + } else { + // 401 Not Authorized + next(new Error(401)); + } +}); +// Add a service. +app.use('/todos', { get: function(name, params, callback) { callback(null, { id: name, description: "You have to do " + name + "!" }); } +}); +``` + +### service + +`app.service([path], service)` is what is called internally by `app.use([path], service)` if a service object is being passed. Use it instead of `app.use([path], service)` if you want to be more explicit that you are registering a service. `app.service` does __not__ provide the Express `app.use` functionality and doesn't check the service object for valid methods. + +## Services + +A service can be any JavaScript object that offers one or more of the `find`, `get`, `create`, `update`, `remove` and `setup` service methods with the following signatures: + +```js +var myService = { + find: function(params, callback) {}, + get: function(id, params, callback) {}, + create: function(data, params, callback) {}, + update: function(id, data, params, callback) {}, + remove: function(id, params, callback) {}, + setup: function(app) {} +} +``` + +All callbacks follow the `function(error, data)` NodeJS convention. `params` can contain any additional parameters, for example the currently authenticated user. REST service calls set `params.query` with the query parameters (e.g. a query string like `?status=active&type=user` becomes `{ query: { status: "active", type: "user" } }`). + +### find + +`find(params, callback)` retrieves a list of all resources from the service. Ideally use `params.query` for things like filtering and paging so that REST calls like `todo?status=completed&user=10` work right out of the box. + +__REST__ + + GET todo?status=completed&user=10 + +__SocketIO__ + +```js +socket.emit('todo::find', { + query: { + status: 'completed' + user: 10 + } +}, function(error, data) { +}); +``` + +### get + +`get(id, params, callback)` retrieves a single resource with the given `id` from the service. + +__REST__ + + GET todo/1 + +__SocketIO__ + +```js +socket.emit('todo::get', 1, {}, function(error, data) { + +}); +``` + +### create + +`create(data, params, callback)` creates a new resource with `data`. The callback should be called with the newly +created resource data. + +__REST__ + + POST todo + { "description": "I really have to iron" } + +By default the body can be eihter JSON or form encoded as long as the content type is set accordingly. + +__SocketIO__ + +```js +socket.emit('todo::create', { + description: 'I really have to iron' +}, {}, function(error, data) { +}); +``` + +### update + +`update(id, data, params, callback)` updates the resource identified by `id` using `data`. The callback should +be called with the updated resource data. + +__REST__ + + PUT todo/2 + { "description": "I really have to do laundry" } + +__SocketIO__ + +```js +socket.emit('todo::update', 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. + +__REST__ + + DELETE todo/2 + +__SocketIO__ + +```js +socket.emit('todo::remove', 2, {}, function(error, data) { +}); +``` + +### setup + +`setup(app, path)` initializes the service passing an instance of the Feathers application and the path it has been registered on. The SocketIO server is available via `app.io`. `setup` is a great way to connect services: + +```js +var todoService = { + get: function(name, params, callback) { + callback(null, { + id: name, + description: 'You have to ' + name + '!' + }); + } }; +var myService = { + setup: function(app) { + this.todo = app.lookup('todo'); + }, + + get: function(name, params, callback) { + this.todo.get('take out trash', {}, function(error, todo) { + callback(error, { + name: name, + todo: todo + }); + }); + } +} + feathers() - .configure(feathers.socketio()) - .use('/todo', todoService) + .use('todo', todoService) + .use('my', myService) .listen(8000); ``` -That's all there really is to building an app with Feathers. +You can see the combination when going to `http://localhost:8000/my/test`. -### REST +__Pro tip:__ -You can access the REST service by going to `http://localhost:8000/todo/dishes` in your browser -and will see: +Bind the apps `lookup` method to your service to always look your services up dynamically: -```js -{ - "id": "dishes", - "description": "You have to do dishes!" +``` +var myService = { + setup: function(app) { + this.lookup = app.lookup.bind(app); + }, + + get: function(name, params, callback) { + this.lookup('todos').get('take out trash', {}, function(error, todo) { + callback(null, { + name: name, + todo: todo + }); + }); + } } ``` -> Note: Query parameters like `http://localhost:8000/todo/dishes?type=dirty` will be passed as `params.query` +## Events -### SocketIO +Any registered service will be automatically turned into an event emitter that emits events when a resource has changed, that is a `create`, `update` or `remove` service call returned successfully. It is therefore possible to bind to the below events via `app.lookup(servicename).on()` and, if enabled, all events will also broadcast to all connected SocketIO clients in the form of ` `. Note that the service path will always be stripped of leading and trailing slashes regardless of how it has been registered (e.g. `/my/service/` will become `my/service`). + +### created + +The `created` event will be published with the callback data when a service `create` calls back successfully. + +```js +app.use('/todos', { + create: function(data, params, callback) { + callback(null, data); + } +}); + +app.lookup('/todos').on('created', function(todo) { + console.log('Created todo', todo); +}); -Since we configured our app with `feathers.socketio()`, you can also connect to your service via SocketIO. -Create an HTML page and insert the following code to see the response data logged on the console: +app.lookup('/todos').create({ + description: 'We have to do something!' +}, {}, function(error, callback) { + // ... +}); + +app.listen(8000); +``` + +__SocketIO__ ```html ``` -## What's next? +### updated + +The `updated` event will be published with the callback data when a service `update` calls back successfully. + +```js +app.use('/my/todos/', { + update: function(id, data, params, callback) { + callback(null, data); + } +}); + +app.listen(8000); +``` + +__SocketIO__ + +```html + + +``` + +### removed + +The `removed` event will be published with the callback data when a service `remove` calls back successfully. + +```js +app.use('/todos', { + remove: function(id, params, callback) { + callback(null, { id: id }); + } +}); + +app.lookup('/todos').remove(1, {}, function(error, callback) { + // ... +}); + +app.listen(8000); +``` + +__SocketIO__ + +```html + + +``` + +## 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. + +We also think that your data resources can and should be encapsulated in such a way that they can be scalable, easily testable and self contained. The classic web MVC pattern used to work well but is becoming antiquated in today's web. + +With that being said there are some amazing frameworks already out there and we wanted to leverage the ideas that have been put into them, which is why Feathers is built on top of [Express](http://expressjs.com) and is inspired in part by [Sails](http://sailsjs.org), [Flatiron](http://flatironjs.org) and [Derby](http://derbyjs.com). + +## Changelog + +__0.3.1__ + +- REST provider refactoring ([#35](https://github.com/feathersjs/feathers/pull/35)) to make it easier to develop plugins +- HTTP requests now return 405 (Method not allowed) when trying to access unavailable service methods ([#35](https://github.com/feathersjs/feathers/pull/35)) + +__0.3.0__ + +- Added [Primus](https://github.com/primus/primus) provider ([#34](https://github.com/feathersjs/feathers/pull/34)) +- `app.setup(server)` to support HTTPS (and other functionality that requires a custom server) ([#33](https://github.com/feathersjs/feathers/pull/33)) +- Removed bad SocketIO configuration ([#19](https://github.com/feathersjs/feathers/issues/19)) +- Add .npmignore to not publish .idea folder ([#30](https://github.com/feathersjs/feathers/issues/30)) +- Remove middleware: connect.bodyParser() ([#27](https://github.com/feathersjs/feathers/pull/27)) + +__0.2.0__ + +- Pre-initialize `req.feathers` in REST provider to set service parameters +- Allowing to initialize services with or without slashes to be more express-compatible + +__0.1.0__ + +- First beta release +- Directly extends Express +- Removed built in services and moved to [Legs](https://github.com/feathersjs/legs) +- Created [example repository](https://github.com/feathersjs/examples) + +__0.0.x__ + +- Initial test alpha releases + +## License + +Copyright (C) 2013 David Luecke daff@neyeon.com +Copyright (C) 2013 Eric Kryski e.kryski@gmail.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. -Head over to the Feathers website at [feathersjs.com](http://feathersjs.com/) for more examples and the detailed documenation. \ No newline at end of file +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. From 9ca3dc6cb983e952acd1bb8fd3b8f39a8ba53007 Mon Sep 17 00:00:00 2001 From: David Luecke Date: Tue, 18 Feb 2014 21:31:32 -0700 Subject: [PATCH 3/3] release 0.3.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d54277ed95..1890f5fe33 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.0", + "version": "0.3.1", "homepage": "http://feathersjs.com", "repository": { "type": "git",