Skip to content

Commit ba9cad3

Browse files
committed
Add feathers-hooks to core (#596)
Add feathers-hooks to core
1 parent e7487f3 commit ba9cad3

10 files changed

Lines changed: 1740 additions & 3 deletions

File tree

src/hooks/commons.js

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { each, hooks as utils } from 'feathers-commons';
2+
3+
export function isHookObject (hookObject) {
4+
return typeof hookObject === 'object' &&
5+
typeof hookObject.method === 'string' &&
6+
typeof hookObject.type === 'string';
7+
}
8+
9+
export function processHooks (hooks, initialHookObject) {
10+
let hookObject = initialHookObject;
11+
let updateCurrentHook = current => {
12+
if (current) {
13+
if (!isHookObject(current)) {
14+
throw new Error(`${hookObject.type} hook for '${hookObject.method}' method returned invalid hook object`);
15+
}
16+
17+
hookObject = current;
18+
}
19+
20+
return hookObject;
21+
};
22+
let promise = Promise.resolve(hookObject);
23+
24+
// Go through all hooks and chain them into our promise
25+
hooks.forEach(fn => {
26+
const hook = fn.bind(this);
27+
28+
if (hook.length === 2) { // function(hook, next)
29+
promise = promise.then(hookObject => {
30+
return new Promise((resolve, reject) => {
31+
hook(hookObject, (error, result) =>
32+
error ? reject(error) : resolve(result));
33+
});
34+
});
35+
} else { // function(hook)
36+
promise = promise.then(hook);
37+
}
38+
39+
// Use the returned hook object or the old one
40+
promise = promise.then(updateCurrentHook);
41+
});
42+
43+
return promise.catch(error => {
44+
// Add the hook information to any errors
45+
error.hook = hookObject;
46+
throw error;
47+
});
48+
}
49+
50+
export function addHookTypes (target, types = ['before', 'after', 'error']) {
51+
Object.defineProperty(target, '__hooks', {
52+
value: {}
53+
});
54+
55+
types.forEach(type => {
56+
// Initialize properties where hook functions are stored
57+
target.__hooks[type] = {};
58+
});
59+
}
60+
61+
export function getHooks (app, service, type, method, appLast = false) {
62+
const appHooks = app.__hooks[type][method] || [];
63+
const serviceHooks = service.__hooks[type][method] || [];
64+
65+
if (appLast) {
66+
return serviceHooks.concat(appHooks);
67+
}
68+
69+
return appHooks.concat(serviceHooks);
70+
}
71+
72+
export function baseMixin (methods, ...objs) {
73+
const mixin = {
74+
hooks (allHooks) {
75+
each(allHooks, (obj, type) => {
76+
if (!this.__hooks[type]) {
77+
throw new Error(`'${type}' is not a valid hook type`);
78+
}
79+
80+
const hooks = utils.convertHookData(obj);
81+
82+
each(hooks, (value, method) => {
83+
if (method !== 'all' && methods.indexOf(method) === -1) {
84+
throw new Error(`'${method}' is not a valid hook method`);
85+
}
86+
});
87+
88+
methods.forEach(method => {
89+
if (!(hooks[method] || hooks.all)) {
90+
return;
91+
}
92+
93+
const myHooks = this.__hooks[type][method] ||
94+
(this.__hooks[type][method] = []);
95+
96+
if (hooks.all) {
97+
myHooks.push.apply(myHooks, hooks.all);
98+
}
99+
100+
if (hooks[method]) {
101+
myHooks.push.apply(myHooks, hooks[method]);
102+
}
103+
});
104+
});
105+
106+
return this;
107+
}
108+
};
109+
110+
return Object.assign(mixin, ...objs);
111+
}

src/hooks/index.js

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import Proto from 'uberproto';
2+
import { hooks as utils } from 'feathers-commons';
3+
import { addHookTypes, processHooks, baseMixin, getHooks } from './commons';
4+
5+
function isPromise (result) {
6+
return typeof result !== 'undefined' &&
7+
typeof result.then === 'function';
8+
}
9+
10+
function hookMixin (service) {
11+
if (typeof service.hooks === 'function') {
12+
return;
13+
}
14+
15+
const app = this;
16+
const methods = app.methods;
17+
const old = {
18+
before: service.before,
19+
after: service.after
20+
};
21+
const mixin = baseMixin(methods, {
22+
before (before) {
23+
return this.hooks({ before });
24+
},
25+
26+
after (after) {
27+
return this.hooks({ after });
28+
}
29+
});
30+
31+
addHookTypes(service);
32+
33+
methods.forEach(method => {
34+
if (typeof service[method] !== 'function') {
35+
return;
36+
}
37+
38+
mixin[method] = function () {
39+
const service = this;
40+
// A reference to the original method
41+
const _super = this._super.bind(this);
42+
// Additional data to add to the hook object
43+
const hookData = {
44+
app,
45+
service,
46+
get path () {
47+
return Object.keys(app.services)
48+
.find(path => app.services[path] === service);
49+
}
50+
};
51+
// Create the hook object that gets passed through
52+
const hookObject = utils.hookObject(method, 'before', arguments, hookData);
53+
// Get all hooks
54+
const hooks = {
55+
// For before hooks the app hooks will run first
56+
before: getHooks(app, this, 'before', method),
57+
// For after and error hooks the app hooks will run last
58+
after: getHooks(app, this, 'after', method, true),
59+
error: getHooks(app, this, 'error', method, true)
60+
};
61+
62+
// Process all before hooks
63+
return processHooks.call(this, hooks.before, hookObject)
64+
// Use the hook object to call the original method
65+
.then(hookObject => {
66+
if (typeof hookObject.result !== 'undefined') {
67+
return Promise.resolve(hookObject);
68+
}
69+
70+
return new Promise((resolve, reject) => {
71+
const args = utils.makeArguments(hookObject);
72+
// The method may not be normalized yet so we have to handle both
73+
// ways, either by callback or by Promise
74+
const callback = function (error, result) {
75+
if (error) {
76+
reject(error);
77+
} else {
78+
hookObject.result = result;
79+
resolve(hookObject);
80+
}
81+
};
82+
83+
// We replace the callback with resolving the promise
84+
args.splice(args.length - 1, 1, callback);
85+
86+
const result = _super(...args);
87+
88+
if (isPromise(result)) {
89+
result.then(data => callback(null, data), callback);
90+
}
91+
});
92+
})
93+
// Make a copy of hookObject from `before` hooks and update type
94+
.then(hookObject => Object.assign({}, hookObject, { type: 'after' }))
95+
// Run through all `after` hooks
96+
.then(processHooks.bind(this, hooks.after))
97+
// Finally, return the result
98+
.then(hookObject => hookObject.result)
99+
// Handle errors
100+
.catch(error => {
101+
const errorHook = Object.assign({}, error.hook || hookObject, {
102+
type: 'error',
103+
original: error.hook,
104+
error
105+
});
106+
107+
return processHooks
108+
.call(this, hooks.error, errorHook)
109+
.then(hook => Promise.reject(hook.error));
110+
});
111+
};
112+
});
113+
114+
service.mixin(mixin);
115+
116+
// Before hooks that were registered in the service
117+
if (old.before) {
118+
service.before(old.before);
119+
}
120+
121+
// After hooks that were registered in the service
122+
if (old.after) {
123+
service.after(old.after);
124+
}
125+
}
126+
127+
function configure () {
128+
return function () {
129+
const app = this;
130+
131+
addHookTypes(app);
132+
133+
Proto.mixin(baseMixin(app.methods), app);
134+
135+
this.mixins.unshift(hookMixin);
136+
};
137+
}
138+
139+
export default configure;

src/mixins/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export default function () {
22
const mixins = [
33
require('./promise'),
44
require('./event'),
5+
require('../hooks'),
56
require('./normalizer')
67
];
78

test/application.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -333,11 +333,11 @@ describe('Feathers application', () => {
333333
it('mixins are unique to one application', function () {
334334
const app = feathers();
335335
app.mixins.push(function () {});
336-
assert.equal(app.mixins.length, 4);
336+
assert.equal(app.mixins.length, 5);
337337

338338
const otherApp = feathers();
339339
otherApp.mixins.push(function () {});
340-
assert.equal(otherApp.mixins.length, 4);
340+
assert.equal(otherApp.mixins.length, 5);
341341
});
342342

343343
it('initializes a service with only a setup method (#285)', done => {

0 commit comments

Comments
 (0)