8000 Allow adding hooks to other service methods by bertho-zero · Pull Request #924 · feathersjs/feathers · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
be0d583
open hooks workflow to custom methods
bertho-zero May 1, 2018
8071cae
remove console.log
bertho-zero May 1, 2018
8f62215
remove useless line
bertho-zero May 1, 2018
8bad3a3
new service 'methods' shape
bertho-zero May 5, 2018
214cbe9
pick arguments in validateHook for custom methods
bertho-zero May 23, 2018
700cf47
separate validateHook and pickArgsHook
bertho-zero May 23, 2018
a25679f
extend pickArgsHook to all methods
bertho-zero May 26, 2018
62cc332
remove variable with single usage
bertho-zero May 26, 2018
ecb6617
validate before pick arguments
bertho-zero May 26, 2018
4dacd27
export withHooks method
bertho-zero May 27, 2018
8fe66d3
add tests for withHooks
bertho-zero May 27, 2018
e6e0df0
fix getHookArray
bertho-zero May 27, 2018
2ceaf94
Merge branch 'master' into custom
bertho-zero May 27, 2018
eb9f119
Merge branch 'master' of https://github.com/feathersjs/feathers into …
bertho-zero May 27, 2018
70adf86
Merge branch 'master' of https://github.com/feathersjs/feathers into …
bertho-zero May 27, 2018
bd6187d
Merge branch 'custom' of github.com:bertho-zero/feathers into custom
bertho-zero May 27, 2018
0cb4b90
Merge branch 'custom' into with-hooks
bertho-zero May 27, 2018
0715859
replace Object.assign by assignation
bertho-zero May 31, 2018
390abfc
simplify names
bertho-zero May 31, 2018
df90c05
add activeHooks method
bertho-zero Jun 3, 2018
ab6f359
all methods in service.methods
bertho-zero Jun 3, 2018
84e8c31
rename activeHooks to activateHooks
bertho-zero Jun 4, 2018
c9ffd1b
remove useless assignation
bertho-zero Jun 4, 2018
05c63ba
Merge branch 'custom' of github.com:bertho-zero/feathers into custom
bertho-zero Jun 4, 2018
96b9144
use Object.getOwnPropertyNames instead of Object.keys
bertho-zero Jul 23, 2018
4727b7d
update @featherjs/commons
bertho-zero Aug 3, 2018
fc2903d
Merge branch 'custom' of https://github.com/bertho-zero/feathers into…
bertho-zero Aug 3, 2018
97a18b5
Merge branch 'master' into custom
bertho-zero Aug 3, 2018
c06ed86
fix test for node 6
bertho-zero Aug 3, 2018
3444975
Merge branch 'custom' of https://github.com/bertho-zero/feathers into…
bertho-zero Aug 3, 2018
c347bbd
Merge branch 'master' into custom
daffl Aug 3, 2018
ccf8db3
Merge branch 'custom' into with-hooks
bertho-zero Aug 6, 2018
52c1cfa
fix withHooks
bertho-zero Aug 6, 2018
4f74204
Update hooks.js
bertho-zero Aug 7, 2018
254074a
use Object.defineProperty in the activateHooks method
bertho-zero Aug 7, 2018
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
3 changes: 3 additions & 0 deletions .gitignore
8000
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ node_modules

# Users Environment Variables
.lock-wscript

# IDEs
.idea
250 changes: 163 additions & 87 deletions lib/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,119 +8,184 @@ const {
makeArguments
} = hooks;

const ACTIVATE_HOOKS = typeof Symbol !== 'undefined'
? Symbol('__feathersActivateHooks')
: '__feathersActivateHooks';

function getHookArray (hooks, type) {
return hooks && hooks[type] && Array.isArray(hooks[type])
? hooks[type]
: hooks && hooks[type]
? [hooks[type]]
: [];
}

