-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
feat(context): add support for method interceptors #2687
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How would the interceptor affect the sequence? will they be executed when invoking the controller function?
They will be invoked by the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the idea of interceptors 👍, and I can see how they can be used, but was wondering how we want to use them with LB4. Are they supposed to represent before/after hooks in LB3 for CRUD methods? Are there some use cases we can show users in the context of LB4?
The interceptors can be used as
They are close to remote hooks. It's also possible for method decorators to return a proxy method. But that won't allow context access for such interceptor functions. |
I am not sure if I'll manage to review this pull request by the end of this week, please give me more time to catch up. I feel this is pull request is adding a fundamental building block that will be difficult to change later, therefore a careful review is needed. |
21b8576
to
b9ced8d
Compare
FYI: I also added an acceptance test for |
9c74ff2
to
3859148
Compare
Implements #133 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I reviewed the documentation and I like the simplicity of the proposed design 💯
(1)
I have one major concern though: you are proposing to apply interceptors on controller methods only. However, LB4 supports other flavors for endpoint implementations, most notable route handler functions:
app.route('get', '/hello', {/*spec*/}, () => 'hello world');
How do you envision applying interceptors on these routes, especially the interceptors registered at global (application) level?I would find it very confusing if global interceptors were applied on a subset of routes only.
(2)
What is our recommendation for extension (component) developers? How should they structure their npm package to contribute interceptors? How are application developers going to consume such interceptors? I'd like to see a new section in Extending LoopBack 4 providing content for extension developers. I think we can also move many advanced details from your original content in Key Concepts to that new page too.
@intercept(logWithErrorHandling) | ||
async helloWithError(name: string) { | ||
throw new Error('error: ' + name); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find this example too long. There are about 10 methods in an unstructured list, many of them are slight variations on the same theme. Could you please pick 3-5 most important cases and convert them into the form of a combination of short explanatory text with a code snippet?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did some refactoring and add more comments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am afraid the changes are not enough. We are writing documentation here, the content should read as well-structured text. It's ok to use code snippets for illustrations, but these snippets should not serve as a replacement for the actual human-readable text.
@raymondfeng please review the code coverage report, there may be valid use cases/scenarios that are not covered by your tests. |
It's now supported. |
33973fa
to
8c9e8dc
Compare
15f0cad
to
cb4a270
Compare
291acca
to
b1437df
Compare
b1437df
to
bfb3646
Compare
@raymondfeng before I review the changes in details, I'd like to reach a shared high-level understanding of how we want to deal with asynchronicity. At high level:
This gives us four combinations:
In theory, the first combination (sync target, sync interceptor) can preserve sync behavior of the invoked operation. The remaining three combinations will always cause the operation to be async. (Assuming interceptors wait for the outcome of the intercepted operations.) As I understand your current proposal, you want to support sync behavior, which is reflected in the Interceptor API:
IMO, this is complicating implementation a lot.
The first point is of lesser importance because interceptors are usually invoked by the framework, thus there are only few places where we need to be careful. I am mostly concerned about the second point, about interceptors that will be written by regular LB users. IMO, most of these developers won't understand delicate details of sync vs. async flavors of interceptors and target methods. In the better case, they will write all interceptors as In that light, I'd like us to reconsider whether the combination of sync target + sync interceptors is so important to justify burden of extra complexity placed on framework users. Personally, I prefer the simplicity of fully-async approach: export interface Interceptor {
(
context: InvocationContext,
next: () => Promise<InvocationResult>,
): Promise <InvocationResult>;
} @strongloop/loopback-maintainers thoughts? |
I'm not against the idea. Here is the caveat for api compatibility:
|
494fafd
to
81469e7
Compare
I was thinking about this problem during the weekend. My proposal has an important downside: it's not forward-compatible. If we ever decide to add support for sync methods & sync interceptors in the future, then such change could break existing code. For example: function myInterceptor(context, next) {
// a dummy logger using Promise API instead of async/await
return next.then(result => {
console.log('result', result);
return result;
});
}
const proxy = // create proxy with interceptors
proxy.someMethod().then(result => {
// a dummy example invoking an intercepted method
// while using Promise API instead of async/await
}); In that light, I think we should preserve the current implementation that supports both sync and async flavors. BUT! I still would like us to make interceptors easier to understand for new LB developers, I think this can be easily achieved by reworking our documentation to start with simplified information showing fully-async flavor only, and then explaining sync versions later in "advanced" section. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Almost there. My main concern is about the sync/async aspect of the Proxy API and the documentation. There are also few older comments that have not been addressed yet.
/** | ||
* A boolean flag to control if a proxy should be created to apply | ||
* interceptors for the resolved value. It's only honored for bindings backed | ||
* by a class. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should let the users know when this flag was ignored because the binding was not backed by a class.
- You can print a
debug
statement, this way we don't pollute console with excessive output.- Alternatively, I am also fine with throwing an error to let the programmer know about the error they made in their code.
I slightly prefer the second option (throw an error).
☝️
toLowerCase(@param.path.string('text') text: string) { | ||
return text.toLowerCase(); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I want to start the app once
What is the reasoning behind the requirement? Can we find a different solution how to move the definition of controllers into individual tests/context blocks?
☝️
Fixed. |
Fixed. |
FYI: I extracted binding sorting functions to #2848. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks mostly good, please consider addressing my comments below.
No further review is necessary as far as I am concerned, but please check with @hacksparrow @jannyHou and @b-admike before landing, they have participated in the review discussion but have not approved the changes yet.
@strongloop/loopback-maintainers I have squashed all commits into one. PTAL before I merge it. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice to see addition of global interceptors, changes LGTM. I like the fact that the README has a lot of information around the features.
- introduce `@intercept` for classes and/or methods - enable `invokeMethod()` to execute interceptors - add support for global interceptors - add acceptance tests to illustrate how to use interceptors - apply global interceptors for handler routes - allow proxies to be created or injected to apply interceptors - update docs for interceptors - introduce AsyncProxy type for proxy with async interceptors - allow global interceptors to be sorted by group
See https://github.com/strongloop/loopback-next/blob/interceptor/docs/site/Interceptors.md
Implements #133
Checklist
👉 Read and sign the CLA (Contributor License Agreement) 👈
npm test
passes on your machinepackages/cli
were updatedexamples/*
were updated