From c31fca3eae1b3663aee55f058434a2c2f976c379 Mon Sep 17 00:00:00 2001 From: David Luecke Date: Thu, 24 Apr 2014 20:35:26 -0600 Subject: [PATCH 1/3] Allow services to return a promise instead of calling the callback (#41). --- lib/mixins/index.js | 3 +- lib/mixins/promise.js | 28 ++++++++++ lib/providers/rest/wrappers.js | 2 + lib/providers/socket/commons.js | 2 + package.json | 13 ++--- test/application.test.js | 27 ++++++++++ test/mixins/promise.test.js | 93 +++++++++++++++++++++++++++++++++ 7 files changed, 161 insertions(+), 7 deletions(-) create mode 100644 lib/mixins/promise.js create mode 100644 test/mixins/promise.test.js diff --git a/lib/mixins/index.js b/lib/mixins/index.js index 72bab4bd44..9ab12c36ad 100644 --- a/lib/mixins/index.js +++ b/lib/mixins/index.js @@ -1,5 +1,6 @@ 'use strict'; module.exports = [ - require('./event') + require('./promise'), + require('./event') ]; diff --git a/lib/mixins/promise.js b/lib/mixins/promise.js new file mode 100644 index 0000000000..6588599231 --- /dev/null +++ b/lib/mixins/promise.js @@ -0,0 +1,28 @@ +'use strict'; + +var _ = require('lodash'); +var makeWrapper = function() { + return function() { + var result = this._super.apply(this, arguments); + var callback = arguments[arguments.length - 1]; + + if(typeof result !== 'undefined' && _.isFunction(result.then) && _.isFunction(callback)) { + result.then(function(data) { + callback(null, data); + }, function(error) { + callback(error); + }); + } + return result; + }; +}; + +module.exports = function (service) { + if (typeof service.mixin === 'function') { + var mixin = _.transform(_.pick(service, this.methods), function(result, num, key) { + result[key] = makeWrapper(); + }); + + service.mixin(mixin); + } +}; diff --git a/lib/providers/rest/wrappers.js b/lib/providers/rest/wrappers.js index a35203972d..8296b381c6 100644 --- a/lib/providers/rest/wrappers.js +++ b/lib/providers/rest/wrappers.js @@ -1,3 +1,5 @@ +'use strict'; + var _ = require('lodash'); // A function that returns the middleware for a given method and service diff --git a/lib/providers/socket/commons.js b/lib/providers/socket/commons.js index 40711e804d..7b4d74352a 100644 --- a/lib/providers/socket/commons.js +++ b/lib/providers/socket/commons.js @@ -1,3 +1,5 @@ +'use strict'; + var _ = require('lodash'); // The position of the params parameters for a service method so that we can extend them diff --git a/package.json b/package.json index 1bf15f1db8..9f9092e1ab 100644 --- a/package.json +++ b/package.json @@ -44,14 +44,15 @@ "body-parser": "~1.0.2" }, "devDependencies": { - "request": "~2.x", - "socket.io-client": "~0.9.0", - "grunt-cli": "~0.1.0", "grunt": "~0.4.0", - "grunt-release": "~0.5.0", - "mocha": "~1.x", + "grunt-cli": "~0.1.0", "grunt-contrib-jshint": "~0.x", + "grunt-jsbeautifier": "~0.2.0", + "grunt-release": "~0.5.0", "grunt-simple-mocha": "~0.4.0", - "grunt-jsbeautifier": "~0.2.0" + "mocha": "~1.x", + "q": "^1.0.1", + "request": "~2.x", + "socket.io-client": "~0.9.0" } } diff --git a/test/application.test.js b/test/application.test.js index c5e5639df3..7eaae476bf 100644 --- a/test/application.test.js +++ b/test/application.test.js @@ -6,6 +6,7 @@ var io = require('socket.io-client'); var request = require('request'); var https = require('https'); var fs = require('fs'); +var q = require('q'); var feathers = require('../lib/feathers'); @@ -186,4 +187,30 @@ describe('Feathers application', function () { }); }); }); + + it('returns the value of a promise (#41)', function (done) { + var original = {}; + var todoService = { + get: function (name) { + original = { + id: name, + q: true, + description: "You have to do " + name + "!" + }; + return q(original); + } + }; + + var app = feathers() + .configure(feathers.rest()) + .use('/todo', todoService); + + var server = app.listen(6880).on('listening', function () { + request('http://localhost:6880/todo/dishes', function (error, response, body) { + assert.ok(response.statusCode === 200, 'Got OK status code'); + assert.deepEqual(original, JSON.parse(body)); + server.close(done); + }); + }); + }); }); diff --git a/test/mixins/promise.test.js b/test/mixins/promise.test.js new file mode 100644 index 0000000000..c432acfe4b --- /dev/null +++ b/test/mixins/promise.test.js @@ -0,0 +1,93 @@ +'use strict'; + +var assert = require('assert'); +var Proto = require('uberproto'); +var q = require('q'); +var _ = require('lodash'); + +var mixin = require('../../lib/mixins/promise'); + +describe('Promises/A+ mixin', function () { + it('Calls a callback when a promise is returned from the original service', function (done) { + // A dummy context (this will normally be the application) + var context = { + methods: ['get'] + }; + var FixtureService = Proto.extend({ + get: function (id) { + return q({ + id: id, + description: 'You have to do ' + id + }); + } + }); + + mixin.call(context, FixtureService); + + var instance = Proto.create.call(FixtureService); + instance.get('dishes', {}, function (error, data) { + assert.deepEqual(data, { + id: 'dishes', + description: 'You have to do dishes' + }); + done(); + }); + }); + + it('calls back with an error for a failing deferred', function(done) { + // A dummy context (this will normally be the application) + var context = { + methods: ['get'] + }; + var FixtureService = Proto.extend({ + get: function () { + var dfd = q.defer(); + + _.defer(function() { + dfd.reject(new Error('Something went wrong')); + }); + + return dfd.promise; + } + }); + + mixin.call(context, FixtureService); + + var instance = Proto.create.call(FixtureService); + instance.get('dishes', {}, function (error) { + assert.ok(error); + assert.equal(error.message, 'Something went wrong'); + done(); + }); + }); + + it('does not try to call the callback if it does not exist', function(done) { + // A dummy context (this will normally be the application) + var context = { + methods: ['create'] + }; + var FixtureService = Proto.extend({ + create: function (data) { + var dfd = q.defer(); + + _.defer(function() { + dfd.resolve(data); + }); + + return dfd.promise; + } + }); + var original = { + id: 'laundry', + description: 'You have to do laundry' + }; + + mixin.call(context, FixtureService); + + var instance = Proto.create.call(FixtureService); + instance.create(original, {}).then(function(data) { + assert.deepEqual(data, original); + done(); + }); + }); +}); From 243ba80860ae37da221cc70264100b7d16656612 Mon Sep 17 00:00:00 2001 From: David Luecke Date: Thu, 24 Apr 2014 22:10:32 -0600 Subject: [PATCH 2/3] Fixing package.json for older NPM. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9f9092e1ab..0051c645f5 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "grunt-release": "~0.5.0", "grunt-simple-mocha": "~0.4.0", "mocha": "~1.x", - "q": "^1.0.1", + "q": "~1.0.1", "request": "~2.x", "socket.io-client": "~0.9.0" } From a1e9e22a219caeff521cdadef22c1f58204d046a Mon Sep 17 00:00:00 2001 From: David Luecke Date: Fri, 25 Apr 2014 18:44:26 -0600 Subject: [PATCH 3/3] Updating documentaiton for use with Promises. --- readme.md | 215 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 119 insertions(+), 96 deletions(-) diff --git a/readme.md b/readme.md index 17b554a113..99ba0e55d7 100644 --- a/readme.md +++ b/readme.md @@ -171,103 +171,9 @@ app.configure(feathers.primus({ })); ``` -## 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: +As mentioned, the basic Feathers functionality is fully compatible with Express. The key concept added to that of middleware is *service objects. 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 = { @@ -281,7 +187,30 @@ var myService = { } ``` -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" } }`). +And can be used like any other Express middleware `app.use('/my-service', myService)`. + +All service 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" } }`). + +It is also possible to return a [Promise](http://promises-aplus.github.io/promises-spec/) object from a service instead of using the callback, for example using [Q](https://github.com/kriskowal/q): + +```js +var Q = require('q'); + +var todos = { + get: function(id) { + var dfd = Q.defer(); + + setTimeout(function() { + dfd.resolve({ + id: id, + description: 'You have to do ' + id + }); + }, 500); + + return dfd.promise; + } +} +``` ### find @@ -626,6 +555,100 @@ socket.on('todo updated', function(data) { }); ``` +## 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. + ## 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.