const withHooks = function withHooks ({
app,
service,
method
}) {
return (hooks = {}) => (...args) => {
const returnHook = args[args.length - 1] === true
? args.pop() : false;

// A reference to the original method
const _super = service._super ? service._super.bind(service) : service[method].bind(service);
// Create the hook object that gets passed through
const hookObject = createHookObject(method, {
type: 'before', // initial hook object type
service,
app
});

// A hook that pick arguments for methods defined in `service.methods`
const pickArgsHook = context => {
const argsObject = args.reduce(
(result, value, index) => {
result[service.methods[method][index]] = value;
return result;
},
{ params: {} }
);

Object.assign(context, argsObject);

return context;
};

// Process all before hooks
return processHooks.call(service, [pickArgsHook, ...getHookArray(hooks, 'before')], hookObject)
// Use the hook object to call the original method
.then(hookObject => {
// If `hookObject.result` is set, skip the original method
if (typeof hookObject.result !== 'undefined') {
return hookObject;
}

// Otherwise, call it with arguments created from the hook object
const promise = _super(...makeArguments(hookObject));

if (!isPromise(promise)) {
throw new Error(`Service method '${hookObject.method}' for '${hookObject.path}' service must return a promise`);
}

return promise.then(result => {
hookObject.result = result;

return hookObject;
});
})
// Make a (shallow) copy of hookObject from `before` hooks and update type
.then(hookObject => Object.assign({}, hookObject, { type: 'after' }))
// Run through all `after` hooks
.then(hookObject => {
// Combine all app and service `after` and `finally` hooks and process
const hookChain = getHookArray(hooks, 'after')
.concat(getHookArray(hooks, 'finally'));

return processHooks.call(service, hookChain, hookObject);
})
.then(hookObject =>
// Finally, return the result
// Or the hook object if the `returnHook` flag is set
returnHook ? hookObject : hookObject.result
)
// Handle errors
.catch(error => {
// Combine all app and service `error` and `finally` hooks and process
const hookChain = getHookArray(hooks, 'error')
.concat(getHookArray(hooks, 'finally'));

// A shallow copy of the hook object
const errorHookObject = _.omit(Object.assign({}, error.hook, hookObject, {
type: 'error',
original: error.hook,
error
}), 'result');

return processHooks.call(service, hookChain, errorHookObject)
.catch(error => {
errorHookObject.error = error;

return errorHookObject;
})
.then(hook => {
if (returnHook) {
// Either resolve or reject with the hook object
return typeof hook.result !== 'undefined' ? hook : Promise.reject(hook);
}

// Otherwise return either the result if set (to swallow errors)
// Or reject with the hook error
return typeof hook.result !== 'undefined' ? hook.result : Promise.reject(hook.error);
});
});
};
};

// A service mixin that adds `service.hooks()` method and functionality
const hookMixin = exports.hookMixin = function hookMixin (service) {
if (typeof service.hooks === 'function') {
return;
}

service.methods = Object.getOwnPropertyNames(service)
.filter(key => typeof service[key] === 'function' && service[key][ACTIVATE_HOOKS])
.reduce((result, methodName) => {
result[methodName] = service[methodName][ACTIVATE_HOOKS];
return result;
}, service.methods || {});

Object.assign(service.methods, {
find: ['params'],
get: ['id', 'params'],
create: ['data', 'params'],
update: ['id', 'data', 'params'],
patch: ['id', 'data', 'params'],
remove: ['id', 'params']
});

const app = this;
const methods = app.methods;
const methodNames = Object.keys(service.methods);
const mixin = {};

// Add .hooks method and properties to the service
enableHooks(service, methods, app.hookTypes);
enableHooks(service, methodNames, app.hookTypes);

// Assemble the mixin object that contains all "hooked" service methods
methods.forEach(method => {
methodNames.forEach(method => {
if (typeof service[method] !== 'function') {
return;
}

mixin[method] = function () {
const service = this;
const args = Array.from(arguments);
// If the last argument is `true` we want to return
// the actual hook object instead of the result
const returnHook = args[args.length - 1] === true
? args.pop() : false;

// A reference to the original method
const _super = service._super.bind(service);
// Create the hook object that gets passed through
const hookObject = createHookObject(method, args, {
type: 'before', // initial hook object type
service,
app
});
const returnHook = args[args.length - 1] === true;

// A hook that validates the arguments and will always be the very first
const validateHook = context => {
validateArguments(method, args);
validateArguments(service.methods, method, returnHook ? args.slice(0, -1) : args);

return context;
};
// The `before` hook chain (including the validation hook)
const beforeHooks = [ validateHook, ...getHooks(app, service, 'before', method) ];

// Process all before hooks
return processHooks.call(service, beforeHooks, hookObject)
// Use the hook object to call the original method
.then(hookObject => {
// If `hookObject.result` is set, skip the original method
if (typeof hookObject.result !== 'undefined') {
return hookObject;
}

// Otherwise, call it with arguments created from the hook object
const promise = _super(...makeArguments(hookObject));

if (!isPromise(promise)) {
throw new Error(`Service method '${hookObject.method}' for '${hookObject.path}' service must return a promise`);
}

return promise.then(result => {
hookObject.result = result;
// Needed
416B service._super = service._super.bind(service);

return hookObject;
});
})
// Make a (shallow) copy of hookObject from `before` hooks and update type
.then(hookObject => Object.assign({}, hookObject, { type: 'after' }))
// Run through all `after` hooks
.then(hookObject => {
// Combine all app and service `after` and `finally` hooks and process
const afterHooks = getHooks(app, service, 'after', method, true);
const finallyHooks = getHooks(app, service, 'finally', method, true);
const hookChain = afterHooks.concat(finallyHooks);

return processHooks.call(service, hookChain, hookObject);
})
.then(hookObject =>
// Finally, return the result
// Or the hook object if the `returnHook` flag is set
returnHook ? hookObject : hookObject.result
)
// Handle errors
.catch(error => {
// Combine all app and service `error` and `finally` hooks and process
const errorHooks = getHooks(app, service, 'error', method, true);
const finallyHooks = getHooks(app, service, 'finally', method, true);
const hookChain = errorHooks.concat(finallyHooks);

// A shallow copy of the hook object
const errorHookObject = _.omit(Object.assign({}, error.hook, hookObject, {
type: 'error',
original: error.hook,
error
}), 'result');

return processHooks.call(service, hookChain, errorHookObject)
.catch(error => {
errorHookObject.error = error;

return errorHookObject;
})
.then(hook => {
if (returnHook) {
// Either resolve or reject with the hook object
return typeof hook.result !== 'undefined' ? hook : Promise.reject(hook);
}

// Otherwise return either the result if set (to swallow errors)
// Or reject with the hook error
return typeof hook.result !== 'undefined' ? hook.result : Promise.reject(hook.error);
});
});
return withHooks({
app,
service,
method
})({
before: [
validateHook,
...getHooks(app, service, 'before', method)
],
after: getHooks(app, service, 'after', method, true),
error: getHooks(app, service, 'error', method, true),
finally: getHooks(app, service, 'finally', method, true)
})(...args);
};
});

Expand All @@ -141,3 +206,14 @@ module.exports = function () {
app.mixins.push(hookMixin);
};
};

module.exports.withHooks = withHooks;

module.exports.ACTIVATE_HOOKS = ACTIVATE_HOOKS;

module.exports.activateHooks = function activateHooks (args) {
return fn => {
Object.defineProperty(fn, ACTIVATE_HOOKS, { value: args });
return fn;
};
};
3 changes: 3 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const { hooks } = require('@feathersjs/commons');
const Proto = require('uberproto');
const Application = require('./application');
const version = require('./version');
const { ACTIVATE_HOOKS, activateHooks } = require('./hooks');

function createApplication () {
const app = {};
Expand All @@ -16,6 +17,8 @@ function createApplication () {

createApplication.version = version;
createApplication.SKIP = hooks.SKIP;
createApplication.ACTIVATE_HOOKS = ACTIVATE_HOOKS;
createApplication.activateHooks = activateHooks;

module.exports = createApplication;

Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"node": ">= 6"
},
"dependencies": {
"@feathersjs/commons": "^1.4.1",
"@feathersjs/commons": "^2.0.0",
"debug": "^3.1.0",
"events": "^3.0.0",
"uberproto": "^2.0.2"
Expand Down
62 changes: 62 additions & 0 deletions test/hooks/hooks.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -242,4 +242,66 @@ describe('hooks basics', () => {
});
});
});

it('can register hooks on a custom method', () => {
const app = feathers().use('/dummy', {
methods: {
custom: ['id', 'data', 'params']
},
get () {},
custom (id, data, params) {
return Promise.resolve([id, data, params]);
},
// activateHooks is usable as a decorator: @activateHooks(['id', 'data', 'params'])
other: feathers.activateHooks(['id', 'data', 'params'])(
(id, data, params) => {
return Promise.resolve([id, data, params]);
}
)
});

app.service('dummy').hooks({
before: {
all (context) {
context.test = ['all::before'];
},
custom (context) {
context.test.push('custom::before');
}
},
after: {
all (context) {
context.test.push('all::after');
},
custom (context) {
context.test.push('custom::after');
}
}
});

const args = [1, { test: 'ok' }, { provider: 'rest' }];

assert.deepEqual(app.service('dummy').methods, {
find: ['params'],
get: ['id', 'params'],
create: ['data', 'params'],
update: ['id', 'data', 'params'],
patch: ['id', 'data', 'params'],
remove: ['id', 'params'],
custom: ['id', 'data', 'params'],
other: ['id', 'data', 'params']
});

return app.service('dummy').custom(...args, true)
.then(hook => {
assert.deepEqual(hook.result, args);
assert.deepEqual(hook.test, ['all::before', 'custom::before', 'all::after', 'custom::after']);

app.service('dummy').other(...args, true)
.then(hook => {
assert.deepEqual(hook.result, args);
assert.deepEqual(hook.test, ['all::before', 'all::after']);
});
});
});
});
Loading
0