Skip to content

Commit 382c246

Browse files
committed
feat: enhance plugin system to include request context in bindings
1 parent ddd8660 commit 382c246

3 files changed

Lines changed: 144 additions & 33 deletions

File tree

src/core/plugins.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,31 @@
11
import { BindToken, BindValue, Container } from './bindings'
2+
3+
import type { Request as CoreRequest } from './Request'
4+
import type { Response as CoreResponse } from './Response'
25
import type { RouterConfig } from 'types/basic'
36

47
export type PluginSetupResult = void | Promise<void>
58

6-
export type PluginBind = <T>(token: BindToken<T>, value: BindValue<T>) => void
9+
export interface ClearRouterPluginRequestContext {
10+
ctx: any
11+
request: CoreRequest
12+
response: CoreResponse
13+
[key: string]: any
14+
}
15+
16+
export type PluginBindFactory<T = any> = (ctx: ClearRouterPluginRequestContext) => T | Promise<T>
17+
export type PluginBindValue<T = any> = BindValue<T> | PluginBindFactory<T>
18+
export type PluginBind = <T>(token: BindToken<T>, value: PluginBindValue<T>) => void
719

820
export interface ClearRouterPluginContext<Options = any> {
921
container: typeof Container
1022
bind: PluginBind
1123
configure: (options: RouterConfig) => void
1224
configureDefaults: (options: RouterConfig) => void
25+
readonly request?: CoreRequest
26+
readonly response?: CoreResponse
27+
getRequest: () => CoreRequest | undefined
28+
getResponse: () => CoreResponse | undefined
1329
options: Options
1430
}
1531

src/core/router.ts

Lines changed: 76 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ApiResourceMiddleware, ControllerAction, HttpMethod, RouterConfig } from 'types/basic'
2-
import type { ClearRouterPluginContext, ClearRouterPluginInput } from './plugins'
3-
import { Container, getBindingMetadataFromTargets, getDesignParamTypes, getStandardMetadata } from './bindings'
2+
import type { ClearRouterPluginContext, ClearRouterPluginInput, ClearRouterPluginRequestContext, PluginBind } from './plugins'
3+
import { Container, getBindingMetadataFromTargets, getDesignParamTypes, getStandardMetadata, isClass } from './bindings'
44

55
import { AsyncLocalStorage } from 'node:async_hooks'
66
import { Controller } from 'src/Controller'
@@ -266,9 +266,17 @@ export abstract class CoreRouter {
266266
const setup = async (): Promise<void> => {
267267
const ctx: ClearRouterPluginContext<Options> = {
268268
container: Container,
269-
bind: Container.bind.bind(Container),
269+
bind: this.createPluginBind(),
270270
configure: this.configure.bind(this),
271271
configureDefaults: this.configureDefaults.bind(this),
272+
get request () {
273+
return this.getRequest()
274+
},
275+
get response () {
276+
return this.getResponse()
277+
},
278+
getRequest: () => this.getCurrentPluginRequestContext()?.request,
279+
getResponse: () => this.getCurrentPluginRequestContext()?.response,
272280
options: options as Options,
273281
}
274282

@@ -307,6 +315,7 @@ export abstract class CoreRouter {
307315
prefix: string
308316
groupMiddlewares: any[]
309317
}>()
318+
protected static pluginRequestContext = new AsyncLocalStorage<ClearRouterPluginRequestContext>()
310319

311320
static routes: Array<Route<any, any, any>> = []
312321
static routesByPathMethod: Record<string, Route<any, any, any>> = {}
@@ -316,6 +325,39 @@ export abstract class CoreRouter {
316325
static groupMiddlewares: any[] = []
317326
static globalMiddlewares: any[] = []
318327

328+
protected static getCurrentPluginRequestContext (): ClearRouterPluginRequestContext | undefined {
329+
return this.pluginRequestContext.getStore()
330+
}
331+
332+
protected static createPluginRequestContext (ctx: any): ClearRouterPluginRequestContext {
333+
const request: CoreRequest = ctx.clearRequest
334+
const response: CoreResponse = ctx.clearResponse
335+
336+
return {
337+
...ctx,
338+
ctx,
339+
request,
340+
response,
341+
clearRequest: request,
342+
clearResponse: response,
343+
}
344+
}
345+
346+
protected static createPluginBind (): PluginBind {
347+
const bind: PluginBind = (token, value): void => {
348+
if (typeof value === 'function' && !isClass(value)) {
349+
const factory = value as (ctx: ClearRouterPluginRequestContext) => any
350+
Container.bind(token, (ctx: any) => factory(this.createPluginRequestContext(ctx)))
351+
352+
return
353+
}
354+
355+
Container.bind(token, value)
356+
}
357+
358+
return bind
359+
}
360+
319361
protected static ensureState (this: any): void {
320362
this.bindStateAccessors()
321363

@@ -818,42 +860,44 @@ export abstract class CoreRouter {
818860
bindingHandler?: object,
819861
bindingMetadata?: object
820862
): Promise<any> {
821-
await this.pluginsReady()
822-
823-
if (!this.config.container?.enabled) {
824-
return handlerFunction(ctx, ctx.clearRequest)
825-
}
863+
return this.pluginRequestContext.run(this.createPluginRequestContext(ctx), async () => {
864+
await this.pluginsReady()
826865

827-
const metadata = getBindingMetadataFromTargets([
828-
{ target: bindingTarget, propertyKey: bindingMethod },
829-
{ target: bindingHandler },
830-
{ target: bindingTarget, propertyKey: '__class__' },
831-
]) ?? getStandardMetadata(bindingMetadata, bindingMethod)
832-
?? getStandardMetadata(bindingMetadata, '__class__')
833-
if (!metadata) {
834-
return handlerFunction(ctx, ctx.clearRequest)
835-
}
866+
if (!this.config.container?.enabled) {
867+
return handlerFunction(ctx, ctx.clearRequest)
868+
}
836869

837-
const designTokens = [
838-
...(bindingTarget ? getDesignParamTypes(bindingTarget, bindingMethod) : []),
839-
...(bindingHandler ? getDesignParamTypes(bindingHandler) : []),
840-
]
841-
const tokens = metadata.tokens?.length ? metadata.tokens : designTokens
842-
if (!tokens.length) {
843-
return handlerFunction(ctx, ctx.clearRequest)
844-
}
870+
const metadata = getBindingMetadataFromTargets([
871+
{ target: bindingTarget, propertyKey: bindingMethod },
872+
{ target: bindingHandler },
873+
{ target: bindingTarget, propertyKey: '__class__' },
874+
]) ?? getStandardMetadata(bindingMetadata, bindingMethod)
875+
?? getStandardMetadata(bindingMetadata, '__class__')
876+
if (!metadata) {
877+
return handlerFunction(ctx, ctx.clearRequest)
878+
}
845879

846-
const args = []
847-
for (const token of tokens) {
848-
const resolved = await Container.resolve(token, ctx, Boolean(this.config.container?.autoDiscover))
849-
if (typeof resolved === 'undefined') {
880+
const designTokens = [
881+
...(bindingTarget ? getDesignParamTypes(bindingTarget, bindingMethod) : []),
882+
...(bindingHandler ? getDesignParamTypes(bindingHandler) : []),
883+
]
884+
const tokens = metadata.tokens?.length ? metadata.tokens : designTokens
885+
if (!tokens.length) {
850886
return handlerFunction(ctx, ctx.clearRequest)
851887
}
852888

853-
args.push(resolved)
854-
}
889+
const args = []
890+
for (const token of tokens) {
891+
const resolved = await Container.resolve(token, ctx, Boolean(this.config.container?.autoDiscover))
892+
if (typeof resolved === 'undefined') {
893+
return handlerFunction(ctx, ctx.clearRequest)
894+
}
855895

856-
return (handlerFunction as any)(...args)
896+
args.push(resolved)
897+
}
898+
899+
return (handlerFunction as any)(...args)
900+
})
857901
}
858902

859903
protected static bindRequestToInstance (

tests/express.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,57 @@ describe('Express App (JS)', () => {
372372
.expect({ id: '135', audit: 'async-plugin' })
373373
})
374374

375+
it('passes the current Request instance to plugin bindings', async () => {
376+
class RequestAuditService {
377+
constructor(
378+
readonly id: string,
379+
readonly method: string,
380+
) { }
381+
}
382+
383+
const requestAuditPlugin = definePlugin({
384+
name: 'test-request-aware-plugin',
385+
setup ({ bind, getRequest }) {
386+
expect(getRequest()).toBeUndefined()
387+
388+
bind(RequestAuditService, (d: { request: ClearRouterRequest }) => {
389+
return new RequestAuditService(
390+
d.request.param('id'),
391+
d.request.method,
392+
)
393+
})
394+
},
395+
})
396+
397+
class PluginUsersController {
398+
@Bind(RequestAuditService)
399+
show (audit: RequestAuditService) {
400+
return {
401+
id: audit.id,
402+
method: audit.method,
403+
}
404+
}
405+
}
406+
407+
Router.configure({
408+
container: {
409+
enabled: true,
410+
},
411+
})
412+
Router.use(requestAuditPlugin)
413+
Router.put('/api/request-plugin/:id', [PluginUsersController, 'show'])
414+
415+
await setupApp()
416+
417+
await request(app)
418+
.put('/api/request-plugin/864')
419+
.expect(200)
420+
.expect({
421+
id: '864',
422+
method: 'PUT',
423+
})
424+
})
425+
375426
it('falls back to the default handler signature when binding is disabled', async () => {
376427
class BoundUsersController {
377428
@Bind(ClearRouterRequest)

0 commit comments

Comments
 (0)