Add logic for resolving route handlers

This commit is contained in:
Garrett Mills 2021-03-08 11:08:56 -06:00
parent a9ffa771dc
commit 9747d40659
Signed by: garrettmills
GPG Key ID: D2BF5FBA8298F246
8 changed files with 80 additions and 14 deletions

View File

@ -1,3 +1,12 @@
import {AppClass} from "../lifecycle/AppClass"; 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
}
}

View File

@ -1,8 +1,9 @@
import {ErrorWithContext} from "@extollo/util"; import {ErrorWithContext} from "@extollo/util";
import {Route} from "./Route"; import {ResolvedRouteHandler, Route} from "./Route";
export class ActivatedRoute { export class ActivatedRoute {
public readonly params: {[key: string]: string} public readonly params: {[key: string]: string}
public readonly handler: ResolvedRouteHandler
constructor( constructor(
public readonly route: Route, public readonly route: Route,
@ -19,5 +20,6 @@ export class ActivatedRoute {
} }
this.params = params this.params = params
this.handler = route.resolveHandler()
} }
} }

View File

@ -2,8 +2,16 @@ import {AppClass} from "../../lifecycle/AppClass";
import {HTTPMethod, Request} from "../lifecycle/Request"; import {HTTPMethod, Request} from "../lifecycle/Request";
import {Application} from "../../lifecycle/Application"; import {Application} from "../../lifecycle/Application";
import {RouteGroup} from "./RouteGroup"; 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<ResponseObject>
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<void> // FIXME want to do some improvements here
// TODO middleware, domains, named routes - support this on groups as well // 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 ) { for ( const group of stack ) {
route.prepend(group.prefix) route.prepend(group.prefix)
} }
route.resolveHandler() // Try to resolve here to catch any errors at boot-time
} }
for ( const group of registeredGroups ) { for ( const group of registeredGroups ) {
@ -82,7 +92,7 @@ export class Route extends AppClass {
constructor( constructor(
protected method: HTTPMethod | HTTPMethod[], protected method: HTTPMethod | HTTPMethod[],
protected handler: RouteHandler, protected readonly handler: RouteHandler,
protected route: string protected route: string
) { super() } ) { super() }
@ -128,6 +138,47 @@ export class Route extends AppClass {
return params 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 = <Controllers> 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 = <Controller> request.make(controllerClass, request)
const method = controller.getBoundMethod(methodName)
return method()
}
}
}
private prepend(prefix: string) { private prepend(prefix: string) {
if ( !prefix.endsWith('/') ) prefix = `${prefix}/` if ( !prefix.endsWith('/') ) prefix = `${prefix}/`
if ( this.route.startsWith('/') ) this.route = this.route.substring(1) if ( this.route.startsWith('/') ) this.route = this.route.substring(1)

View File

@ -1,5 +1,5 @@
import {Application} from './Application'; 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. * Base type for a class that supports binding methods by string.
@ -37,6 +37,10 @@ export class AppClass {
return this.appClassApplication; return this.appClassApplication;
} }
protected make<T>(target: DependencyKey, ...parameters: any[]): T {
return this.container().make<T>(target, ...parameters)
}
/** /**
* Get the method with the given name from this class, bound to this class. * Get the method with the given name from this class, bound to this class.
* @param {string} methodName * @param {string} methodName

View File

@ -105,7 +105,7 @@ export abstract class Canonical<T> extends Unit {
} }
public async initCanonicalItem(definition: CanonicalDefinition): Promise<T> { public async initCanonicalItem(definition: CanonicalDefinition): Promise<T> {
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<CanonicalDefinition> { protected async buildCanonicalDefinition(filePath: string): Promise<CanonicalDefinition> {

View File

@ -17,8 +17,8 @@ export class CanonicalInstantiable<T> extends Canonical<Instantiable<T>> {
return this.app().make(definition.imported.default) return this.app().make(definition.imported.default)
} }
if ( isInstantiable(definition.imported[definition.canonicalName]) ) { if ( isInstantiable(definition.imported[definition.canonicalName.split(':').reverse()[0]]) ) {
return this.app().make(definition.imported[definition.canonicalName]) return this.app().make(definition.imported[definition.canonicalName.split(':').reverse()[0]])
} }
throw new InvalidCanonicalExportError(definition.originalName) throw new InvalidCanonicalExportError(definition.originalName)

View File

@ -8,8 +8,8 @@ export class CanonicalStatic<T, T2> extends Canonical<StaticClass<T, T2>> {
return definition.imported.default return definition.imported.default
} }
if ( isStaticClass(definition.imported[definition.canonicalName]) ) { if ( isStaticClass(definition.imported[definition.canonicalName.split(':').reverse()[0]]) ) {
return definition.imported[definition.canonicalName] return definition.imported[definition.canonicalName.split(':').reverse()[0]]
} }
throw new InvalidCanonicalExportError(definition.originalName) throw new InvalidCanonicalExportError(definition.originalName)

View File

@ -1,17 +1,17 @@
import {CanonicalInstantiable} from "./CanonicalInstantiable"; import {CanonicalStatic} from "./CanonicalStatic";
import {Singleton} from "@extollo/di"; import {Singleton, Instantiable} from "@extollo/di";
import {Controller} from "../http/Controller"; import {Controller} from "../http/Controller";
import {CanonicalDefinition} from "./Canonical"; import {CanonicalDefinition} from "./Canonical";
@Singleton() @Singleton()
export class Controllers extends CanonicalInstantiable<Controller> { export class Controllers extends CanonicalStatic<Instantiable<Controller>, Controller> {
protected appPath = ['http', 'controllers'] protected appPath = ['http', 'controllers']
protected canonicalItem = 'controller' protected canonicalItem = 'controller'
protected suffix = '.controller.js' protected suffix = '.controller.js'
public async initCanonicalItem(definition: CanonicalDefinition) { public async initCanonicalItem(definition: CanonicalDefinition) {
const item = await super.initCanonicalItem(definition) 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.`) throw new TypeError(`Invalid controller definition: ${definition.originalName}. Controllers must extend from @extollo/lib.http.Controller.`)
} }