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, Collection} from "@extollo/util"; import {Controller} from "../Controller"; import {Middlewares} from "../../service/Middlewares"; import {Middleware} from "./Middleware"; 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 // TODO domains, named routes - support this on groups as well export class Route extends AppClass { private static registeredRoutes: Route[] = [] private static registeredGroups: RouteGroup[] = [] private static compiledGroupStack: RouteGroup[] = [] public static registerGroup(group: RouteGroup) { this.registeredGroups.push(group) } public static async compile(): Promise { let registeredRoutes = this.registeredRoutes const registeredGroups = this.registeredGroups this.registeredRoutes = [] this.registeredGroups = [] const stack = [...this.compiledGroupStack].reverse() for ( const route of registeredRoutes ) { for ( const group of stack ) { route.prepend(group.prefix) group.getGroupMiddlewareDefinitions() .each(def => route.prependMiddleware(def)) } for ( const group of this.compiledGroupStack ) { group.getGroupMiddlewareDefinitions() .each(def => route.appendMiddleware(def)) } route.resolvePreflight() // Try to resolve here to catch any errors at boot-time and pre-compile route.resolveHandler() route.resolvePostflight() } for ( const group of registeredGroups ) { this.compiledGroupStack.push(group) await group.group() const childCompilation = await this.compile() registeredRoutes = registeredRoutes.concat(childCompilation) this.compiledGroupStack.pop() } return registeredRoutes } public static endpoint(method: HTTPMethod | HTTPMethod[], definition: string, handler: RouteHandler) { const route = new Route(method, handler, definition) this.registeredRoutes.push(route) return route } public static get(definition: string, handler: RouteHandler) { return this.endpoint('get', definition, handler) } public static post(definition: string, handler: RouteHandler) { return this.endpoint('post', definition, handler) } public static put(definition: string, handler: RouteHandler) { return this.endpoint('put', definition, handler) } public static patch(definition: string, handler: RouteHandler) { return this.endpoint('patch', definition, handler) } public static delete(definition: string, handler: RouteHandler) { return this.endpoint('delete', definition, handler) } public static any(definition: string, handler: RouteHandler) { return this.endpoint(['get', 'put', 'patch', 'post', 'delete'], definition, handler) } public static group(prefix: string, group: () => void | Promise) { const grp = Application.getApplication().make(RouteGroup, group, prefix) this.registeredGroups.push(grp) return grp } protected middlewares: Collection<{ stage: 'pre' | 'post', handler: RouteHandler }> = new Collection<{stage: "pre" | "post"; handler: RouteHandler}>() protected _compiledPreflight?: ResolvedRouteHandler[] protected _compiledHandler?: ResolvedRouteHandler protected _compiledPostflight?: ResolvedRouteHandler[] constructor( protected method: HTTPMethod | HTTPMethod[], protected readonly handler: RouteHandler, protected route: string ) { super() } public match(method: HTTPMethod, potential: string): boolean { if ( Array.isArray(this.method) && !this.method.includes(method) ) return false else if ( !Array.isArray(this.method) && this.method !== method ) return false return !!this.extract(potential) } public extract(potential: string): {[key: string]: string} | undefined { const routeParts = (this.route.startsWith('/') ? this.route.substr(1) : this.route).split('/') const potentialParts = (potential.startsWith('/') ? potential.substr(1) : potential).split('/') const params: any = {} let wildcardIdx = 0 for ( let i = 0; i < routeParts.length; i += 1 ) { const part = routeParts[i] if ( part === '**' ) { params[wildcardIdx] = potentialParts.slice(i).join('/') return params } if ( (potentialParts.length - 1) < i ) { return } if ( part === '*' ) { params[wildcardIdx] = potentialParts[i] wildcardIdx += 1 } else if ( part.startsWith(':') ) { params[part.substr(1)] = potentialParts[i] } else if ( potentialParts[i] !== part ) { return } } // If we got here, we didn't find a ** // So, if the lengths are different, fail if ( routeParts.length !== potentialParts.length ) return return params } public resolvePreflight(): ResolvedRouteHandler[] { if ( !this._compiledPreflight ) { this._compiledPreflight = this.resolveMiddlewareHandlersForStage('pre') } return this._compiledPreflight } public resolvePostflight(): ResolvedRouteHandler[] { if ( !this._compiledPostflight ) { this._compiledPostflight = this.resolveMiddlewareHandlersForStage('post') } return this._compiledPostflight } public resolveHandler(): ResolvedRouteHandler { if ( !this._compiledHandler ) { this._compiledHandler = this._resolveHandler() } return this._compiledHandler } pre(middleware: RouteHandler) { this.middlewares.push({ stage: 'pre', handler: middleware }) return this } post(middleware: RouteHandler) { this.middlewares.push({ stage: 'post', handler: middleware, }) return this } private prepend(prefix: string) { if ( !prefix.endsWith('/') ) prefix = `${prefix}/` if ( this.route.startsWith('/') ) this.route = this.route.substring(1) this.route = `${prefix}${this.route}` } private prependMiddleware(def: { stage: 'pre' | 'post', handler: RouteHandler }) { this.middlewares.prepend(def) } private appendMiddleware(def: { stage: 'pre' | 'post', handler: RouteHandler }) { this.middlewares.push(def) } private _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 < 2 ) { 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 resolveMiddlewareHandlersForStage(stage: 'pre' | 'post'): ResolvedRouteHandler[] { return this.middlewares.where('stage', '=', stage) .map(def => { if ( typeof def.handler !== 'string' ) { return (request: Request) => { // @ts-ignore return def.handler(request, request.response) } } else { const parts = def.handler.split('.') if ( parts.length < 2 ) { parts.push('apply') // default middleware method name, if none provided } const [middlewareName, methodName] = parts const middlewaresService = this.make(Middlewares) const middlewareClass = middlewaresService.get(middlewareName) if ( !middlewareClass ) { const e = new ErrorWithContext('Middleware not found for route handler.') e.context = { handler: def.handler, middlewareName, methodName, } throw e } return (request: Request) => { // If not a function, then we got a string reference to a middleware method // So, we need to use the request container to instantiate the middleware // and bind the method const middleware = request.make(middlewareClass, request) const method = middleware.getBoundMethod(methodName) return method() } } }) .toArray() } toString() { const method = Array.isArray(this.method) ? this.method : [this.method] return `${method.join('|')} -> ${this.route}` } }