-
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(rest): enable Express middleware to be used as interceptors with LoopBack #5118
Conversation
009cbc0
to
a5911ca
Compare
docs/site/Express-middleware.md
Outdated
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 |
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.
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.
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 can improve it once the apidocs is live.
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.
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.
): GenericInterceptor<CTX> { | ||
let factory: MiddlewareFactory<CFG>; | ||
if (typeof middleware === 'string') { | ||
factory = require(middleware); |
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.
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?
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.
- Support for middleware module as
string
is removed. - Default
InvokeMiddleware
is set to no-op. - Caching is handled at binding scope level.
@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? |
928bc0b
to
6f5f5e7
Compare
6f5f5e7
to
e1171af
Compare
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.
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); |
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.
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.
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.
Yes.
/** | ||
* An Express server that provides middleware composition and injection | ||
*/ | ||
export class ExpressServer extends MiddlewareRegistry implements Server { |
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.
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.
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 could make MiddlewareRegistry
a mixin if needed.
/** | ||
* A LoopBack application with Express server | ||
*/ | ||
export class ExpressApplication extends Application { |
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.
+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) => { |
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.
Any advantage we have, by this being an independent package ?
c97955f
to
2539258
Compare
0059ac4
to
954ada9
Compare
570a30c
to
abc2ee8
Compare
…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`.
abc2ee8
to
7970266
Compare
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.
👍 Awesome! LGTM.
Depends on #5317
Implements #2035
Supersedes #2434
This PR adds ability to use Express middleware with LoopBack 4 as follows:
Middleware
step. It creates an invocation chain to call registered middleware handlers via extension point/extension pattern.Middleware
steps.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:
Checklist
👉 Read and sign the CLA (Contributor License Agreement) 👈
npm test
passes on your machinepackages/cli
were updatedexamples/*
were updated👉 Check out how to submit a PR 👈