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(rest): enable Express middleware to be used as interceptors with LoopBack #5118

Merged
merged 5 commits into from
May 5, 2020

Conversation

raymondfeng
Copy link
Contributor

@raymondfeng raymondfeng commented Apr 16, 2020

Depends on #5317
Implements #2035
Supersedes #2434

This PR adds ability to use Express middleware with LoopBack 4 as follows:

  1. The default sequence now has Middleware step. It creates an invocation chain to call registered middleware handlers via extension point/extension pattern.
  2. The sequence can be customized to have more than one Middleware steps.
  3. Express middleware can also be wrapped as LB4 interceptors, which can in turn be added to global/class/method level.
  4. Move built-in cors and openapi endpoints to middleware action.

restServer -> sequence (middleware, findRoute, ..., invoke, send) --> global middleware interceptor --> class/method level interceptor.

Some of the ideas are inspired by https://github.com/codejamninja/lb4-middleware. Thanks @codejamninja and @victor-enogwe

See docs at:

middleware

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

👉 Check out how to submit a PR 👈

@raymondfeng raymondfeng self-assigned this Apr 16, 2020
@raymondfeng raymondfeng added the REST Issues related to @loopback/rest package and REST transport in general label Apr 16, 2020
We create a few helper functions that wraps an Express middleware module into an
LoopBack 4 interceptor.

### Create an interceptor from Express middleware module name or factory function
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this file will be a reference doc, we should prioritize on user API documentation. Including implementation details should be an added bonus, we could even omit it, IMO.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can improve it once the apidocs is live.

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.

Great initiative! I appreciate that your new design is addressing the concerns I described in #2434 (comment) and #2434 (review).

I don't have bandwidth to review this proposal in full, so I am posting just few comments on things that caught my eye when skimming through the docs.

docs/site/Express-middleware.md Outdated Show resolved Hide resolved
packages/rest/package.json Outdated Show resolved Hide resolved
): GenericInterceptor<CTX> {
let factory: MiddlewareFactory<CFG>;
if (typeof middleware === 'string') {
factory = require(middleware);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please note this require call will resolve the path relatively to the location of @loopback/rest module, not relatively to the code calling createInterceptor. This creates confusing behavior when createInterceptor is called e.g. from a 3rd-party package that's symlinked into node_modules tree.

We have been bitten by this problem multiple times in the past. While there are ways how to work around it, I feel the complexity is not worth the benefits.

Can you please remove the string-based variant and always require the factory function instead?

Also what if the middleware does not provide any factory function and all the users have is a regular function (req, res, next) => void? Are they supposed to wrap that function into a factory just for the sake of satisfying this API?

Last but not least, how do you envision caching the instances of middleware between requests? It's important to call middleware factory function only once at app startup, we must not call it for every incoming request. I don't yet fully comprehend how is createInterceptor going to be used, so maybe this is not a problem? IDK.

I also feel this function is violating Single Responsibility Principle, because it has two responsibilities:

  • Obtain an instance of middleware handler.
  • Convert a middleware handler to an interceptor.

Have you considering removing the first responsibility and modifying the function to accept the middleware handler only?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Support for middleware module as string is removed.
  • Default InvokeMiddleware is set to no-op.
  • Caching is handled at binding scope level.

@bajtos
Copy link
Member

bajtos commented Apr 16, 2020

@raymondfeng this is a sizeable change with major design implications, can you please work with @dhmlau to create space in our plan to give us all enough time to review the proposal?

@raymondfeng raymondfeng force-pushed the middleware-interceptor branch 5 times, most recently from 928bc0b to 6f5f5e7 Compare April 16, 2020 19:56
@dhmlau dhmlau added this to the April 2020 milestone Apr 16, 2020
Copy link
Contributor

@deepakrkris deepakrkris left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@@ -95,6 +103,8 @@ export class DefaultSequence implements SequenceHandler {
async handle(context: RequestContext): Promise<void> {
try {
const {request, response} = context;
// Invoke registered Express middleware
await this.invokeMiddleware(context);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have interceptors for each step in the default sequence, like before and after for find route, parse params, invoke, etc, similar to how we had phases in loopback 3 ( initial -> session -> auth -> parse -> routes -> files -> final ) . So, users dont have to write their own sequence class every time just for adding an authentication step.
The default sequence then becomes more reusable, than just being a template.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.

/**
* An Express server that provides middleware composition and injection
*/
export class ExpressServer extends MiddlewareRegistry implements Server {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is more intuitive to think ExpressServer as an extension of RestServer and a mixin of MiddlewareRegistry. Also then we could change any existing LB4 app as an express app without the need for composition with multiple apps. This could become a great advantage for having LB4 web apps, and that opens a lot of use cases for LB4.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could make MiddlewareRegistry a mixin if needed.

/**
* A LoopBack application with Express server
*/
export class ExpressApplication extends Application {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, this makes sense extending ExpressApplication from Application, similarly can we extend ExpressServer from RestServer, so that any existing LB4 app can become an express app.

}
}
const binding = this.app.controller(MyController);
this.app.expressServer.expressApp.post('/hello', async (req, res, next) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any advantage we have, by this being an independent package ?

@dhmlau dhmlau modified the milestones: April 2020, May 2020 May 2, 2020
@raymondfeng raymondfeng force-pushed the middleware-interceptor branch 3 times, most recently from 0059ac4 to 954ada9 Compare May 3, 2020 17:30
@raymondfeng raymondfeng force-pushed the middleware-interceptor branch 4 times, most recently from 570a30c to abc2ee8 Compare May 4, 2020 15:45
…ware

- add `Middleware` as interceptors that use request context
- add `InvokeMiddleware` to invoke middleware in a chain
- add extension points for Middleware extensions
- add helper methods to adapt express middleware as interceptors
- add helper methods to adapt LoopBack middleware chain as Express middleware
- add ExpressServer and ExpressApplication to extend Express with LoopBack core
- allow dynamic configuration changes for Express middleware
- allow `toInterceptor` to take one or more Express middleware handlers
- add MiddlewareMixin
- make namespaces consistent and simplify chain invocations
- add `InvokeMiddleware` action to the default sequence
- add `middleware/expressMiddleware` APIs to RestServer/RestApplication
- use middleware APIs to register CORS and OpenAPI spec endpoints

BREAKING CHANGE: `basePath` now also applies to endpoints that serve OpenAPI
specs. For example, the OpenAPI specs are available at `/api/openapi.json`
or `/api/openapi.yaml` if the base path is set to `/api`.
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.

👍 Awesome! LGTM.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
REST Issues related to @loopback/rest package and REST transport in general
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants