-
-
Notifications
You must be signed in to change notification settings - Fork 753
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Koa style middleware: Hooks for any asynchronous method #932
Comments
I would hope this proposal can solve the 2 biggest issues I face with the common hooks using the Buzzard design. Use case 1: More complex hooks often need to make a call on their own or another service. In my experience its often a Problems: (1) The Solution: (1) Service calls made on the server need to optionally skip all (some?) hooks. Use case 2: When the cache hook, for example, finds a cache hit, it needs to set context.result and skip the following before hooks. Problem: (1) Its not possible to both set the content and SKIP in the same hook. Two hooks would be needed and that's not elegant. |
This should definitely work for a caching hook. If it is one of the first registered, it can set app.hooks({
async: [
async (context, next) => {
const { app, method, params } = context;
if(method === 'find' || method === 'get') {
const key = JSON.stringify(context.params.query);
const cached = app.cache[key];
if(cached) {
context.result = cached;
// Just return the context, don't call `next`
return context;
}
const { result } = await next();
app.cache[key] = result;
}
return context;
}
]
}); For skipping hooks we could add an additional parameter (similar to the // Return the hook object
app.service('users').get(id, hooks.RETURN);
// Skip all hooks
app.service('users').get(id, hooks.SKIP); To skip specific ones, I'm not sure if there is a way around wrapping them in a conditional hook or having a hook check it themselves. |
I would suggest the cache contain the records returned by the DB call itself. That way the hooks intended after the DB call can customize the response for the specifics of that call, e.g. the role of the user. Perhaps the "after" hooks can do that. |
Its been a while since I looked at Koa, but doesn't its middleware bubble downstream and then bubble upstream? Perhaps you can consider running the async hooks before the 'before" hooks, and before the "after" hooks, and maybe (?) even before the "error" hooks. That would produce a similar downstream/upstream feature. I assume Perhaps |
Maybe it's not clear in the proposal but the idea is that Koa style middleware has access to the whole flow that can be controlled by calling // Before hook
const beforeHook = async (context, next) => {
console.log('Before');
await next();
}
// After hook
const afterHook = async (context, next) => {
await next();
console.log('After');
}
// Error hook
const errorHook = async (context, next) => {
try {
await next();
} catch(error) {
console.error('Error', error.message);
throw error;
}
}
// Before, after and error
const afterHook = async (context, next) => {
try {
console.log('Before');
await next();
console.log('After');
} catch(error) {
console.error('Error', error.message);
throw error;
}
} |
Totally agree with the proposal. One suggestion: instead of using |
BTW it would be nice if in the next major version we gave |
I've been too busy to contribute on ths issue and will continue to be till the 12th. However, as I mentioned above, its important a (async?) hook be able to get/find a record from its own or from another service, without any of the hooks on that service being run. So I'm assuming something like your |
I'm waiting for it impatiently. |
How do you want to proceed to attach hooks directly to methods? The ideal would be to use the services even without app. Does it mean that there will be hooks for every method, every service and every app? |
Yes. The Readme of the prototype has a few examples for this: const { hooks } = require('@feathersjs/hooks');
const addExclamation = context => {
context.result += '!!!';
}
// On a function
const helloWithHook = hooks(async function() {
return 'Hello world';
}, [ addExclamation ]);
console.log(await helloWithHook());
class HelloWorld {
sayHello () {
return 'Hello world';
}
}
// On the class
hooks(HelloWorld, {
sayHello: [ addExclamation ]
});
// On the object
const helloWorlder = new HelloWorld();
hooks(helloWorlder, {
sayHello: [ addExclamation ]
});
// As a JavaScript or TypeScrip decorator
class HelloWorld {
@hooks([ addExclamation ])
sayHello () {
return 'Hello world';
}
} |
Personally most looking forward to the decorator option because you see how everything fits together in a single place: // As a JavaScript or TypeScript decorator
class HelloWorld {
@hooks([
reportErrors,
logRuntime,
addExclamation
])
async sayHello () {
return 'Hello world';
}
} There will probably be a backwards compatible |
I think of something like that, what do you think? This could replace the @hooks([
reportErrors,
logRuntime
])
class HelloWorld {
@hooks([
addExclamation
])
async sayHello () {
return 'Hello world';
}
} |
Absolutely. I have to look at how to make that work with a decorator but I think that's a pretty nice way to see everything that is happening. |
This seems to hide hook type conversions... What if a service hook converts a patch call to an update. App hooks before next would have to be gated somehow... |
I just published the first version of It is not integrated with Feathers core yet but can be used by itself to your ❤️'s content all over your existing applications. |
Best news of 2020, eager to see more. |
My wrappers: function firstHook(context, next) {
context.type = 'before';
return next();
}
function lastHook(context, next) {
context.type = 'after';
return next();
}
function toBeforeHook(hook) {
return async (context, next) => {
await hook(context);
await next();
};
}
function toAfterHook(hook) {
return async (context, next) => {
await next();
await hook(context);
};
}
function beforeWrapper(...hooks) {
return [firstHook, ...hooks.map(toBeforeHook)];
}
function afterWrapper(...hooks) {
return [...hooks.reverse().map(toAfterHook), lastHook];
}
function wrap({ async = [], before = [], after = [] } = {}) {
return [
...[].concat(async),
firstHook,
...[].concat(before).map(toBeforeHook),
...[].concat(after).reverse().map(toAfterHook),
lastHook
];
}
module.exports = {
wrap,
beforeWrapper,
afterWrapper,
toBeforeHook,
toAfterHook
}; |
Hooks are a powerful way to control the flow of asynchronous methods. Koa popularized a new kind of middleware that allows an even more flexible approach to control this flow. I propose to adapt this pattern as a new
async
hook type.Problem
Currently, Feathers hooks are split into three types,
before
,after
anderror
:While this pattern works quite well, it is a little cumbersome to implement functionality that needs to have access to the
before
,after
and/orerror
flow at the same time. For example functionality for a profiler that logs the method runtime or extracting and storing data (e.g. associations) that will be used after the method call returns requires abefore
and anafter
hook.Proposal
With async/await now being fully supported by all recent versions of Node - and the ability to fall back to Promises for Node 6 - we now have a powerful new way to create middleware that can control the whole flow within a single function.
This pattern has been implemented already in KoaJS for processing HTTP requests. However, similar to what I wrote before about Express style middleware, this also doesn't have to be limited to the HTTP request/response cycle but can apply to any asynchronous function, including Feathers services. Basically this new kind of hook will wrap around the following hooks and the service method call like this:
How it works
Additionally to the hook context
async
hooks also get an asynchronousnext
function that controls passing the flow to the next hook in the chain.The following example registers an application wide before, after and error logger and profiler for all service method calls:
The following example extracts the
address
association for a usercreate
method and saves it with theuserId
reference:async
hook typeThe core of
async
hooks would be implemented as an individual module that allows to add hooks to any asynchronous method. This has been inspired by the great work @bertho-zero has been doing in #924 to allow using hooks on other service methods. I created a first prototype in https://github.com/daffl/async-hooks (test case can be found here). It uses Koa's middleware composition module koa-compose.async
hooks would be implemented as an additional hook type but all hook dispatching will be done using the new middleware composition library. Existingbefore
after
anderror
hooks will be wrapped and registered in a backwards compatible way:async
hooks will have precedence so the execution order would beasync -> before -> after
. Even when only using the old hooks there are several advantages:Migration path
There will be two breaking changes:
function(context, next)
signature for existingbefore
,after
anderror
hooks will no longer work.koa-compose
does not allow to replace the entire context. Returning the context is still possible but other hooks have to retrieve it from theawait next();
call withconst ctx = await next();
.The text was updated successfully, but these errors were encountered: