Skip to content

Commit

Permalink
feat: allow having controllers with a singleton lifetime and permit u…
Browse files Browse the repository at this point in the history
…sage without scoped containers in koa state
  • Loading branch information
Dave Goodchild committed Sep 10, 2024
1 parent 59721e5 commit d9863ab
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 15 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,7 @@ That concludes the tutorial! Hope you find it useful, I know I have.
The package exports everything from `awilix-router-core` as well as the following **Koa middleware factories**:

- `scopePerRequest(container)`: creates a scope per request.
- `attachContainer(container)`: permits use of awilix-koa without creating a scope per request.
- `controller(decoratedClassOrController)`: registers routes and delegates to Koa Router.
- `importControllers(router, pattern, opts)`: imports files matching a glob pattern, registers their exports as controllers, applying them to the supplied koa-router
- `loadControllers(pattern, opts, router)`: loads files matching a glob pattern and registers their exports as controllers and returns a middleware for use with Koa
Expand Down
34 changes: 23 additions & 11 deletions src/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,30 @@ export type ConstructorOrControllerBuilder =
| (new (...args: Array<any>) => any)
| IAwilixControllerBuilder

export interface InstanceOptions {
singleton?: boolean
}

/**
* Registers one or multiple decorated controller classes.
*
* @param ControllerClass One or multiple "controller" classes
* with decorators to register
* @param options
*/
export function controller(
ControllerClass:
| ConstructorOrControllerBuilder
| Array<ConstructorOrControllerBuilder>,
options?: InstanceOptions
): Middleware {
const router = new Router()
if (Array.isArray(ControllerClass)) {
ControllerClass.forEach((c) =>
_registerController(router, getStateAndTarget(c)),
_registerController(router, options, getStateAndTarget(c)),
)
} else {
_registerController(router, getStateAndTarget(ControllerClass))
_registerController(router, options, getStateAndTarget(ControllerClass))
}

return compose([router.routes(), router.allowedMethods()]) as any
Expand All @@ -47,53 +53,59 @@ export function controller(
*
* @param router
* @param pattern
* @param opts
* @param globOptions
* @param options
*/
export function importControllers(
router: Router,
pattern: string,
opts?: IOptions,
globOptions?: IOptions,
options?: InstanceOptions
): void {
findControllers(pattern, {
...opts,
...globOptions,
absolute: true,
}).forEach(_registerController.bind(null, router))
}).forEach(_registerController.bind(null, router, options))
}

/**
* Loads controllers for the given pattern and returns a koa-compose'd Middleware
* This return value must be used with `Koa.use`, and is incompatible with `Router.use`
*
* @param pattern
* @param opts
* @param globOptions
* @param router
* @param options
*/
export function loadControllers(
pattern: string,
opts?: IOptions,
globOptions?: IOptions,
router?: Router,
options?: InstanceOptions
): Middleware {
const r = router || new Router()
importControllers(r, pattern, opts)
importControllers(r, pattern, globOptions, options)
return compose([r.routes(), r.allowedMethods()]) as any
}

/**
* Reads the config state and registers the routes in the router.
*
* @param router
* @param options
* @param ControllerClass
*/
function _registerController(
router: Router,
stateAndTarget: IStateAndTarget | null,
options?: InstanceOptions,
stateAndTarget?: IStateAndTarget | null,
): void {
if (!stateAndTarget) {
return
}

const { state, target } = stateAndTarget
const invoker = makeInvoker(target as any)
const invoker = makeInvoker(target as any, { lifetime: options?.singleton ? 'SINGLETON' : 'SCOPED' })
const rolledUp = rollUpState(state)
rolledUp.forEach((methodCfg, methodName) => {
methodCfg.verbs.forEach((httpVerb) => {
Expand Down
27 changes: 23 additions & 4 deletions src/invokers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ export function makeInvoker<T>(
* only parameter, and then call the `methodToInvoke` on
* the result.
*
* @param, {Function} fn
* @param {Function} fn
* @param opts
* @return {(methodToInvoke: string) => (ctx) => void}
*/
export function makeFunctionInvoker<T>(
Expand All @@ -51,7 +52,8 @@ export function makeFunctionInvoker<T>(
/**
* Same as `makeInvoker` but for classes.
*
* @param {Class} Class
* @param {Class} Class
* @param opts
* @return {(methodToInvoke: string) => (ctx) => void}
*/
export function makeClassInvoker<T>(
Expand All @@ -68,10 +70,13 @@ export function makeClassInvoker<T>(
* then call the method on the result, passing in the Koa context
* and `next()`.
*
* @param, {Resolver} resolver
* @param {Resolver} resolver
* @return {(methodToInvoke: string) => (ctx) => void}
*/
export function makeResolverInvoker<T>(resolver: Resolver<T>) {
const singleton = resolver.lifetime === 'SINGLETON'
let _resolved: any

/**
* 2nd step is to create a method to invoke on the result
* of the resolver.
Expand All @@ -89,7 +94,21 @@ export function makeResolverInvoker<T>(resolver: Resolver<T>) {
*/
return function memberInvoker(ctx: any, ...rest: any[]) {
const container: AwilixContainer = ctx.state.container
const resolved: any = container.build(resolver)
if (!container) {
throw new Error('Awilix container not found on Koa state object. Please ensure you use either scopePerRequest or attachContainer')
}

let resolved: any
if (singleton) {
if (!_resolved) {
_resolved = container.build(resolver)
}
resolved = _resolved
}
else {
resolved = container.build(resolver)
}

assert(
methodToInvoke,
`methodToInvoke must be a valid method type, such as string, number or symbol, but was ${String(
Expand Down
19 changes: 19 additions & 0 deletions src/scope-per-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,22 @@ export function scopePerRequest(container: AwilixContainer) {
return next()
}
}

/**
* Koa middleware factory that will simply attach the container
* to the context (ctx) state, with no additional scoping.
*
* You should only use one of either scopePerRequest or attachContainer.
*
* @param {AwilixContainer} container
* @return {Function}
*/
export function attachContainer(container: AwilixContainer) {
return function attachContainerMiddleware(
ctx: any,
next: import('koa').Next,
) {
ctx.state.container = container
return next()
}
}

0 comments on commit d9863ab

Please sign in to comment.