From 9747d4065916657abc55ebb25e9cd9720b029ed8 Mon Sep 17 00:00:00 2001 From: garrettmills Date: Mon, 8 Mar 2021 11:08:56 -0600 Subject: [PATCH] Add logic for resolving route handlers --- src/http/Controller.ts | 11 +++++- src/http/routing/ActivatedRoute.ts | 4 +- src/http/routing/Route.ts | 55 +++++++++++++++++++++++++++- src/lifecycle/AppClass.ts | 6 ++- src/service/Canonical.ts | 2 +- src/service/CanonicalInstantiable.ts | 4 +- src/service/CanonicalStatic.ts | 4 +- src/service/Controllers.ts | 8 ++-- 8 files changed, 80 insertions(+), 14 deletions(-) diff --git a/src/http/Controller.ts b/src/http/Controller.ts index 588cb05..f0aa014 100644 --- a/src/http/Controller.ts +++ b/src/http/Controller.ts @@ -1,3 +1,12 @@ import {AppClass} from "../lifecycle/AppClass"; +import {Request} from "./lifecycle/Request"; -export class Controller extends AppClass {} +export class Controller extends AppClass { + constructor( + protected readonly request: Request + ) { super() } + + protected container() { + return this.request + } +} diff --git a/src/http/routing/ActivatedRoute.ts b/src/http/routing/ActivatedRoute.ts index 46cf713..a340f14 100644 --- a/src/http/routing/ActivatedRoute.ts +++ b/src/http/routing/ActivatedRoute.ts @@ -1,8 +1,9 @@ import {ErrorWithContext} from "@extollo/util"; -import {Route} from "./Route"; +import {ResolvedRouteHandler, Route} from "./Route"; export class ActivatedRoute { public readonly params: {[key: string]: string} + public readonly handler: ResolvedRouteHandler constructor( public readonly route: Route, @@ -19,5 +20,6 @@ export class ActivatedRoute { } this.params = params + this.handler = route.resolveHandler() } } diff --git a/src/http/routing/Route.ts b/src/http/routing/Route.ts index fc64add..f120e06 100644 --- a/src/http/routing/Route.ts +++ b/src/http/routing/Route.ts @@ -2,8 +2,16 @@ import {AppClass} from "../../lifecycle/AppClass"; import {HTTPMethod, Request} from "../lifecycle/Request"; import {Application} from "../../lifecycle/Application"; import {RouteGroup} from "./RouteGroup"; +import {ResponseFactory} from "../response/ResponseFactory"; +import {Response} from "../lifecycle/Response"; +import {Controllers} from "../../service/Controllers"; +import {ErrorWithContext} from "@extollo/util"; +import {Controller} from "../Controller"; + +export type ResponseObject = ResponseFactory | string | number | void | any | Promise +export type RouteHandler = ((request: Request, response: Response) => ResponseObject) | ((request: Request) => ResponseObject) | (() => ResponseObject) | string +export type ResolvedRouteHandler = (request: Request) => ResponseObject -export type RouteHandler = (request: Request) => void | Promise // FIXME want to do some improvements here // TODO middleware, domains, named routes - support this on groups as well @@ -29,6 +37,8 @@ export class Route extends AppClass { for ( const group of stack ) { route.prepend(group.prefix) } + + route.resolveHandler() // Try to resolve here to catch any errors at boot-time } for ( const group of registeredGroups ) { @@ -82,7 +92,7 @@ export class Route extends AppClass { constructor( protected method: HTTPMethod | HTTPMethod[], - protected handler: RouteHandler, + protected readonly handler: RouteHandler, protected route: string ) { super() } @@ -128,6 +138,47 @@ export class Route extends AppClass { return params } + public resolveHandler(): ResolvedRouteHandler { + if ( typeof this.handler !== 'string' ) { + return (request: Request) => { + // @ts-ignore + return this.handler(request, request.response) + } + } else { + const parts = this.handler.split('.') + if ( parts.length < 1 ) { + const e = new ErrorWithContext('Route handler does not specify a method name.') + e.context = { + handler: this.handler + } + throw e + } + + const [controllerName, methodName] = parts + + const controllersService = this.make(Controllers) + const controllerClass = controllersService.get(controllerName) + if ( !controllerClass ) { + const e = new ErrorWithContext('Controller not found for route handler.') + e.context = { + handler: this.handler, + controllerName, + methodName, + } + throw e + } + + return (request: Request) => { + // If not a function, then we got a string reference to a controller method + // So, we need to use the request container to instantiate the controller + // and bind the method + const controller = request.make(controllerClass, request) + const method = controller.getBoundMethod(methodName) + return method() + } + } + } + private prepend(prefix: string) { if ( !prefix.endsWith('/') ) prefix = `${prefix}/` if ( this.route.startsWith('/') ) this.route = this.route.substring(1) diff --git a/src/lifecycle/AppClass.ts b/src/lifecycle/AppClass.ts index 9afa060..5f4f769 100644 --- a/src/lifecycle/AppClass.ts +++ b/src/lifecycle/AppClass.ts @@ -1,5 +1,5 @@ import {Application} from './Application'; -import {Container} from "@extollo/di"; +import {Container, DependencyKey} from "@extollo/di"; /** * Base type for a class that supports binding methods by string. @@ -37,6 +37,10 @@ export class AppClass { return this.appClassApplication; } + protected make(target: DependencyKey, ...parameters: any[]): T { + return this.container().make(target, ...parameters) + } + /** * Get the method with the given name from this class, bound to this class. * @param {string} methodName diff --git a/src/service/Canonical.ts b/src/service/Canonical.ts index fadbdc5..afc2170 100644 --- a/src/service/Canonical.ts +++ b/src/service/Canonical.ts @@ -105,7 +105,7 @@ export abstract class Canonical extends Unit { } public async initCanonicalItem(definition: CanonicalDefinition): Promise { - return definition.imported.default ?? definition.imported[definition.canonicalName] + return definition.imported.default ?? definition.imported[definition.canonicalName.split(':').reverse()[0]] } protected async buildCanonicalDefinition(filePath: string): Promise { diff --git a/src/service/CanonicalInstantiable.ts b/src/service/CanonicalInstantiable.ts index 4210150..d887914 100644 --- a/src/service/CanonicalInstantiable.ts +++ b/src/service/CanonicalInstantiable.ts @@ -17,8 +17,8 @@ export class CanonicalInstantiable extends Canonical> { return this.app().make(definition.imported.default) } - if ( isInstantiable(definition.imported[definition.canonicalName]) ) { - return this.app().make(definition.imported[definition.canonicalName]) + if ( isInstantiable(definition.imported[definition.canonicalName.split(':').reverse()[0]]) ) { + return this.app().make(definition.imported[definition.canonicalName.split(':').reverse()[0]]) } throw new InvalidCanonicalExportError(definition.originalName) diff --git a/src/service/CanonicalStatic.ts b/src/service/CanonicalStatic.ts index 37e844b..ffbfaed 100644 --- a/src/service/CanonicalStatic.ts +++ b/src/service/CanonicalStatic.ts @@ -8,8 +8,8 @@ export class CanonicalStatic extends Canonical> { return definition.imported.default } - if ( isStaticClass(definition.imported[definition.canonicalName]) ) { - return definition.imported[definition.canonicalName] + if ( isStaticClass(definition.imported[definition.canonicalName.split(':').reverse()[0]]) ) { + return definition.imported[definition.canonicalName.split(':').reverse()[0]] } throw new InvalidCanonicalExportError(definition.originalName) diff --git a/src/service/Controllers.ts b/src/service/Controllers.ts index 4319494..bd76d19 100644 --- a/src/service/Controllers.ts +++ b/src/service/Controllers.ts @@ -1,17 +1,17 @@ -import {CanonicalInstantiable} from "./CanonicalInstantiable"; -import {Singleton} from "@extollo/di"; +import {CanonicalStatic} from "./CanonicalStatic"; +import {Singleton, Instantiable} from "@extollo/di"; import {Controller} from "../http/Controller"; import {CanonicalDefinition} from "./Canonical"; @Singleton() -export class Controllers extends CanonicalInstantiable { +export class Controllers extends CanonicalStatic, Controller> { protected appPath = ['http', 'controllers'] protected canonicalItem = 'controller' protected suffix = '.controller.js' public async initCanonicalItem(definition: CanonicalDefinition) { const item = await super.initCanonicalItem(definition) - if ( !(item instanceof Controller) ) { + if ( !(item.prototype instanceof Controller) ) { throw new TypeError(`Invalid controller definition: ${definition.originalName}. Controllers must extend from @extollo/lib.http.Controller.`) }