Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions src/hooks/commons.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { each, hooks as utils } from 'feathers-commons';

export function isHookObject (hookObject) {
return typeof hookObject === 'object' &&
typeof hookObject.method === 'string' &&
typeof hookObject.type === 'string';
}

export function processHooks (hooks, initialHookObject) {
let hookObject = initialHookObject;
let updateCurrentHook = current => {
if (current) {
if (!isHookObject(current)) {
throw new Error(`${hookObject.type} hook for '${hookObject.method}' method returned invalid hook object`);
}

hookObject = current;
}

return hookObject;
};
let promise = Promise.resolve(hookObject);

// Go through all hooks and chain them into our promise
hooks.forEach(fn => {
const hook = fn.bind(this);

if (hook.length === 2) { // function(hook, next)
promise = promise.then(hookObject => {
return new Promise((resolve, reject) => {
hook(hookObject, (error, result) =>
error ? reject(error) : resolve(result));
});
});
} else { // function(hook)
promise = promise.then(hook);
}

// Use the returned hook object or the old one
promise = promise.then(updateCurrentHook);
});

return promise.catch(error => {
// Add the hook information to any errors
error.hook = hookObject;
throw error;
});
}

export function addHookTypes (target, types = ['before', 'after', 'error']) {
Object.defineProperty(target, '__hooks', {
value: {}
});

types.forEach(type => {
// Initialize properties where hook functions are stored
target.__hooks[type] = {};
});
}

export function getHooks (app, service, type, method, appLast = false) {
const appHooks = app.__hooks[type][method] || [];
const serviceHooks = service.__hooks[type][method] || [];

if (appLast) {
return serviceHooks.concat(appHooks);
}

return appHooks.concat(serviceHooks);
}

export function baseMixin (methods, ...objs) {
const mixin = {
hooks (allHooks) {
each(allHooks, (obj, type) => {
if (!this.__hooks[type]) {
throw new Error(`'${type}' is not a valid hook type`);
}

const hooks = utils.convertHookData(obj);

each(hooks, (value, method) => {
if (method !== 'all' && methods.indexOf(method) === -1) {
throw new Error(`'${method}' is not a valid hook method`);
}
});

methods.forEach(method => {
if (!(hooks[method] || hooks.all)) {
return;
}

const myHooks = this.__hooks[type][method] ||
(this.__hooks[type][method] = []);

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

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

return this;
}
};

return Object.assign(mixin, ...objs);
}
139 changes: 139 additions & 0 deletions src/hooks/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import Proto from 'uberproto';
import { hooks as utils } from 'feathers-commons';
import { addHookTypes, processHooks, baseMixin, getHooks } from './commons';

function isPromise (result) {
return typeof result !== 'undefined' &&
typeof result.then === 'function';
}

function hookMixin (service) {
if (typeof service.hooks === 'function') {
return;
}

const app = this;
const methods = app.methods;
const old = {
before: service.before,
after: service.after
};
const mixin = baseMixin(methods, {
before (before) {
return this.hooks({ before });
},

after (after) {
return this.hooks({ after });
}
});

addHookTypes(service);

methods.forEach(method => {
if (typeof service[method] !== 'function') {
return;
}

mixin[method] = function () {
const service = this;
// A reference to the original method
const _super = this._super.bind(this);
// Additional data to add to the hook object
const hookData = {
app,
service,
get path () {
return Object.keys(app.services)
.find(path => app.services[path] === service);
}
};
// Create the hook object that gets passed through
const hookObject = utils.hookObject(method, 'before', arguments, hookData);
// Get all hooks
const hooks = {
// For before hooks the app hooks will run first
before: getHooks(app, this, 'before', method),
// For after and error hooks the app hooks will run last
after: getHooks(app, this, 'after', method, true),
error: getHooks(app, this, 'error', method, true)
};

// Process all before hooks
return processHooks.call(this, hooks.before, hookObject)
// Use the hook object to call the original method
.then(hookObject => {
if (typeof hookObject.result !== 'undefined') {
return Promise.resolve(hookObject);
}

return new Promise((resolve, reject) => {
const args = utils.makeArguments(hookObject);
// The method may not be normalized yet so we have to handle both
// ways, either by callback or by Promise
const callback = function (error, result) {
if (error) {
reject(error);
} else {
hookObject.result = result;
resolve(hookObject);
}
};

// We replace the callback with resolving the promise
args.splice(args.length - 1, 1, callback);

const result = _super(...args);

if (isPromise(result)) {
result.then(data => callback(null, data), callback);
}
});
})
// 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, hooks.after))
// Finally, return the result
.then(hookObject => hookObject.result)
// Handle errors
.catch(error => {
const errorHook = Object.assign({}, error.hook || hookObject, {
type: 'error',
original: error.hook,
error
});

return processHooks
.call(this, hooks.error, errorHook)
.then(hook => Promise.reject(hook.error));
});
};
});

service.mixin(mixin);

// Before hooks that were registered in the service
if (old.before) {
service.before(old.before);
}

// After hooks that were registered in the service
if (old.after) {
service.after(old.after);
}
}

function configure () {
return function () {
const app = this;

addHookTypes(app);

Proto.mixin(baseMixin(app.methods), app);

this.mixins.unshift(hookMixin);
};
}

export default configure;
1 change: 1 addition & 0 deletions src/mixins/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export default function () {
const mixins = [
require('./promise'),
require('./event'),
require('../hooks'),
require('./normalizer')
];

Expand Down
4 changes: 2 additions & 2 deletions test/application.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -333,11 +333,11 @@ describe('Feathers application', () => {
it('mixins are unique to one application', function () {
const app = feathers();
app.mixins.push(function () {});
assert.equal(app.mixins.length, 4);
assert.equal(app.mixins.length, 5);

const otherApp = feathers();
otherApp.mixins.push(function () {});
assert.equal(otherApp.mixins.length, 4);
assert.equal(otherApp.mixins.length, 5);
});

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