Skip to content
This repository has been archived by the owner on Apr 20, 2018. It is now read-only.

Commit

Permalink
Merge pull request #29 from feathersjs/promises-28
Browse files Browse the repository at this point in the history
Refactoring for hooks to use promises and promise chains
  • Loading branch information
ekryski committed Jan 12, 2016
2 parents 52bc789 + 077e3d3 commit b3baa30
Show file tree
Hide file tree
Showing 10 changed files with 871 additions and 762 deletions.
100 changes: 27 additions & 73 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@

[![Build Status](https://travis-ci.org/feathersjs/feathers-hooks.png?branch=master)](https://travis-ci.org/feathersjs/feathers-hooks)

> Before and after service method call hooks for easy authorization and processing.
> Middleware for Feathers service methods
## Getting Started

To install feathers-hooks from [npm](https://www.npmjs.org/), run:
`feathers-hooks` allow to register composable middleware functions before or after a Feathers service method executes to easily decouple things like authorization and pre- or post processing from your service logic. To install from [npm](https://www.npmjs.com/package/feathers-hooks), run:

```bash
$ npm install feathers-hooks --save
```

Finally, to use the plugin in your Feathers app:
Then, to use the plugin in your Feathers app:

```javascript
var feathers = require('feathers');
Expand All @@ -23,64 +23,36 @@ var app = feathers().configure(hooks());

## Examples

The repository contains the following working examples:
The repository contains the following examples:

- [authorization.js](https://github.com/feathersjs/feathers-hooks/blob/master/examples/authorization.js) - A simple demo showing how to use hooks for authorization (and post-processing the results) where the user is set via a ?user=username query parameter.
- [authorization.js](https://github.com/feathersjs/feathers-hooks/blob/master/examples/authorization.js) - A simple demo showing how to use hooks for authorization (and post-processing the results) where the user is set via a `?user=username` query parameter.
- [timestamp.js](https://github.com/feathersjs/feathers-hooks/blob/master/examples/timestamp.js) - A demo that adds a `createdAt` and `updatedAt` timestamp when creating or updating a Todo using hooks.

## Using hooks

Feathers hooks are a form of [Aspect Oriented Programming](http://en.wikipedia.org/wiki/Aspect-oriented_programming) that allow you to decouple things like authorization and pre- or post processing from your services logic.

You can add as many `before` and `after` hooks to any Feathers service method or `all` service methods (they will be executed in the order they have been registered). There are two ways to use hooks. Either after registering the service by calling `service.before(beforeHooks)` or `service.after(afterHooks)` or by adding a `before` or `after` object with your hooks to the service.

Lets assume a Feathers application initialized like this:

```js
var feathers = require('feathers');
var memory = require('feathers-memory');
var hooks = require('feathers-hooks');

var app = feathers()
.configure(feathers.rest())
.configure(hooks())
.use('/todos', {
todos: [],

get: function(id, params, callback) {
for(var i = 0; i < this.todos.length; i++) {
if(this.todos[i].id === id) {
return callback(null, this.todos[i]);
}
}

callback(new Error('Todo not found'));
},

// Return all todos from this service
find: function(params, callback) {
callback(null, this.todos);
},

// Create a new Todo with the given data
create: function(data, params, callback) {
data.id = this.todos.length;
this.todos.push(data);

callback(null, data);
}
});
.use('/todos', memory());

app.listen(8000);

// Get the wrapped service object which will be used in the other exapmles
// Get the wrapped service object which will be used in the other examples
var todoService = app.service('todos');
```

### `service.before(beforeHooks)`

`before` hooks allow you to pre-process service call parameters. They will be called with the hook object
and a callback which should be called with any errors or no arguments or `null` and the modified hook object.
The hook object contains information about the intercepted method and for `before` hooks can have the following properties:
`before` hooks allow you to pre-process service call parameters. They will be called with the hook object and a callback which should be called with any errors or no arguments or `null` and the modified hook object. The hook object contains information about the intercepted method and for `before` hooks can have the following properties:

- __method__ - The method name
- __type__ - The hook type (`before` or `after`)
Expand All @@ -95,20 +67,12 @@ The following example adds an authorization check (if a user has been provided i

```js
todoService.before({
all: function (hook, next) {
if (!hook.params.user) {
return next(new Error('You are not logged in'));
}

next();
all: function (hook) {
throw new Error('You are not logged in');
},

create: function(hook, next) {
hook.data.createdAt = new Date();

next();
// Or
next(null, hook);
}
});
```
Expand All @@ -133,21 +97,17 @@ The following example filters the data returned by a `find` service call based o

```js
todoService.after({
find: function (hook, next) {
find: function (hook) {
// Manually filter the find results
hook.result = _.filter(hook.result, function (current) {
return current.companyId === params.user.companyId;
});

next();
},

get: function (hook, next) {
get: function (hook) {
if (hook.result.companyId !== hook.params.user.companyId) {
return next(new Error('You are not authorized to access this information'));
throw new Error('You are not authorized to access this information');
}

next();
}
});
```
Expand Down Expand Up @@ -188,52 +148,44 @@ var TodoService = {
},

before: {
find: function (hook, next) {
find: function (hook) {
if (!hook.params.user) {
return next(new Error('You are not logged in'));
throw new Error('You are not logged in');
}

next();
},

create: function (hook, next) {
create: function (hook) {
hook.data.createdAt = new Date();

next();
// Or
next(null, hook);
}
},

after: {
find: function (hook, next) {
find: function (hook) {
// Manually filter the find results
hook.result = _.filter(hook.result, function (current) {
return current.companyId === params.user.companyId;
});

next();
},

get: function (hook, next) {
get: function (hook) {
if (hook.result.companyId !== hook.params.user.companyId) {
return next(new Error('You are not authorized to access this information'));
throw new Error('You are not authorized to access this information');
}

next();
}
}
}
```

### Promises

All hooks can return a [Promise](http://promises-aplus.github.io/promises-spec/) object instead of calling the callback. The promises return value will *not* be used. Using [Q](https://github.com/kriskowal/q) it would look like:
All hooks can return a [Promise](http://promises-aplus.github.io/promises-spec/) object instead of calling the callback.

```js
todoService.before({
find: function (hook) {
return Q(/* ... */);
return new Promise(function(resolve, reject) {

});
}
});
```
Expand All @@ -243,9 +195,11 @@ If you want to change the hook object just chain the returned promise using `.th
```js
todoService.before({
find: function (hook) {
return Q(/* ... */).then(function(result) {
return this.find().then(function(data) {
hook.params.message = 'Ran through promise hook';
hook.data.result = result;
// Always return the hook object
return hook;
});
}
});
Expand Down
10 changes: 3 additions & 7 deletions examples/authorization.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,24 +49,20 @@ var todoService = app.service('todos');

// This `before` hook checks if a user is set
todoService.before({
find: function(hook, next) {
find: function(hook) {
// If no user is set, throw an error
if(!hook.params.user) {
return next(new Error('You are not authorized. Set the ?user=username parameter.'));
throw new Error('You are not authorized. Set the ?user=username parameter.');
}

next();
}
});

// This `after` hook sets the username for each Todo
todoService.after({
find: function(hook, next) {
find: function(hook) {
hook.result.forEach(function(todo) {
todo.user = hook.params.user.name;
});

next();
}
});

Expand Down
8 changes: 2 additions & 6 deletions examples/timestamp.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,12 @@ var todoService = app.service('todos');

// Register a hook that adds a createdAt and updatedAt timestamp
todoService.before({
create: function(hook, next) {
create: function(hook) {
hook.data.createdAt = new Date();

next();
},

update: function(hook, next) {
update: function(hook) {
hook.data.updatedAt = new Date();

next();
}
});

Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
"lib": "lib"
},
"dependencies": {
"debug": "^2.2.0",
"feathers-commons": "^0.5.0"
},
"devDependencies": {
Expand Down
93 changes: 40 additions & 53 deletions src/after.js
Original file line number Diff line number Diff line change
@@ -1,68 +1,55 @@
import makeDebug from 'debug';
import { hooks as utils } from 'feathers-commons';
import { makeHookFn, createMixin } from './commons';
import { addHookMethod, processHooks } from './commons';

const debug = makeDebug('feathers-hooks:after');

/**
* Return the hook mixin method for the given name.
*
* @param {String} method The service method name
* @returns {Function}
*/
function getMixin(method) {
return function() {
var _super = this._super;

if(!this.__after || !this.__after[method].length) {
return _super.apply(this, arguments);
}
export default function(service) {
if(typeof service.mixin !== 'function') {
return;
}

const args = Array.from(arguments);
const hookObject = utils.hookObject(method, 'after', args);
const methods = this.methods;
const old = service.after;

// Make a copy of our hooks
const hooks = this.__after[method].slice();
debug(`Running ${hooks.length} after hooks for method ${method}`);
addHookMethod(service, 'after', methods);

// Remove the old callback and replace with the new callback that runs the hook
args.pop();
// The new _super method callback
args.push(function(error, result) {
if(error) {
// Call the old callback with the error
return hookObject.callback(error);
}
const mixin = {};

var fn = function(hookObject) {
return hookObject.callback(null, hookObject.result);
};
methods.forEach(method => {
if(typeof service[method] !== 'function') {
return;
}

// Set hookObject result
hookObject.result = result;
mixin[method] = function() {
const originalCallback = arguments[arguments.length - 1];

while(hooks.length) {
fn = makeHookFn(hooks.pop(), fn);
}
// Call the _super method which will return the `before` hook object
return this._super.apply(this, arguments)
// Make a copy of hookObject from `before` hooks and update type
.then(hookObject => Object.assign({}, hookObject, { type: 'after' }))
// Run through all `after` hooks
.then(processHooks.bind(this, this.__afterHooks[method]))
// Convert the results and call the original callback if available
.then(hookObject => {
const callback = hookObject.callback || originalCallback;

return fn.call(this, hookObject);
}.bind(this));
if(typeof callback === 'function') {
hookObject.callback(null, hookObject.result);
}

return hookObject.result;
}).catch(error => {
const callback = (error && error.hook && error.hook.callback) || originalCallback;

return _super.apply(this, args);
};
}
if(typeof callback === 'function') {
callback(error);
}

function addHooks(hooks, method) {
const myHooks = this.__after[method];
throw error;
});
};
});

if(hooks[method]) {
myHooks.push.apply(myHooks, hooks[method]);
}
service.mixin(mixin);

if(hooks.all) {
myHooks.push.apply(myHooks, hooks.all);
if(old) {
service.after(old);
}
}

module.exports = createMixin('after', getMixin, addHooks);
Loading

0 comments on commit b3baa30

Please sign in to comment.