Skip to content
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

Merged
merged 1 commit into from
May 9, 2019
Merged

Conversation

raymondfeng
Copy link
Contributor

@raymondfeng raymondfeng commented Apr 3, 2019

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 machine
  • New tests added or existing tests modified to cover all changes
  • Code conforms with the style guide
  • API Documentation in code was updated
  • Documentation in /docs/site was updated
  • Affected artifact templates in packages/cli were updated
  • Affected example projects in examples/* were updated

@raymondfeng raymondfeng requested a review from bajtos as a code owner April 3, 2019 19:26
@raymondfeng raymondfeng added developer-experience Issues affecting ease of use and overall experience of LB users feature IoC/Context @loopback/context: Dependency Injection, Inversion of Control labels Apr 3, 2019
Copy link
Contributor

@jannyHou jannyHou left a 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?

@raymondfeng
Copy link
Contributor Author

How would the interceptor affect the sequence? will they be executed when invoking the controller function?

They will be invoked by the InvokeMethod action. The hook is on the invokeMethod().

Copy link
Contributor

@b-admike b-admike left a 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?

docs/site/Interceptors.md Outdated Show resolved Hide resolved
docs/site/Interceptors.md Outdated Show resolved Hide resolved
@raymondfeng
Copy link
Contributor Author

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 around middleware at per method level. Typical usages:

  1. Implement caching
  2. Add logging/monitoring/..
  3. Implement authorization
  4. Enforce parameter validation
  5. Transform input/output

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.

@bajtos
Copy link
Member

bajtos commented Apr 4, 2019

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.

@raymondfeng raymondfeng force-pushed the interceptor branch 5 times, most recently from 21b8576 to b9ced8d Compare April 5, 2019 17:25
@raymondfeng
Copy link
Contributor Author

FYI: I also added an acceptance test for @loopback/rest to illustrate a mock-up caching interceptor.

@raymondfeng raymondfeng force-pushed the interceptor branch 2 times, most recently from 9c74ff2 to 3859148 Compare April 7, 2019 16:07
@raymondfeng
Copy link
Contributor Author

Implements #133

Copy link
Member

@bajtos bajtos left a 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.

docs/site/Interceptors.md Outdated Show resolved Hide resolved
@intercept(logWithErrorHandling)
async helloWithError(name: string) {
throw new Error('error: ' + name);
}
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member

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.

docs/site/Interceptors.md Show resolved Hide resolved
docs/site/Interceptors.md Outdated Show resolved Hide resolved
docs/site/Interceptors.md Outdated Show resolved Hide resolved
docs/site/Interceptors.md Outdated Show resolved Hide resolved
docs/site/Interceptors.md Outdated Show resolved Hide resolved
docs/site/Interceptors.md Outdated Show resolved Hide resolved
@bajtos
Copy link
Member

bajtos commented Apr 8, 2019

coverage/coveralls — Coverage decreased (-0.03%) to 91.018%

@raymondfeng please review the code coverage report, there may be valid use cases/scenarios that are not covered by your tests.

@raymondfeng
Copy link
Contributor Author

(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:

It's now supported.

@raymondfeng raymondfeng force-pushed the interceptor branch 3 times, most recently from 15f0cad to cb4a270 Compare April 19, 2019 22:01
@raymondfeng raymondfeng force-pushed the interceptor branch 2 times, most recently from 291acca to b1437df Compare April 23, 2019 16:50
@bajtos
Copy link
Member

bajtos commented May 3, 2019

@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:

  1. The target function (intercepted controller method) can be sync or async.
  2. The interceptor can be sync or async.

This gives us four combinations:

  1. sync target, sync interceptor
  2. sync target, async interceptor
  3. async target, sync interceptor (caveat: even if the interceptor is sync in principle, the outcome is async because next returns a promise)
  4. async target, async interceptor

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:

  • The interceptor returns ValueOrPromise (i.e. can return synchronously, but does not have to!)
  • The next callback returns ValueOrPromise (i.e. can return synchronously, but does not have to!)

IMO, this is complicating implementation a lot.

  1. The code calling an interceptor (or a chain of interceptors for a method) cannot tell in advance whether the outcome will be sync or async and thus must be prepared to handle both cases.
  2. Individual interceptor implementations cannot make any assumption on whether next is going to return the result (sync) or a promise (async), they must be prepared to handle both cases.

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 async functions and thus prevent the framework from leveraging benefits of sync interception. Worse, they will try to write their interceptor to support both sync & async workflows and possibly end up with complex code that does not work as expected.

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?

@raymondfeng
Copy link
Contributor Author

Personally, I prefer the simplicity of fully-async approach

I'm not against the idea. Here is the caveat for api compatibility:

  1. Currently invokeMethod allows both sync/async
  2. The PR improved invokeMethod to always apply interceptors if present. As a result, invokeMethod does not allow sync any more.

@raymondfeng raymondfeng force-pushed the interceptor branch 2 times, most recently from 494fafd to 81469e7 Compare May 3, 2019 15:43
@bajtos
Copy link
Member

bajtos commented May 6, 2019

Personally, I prefer the simplicity of fully-async approach

export interface Interceptor {
 (
   context: InvocationContext,
    next: () => Promise<InvocationResult>,
 ): Promise <InvocationResult>;
}

I'm not against the idea. Here is the caveat for api compatibility:

Currently invokeMethod allows both sync/async
The PR improved invokeMethod to always apply interceptors if present. As a result, invokeMethod does not allow sync any more.

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.

Copy link
Member

@bajtos bajtos left a 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.

docs/site/Interceptors.md Outdated Show resolved Hide resolved
docs/site/Interceptors.md Show resolved Hide resolved
docs/site/Interceptors.md Outdated Show resolved Hide resolved
docs/site/Interceptors.md Outdated Show resolved Hide resolved
docs/site/Interceptors.md Show resolved Hide resolved
packages/context/src/interception-proxy.ts Outdated Show resolved Hide resolved
/**
* 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.
Copy link
Member

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.

  1. You can print a debug statement, this way we don't pollute console with excessive output.
  2. 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();
}
}
Copy link
Member

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?

☝️

@raymondfeng
Copy link
Contributor Author

I slightly prefer the second option (throw an error).

Fixed.

@raymondfeng
Copy link
Contributor Author

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.

@raymondfeng raymondfeng requested a review from bajtos May 6, 2019 16:22
@raymondfeng
Copy link
Contributor Author

FYI: I extracted binding sorting functions to #2848.

Copy link
Member

@bajtos bajtos left a 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.

docs/site/Interceptors.md Outdated Show resolved Hide resolved
packages/context/src/interception-proxy.ts Outdated Show resolved Hide resolved
@raymondfeng
Copy link
Contributor Author

@strongloop/loopback-maintainers I have squashed all commits into one. PTAL before I merge it.

Copy link
Contributor

@b-admike b-admike left a 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.

packages/context/src/interceptor.ts Show resolved Hide resolved
docs/site/Interceptors.md Outdated Show resolved Hide resolved
- 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
developer-experience Issues affecting ease of use and overall experience of LB users feature IoC/Context @loopback/context: Dependency Injection, Inversion of Control
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants