Make new routing system the default
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import {HTTPKernel} from '../HTTPKernel'
|
||||
import {Request} from '../../lifecycle/Request'
|
||||
import {ActivatedRoute} from '../../routing/ActivatedRoute'
|
||||
import {ResponseObject} from '../../routing/Route'
|
||||
import {http} from '../../response/HTTPErrorResponseFactory'
|
||||
import {HTTPStatus} from '../../../util'
|
||||
import {AbstractResolvedRouteHandlerHTTPModule} from './AbstractResolvedRouteHandlerHTTPModule'
|
||||
@@ -18,10 +17,17 @@ export class ExecuteResolvedRouteHandlerHTTPModule extends AbstractResolvedRoute
|
||||
|
||||
public async apply(request: Request): Promise<Request> {
|
||||
if ( request.hasInstance(ActivatedRoute) ) {
|
||||
const route = <ActivatedRoute> request.make(ActivatedRoute)
|
||||
const object: ResponseObject = await route.handler(request)
|
||||
const route = <ActivatedRoute<unknown, unknown[]>> request.make(ActivatedRoute)
|
||||
const params = route.resolvedParameters
|
||||
if ( !params ) {
|
||||
throw new Error('Attempted to call route handler without resolved parameters.')
|
||||
}
|
||||
|
||||
await this.applyResponseObject(object, request)
|
||||
const result = await route.handler
|
||||
.tap(handler => handler(...params))
|
||||
.apply(request)
|
||||
|
||||
await this.applyResponseObject(result, request)
|
||||
} else {
|
||||
await http(HTTPStatus.NOT_FOUND).write(request)
|
||||
request.response.blockingWriteback(true)
|
||||
|
||||
@@ -17,7 +17,7 @@ export class ExecuteResolvedRoutePostflightHTTPModule extends AbstractResolvedRo
|
||||
|
||||
public async apply(request: Request): Promise<Request> {
|
||||
if ( request.hasInstance(ActivatedRoute) ) {
|
||||
const route = <ActivatedRoute> request.make(ActivatedRoute)
|
||||
const route = <ActivatedRoute<unknown, unknown[]>> request.make(ActivatedRoute)
|
||||
const postflight = route.postflight
|
||||
|
||||
for ( const handler of postflight ) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import {Request} from '../../lifecycle/Request'
|
||||
import {ActivatedRoute} from '../../routing/ActivatedRoute'
|
||||
import {ResponseObject} from '../../routing/Route'
|
||||
import {AbstractResolvedRouteHandlerHTTPModule} from './AbstractResolvedRouteHandlerHTTPModule'
|
||||
import {collect, isLeft, unleft, unright} from '../../../util'
|
||||
|
||||
/**
|
||||
* HTTP Kernel module that executes the preflight handlers for the route.
|
||||
@@ -17,7 +18,7 @@ export class ExecuteResolvedRoutePreflightHTTPModule extends AbstractResolvedRou
|
||||
|
||||
public async apply(request: Request): Promise<Request> {
|
||||
if ( request.hasInstance(ActivatedRoute) ) {
|
||||
const route = <ActivatedRoute> request.make(ActivatedRoute)
|
||||
const route = <ActivatedRoute<unknown, unknown[]>> request.make(ActivatedRoute)
|
||||
const preflight = route.preflight
|
||||
|
||||
for ( const handler of preflight ) {
|
||||
@@ -27,6 +28,16 @@ export class ExecuteResolvedRoutePreflightHTTPModule extends AbstractResolvedRou
|
||||
request.response.blockingWriteback(true)
|
||||
}
|
||||
}
|
||||
|
||||
const parameters = route.parameters
|
||||
const resolveResult = await collect(parameters)
|
||||
.asyncMapRight(handler => handler(request))
|
||||
|
||||
if ( isLeft(resolveResult) ) {
|
||||
return unleft(resolveResult)
|
||||
}
|
||||
|
||||
route.resolvedParameters = unright(resolveResult).toArray()
|
||||
}
|
||||
|
||||
return request
|
||||
|
||||
@@ -28,8 +28,8 @@ export class MountActivatedRouteHTTPModule extends HTTPKernelModule {
|
||||
const route = this.routing.match(request.method, request.path)
|
||||
if ( route ) {
|
||||
this.logging.verbose(`Mounting activated route: ${request.path} -> ${route}`)
|
||||
const activated = <ActivatedRoute> request.make(ActivatedRoute, route, request.path)
|
||||
request.registerSingletonInstance<ActivatedRoute>(ActivatedRoute, activated)
|
||||
const activated = <ActivatedRoute<unknown, unknown[]>> request.make(ActivatedRoute, route, request.path)
|
||||
request.registerSingletonInstance<ActivatedRoute<unknown, unknown[]>>(ActivatedRoute, activated)
|
||||
} else {
|
||||
this.logging.debug(`No matching route found for: ${request.method} -> ${request.path}`)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import {ErrorWithContext} from '../../util'
|
||||
import {ResolvedRouteHandler, Route} from './Route'
|
||||
import {Injectable} from '../../di'
|
||||
import {ParameterProvidingMiddleware, ResolvedRouteHandler, Route} from './Route'
|
||||
import {Constructable, Injectable} from '../../di'
|
||||
|
||||
export type HandlerParamProviders<THandlerParams extends unknown[]> = {
|
||||
[Index in keyof THandlerParams]: ParameterProvidingMiddleware<THandlerParams[Index]>
|
||||
} & {length: THandlerParams['length']}
|
||||
|
||||
/**
|
||||
* Class representing a resolved route that a request is mounted to.
|
||||
*/
|
||||
@Injectable()
|
||||
export class ActivatedRoute {
|
||||
export class ActivatedRoute<TReturn, THandlerParams extends unknown[]> {
|
||||
/**
|
||||
* The parsed params from the route definition.
|
||||
*
|
||||
@@ -27,7 +31,7 @@ export class ActivatedRoute {
|
||||
/**
|
||||
* The resolved function that should handle the request for this route.
|
||||
*/
|
||||
public readonly handler: ResolvedRouteHandler
|
||||
public readonly handler: Constructable<(...x: THandlerParams) => TReturn>
|
||||
|
||||
/**
|
||||
* Pre-middleware that should be applied to the request on this route.
|
||||
@@ -39,9 +43,13 @@ export class ActivatedRoute {
|
||||
*/
|
||||
public readonly postflight: ResolvedRouteHandler[]
|
||||
|
||||
public readonly parameters: HandlerParamProviders<THandlerParams>
|
||||
|
||||
public resolvedParameters?: THandlerParams
|
||||
|
||||
constructor(
|
||||
/** The route this ActivatedRoute refers to. */
|
||||
public readonly route: Route,
|
||||
public readonly route: Route<TReturn, THandlerParams>,
|
||||
|
||||
/** The request path that activated that route. */
|
||||
public readonly path: string,
|
||||
@@ -56,9 +64,17 @@ export class ActivatedRoute {
|
||||
throw error
|
||||
}
|
||||
|
||||
if ( !route.handler ) {
|
||||
throw new ErrorWithContext('Cannot instantiate ActivatedRoute. Matched route is not handled.', {
|
||||
matchedRoute: String(route),
|
||||
requestPath: path,
|
||||
})
|
||||
}
|
||||
|
||||
this.params = params
|
||||
this.preflight = route.resolvePreflight()
|
||||
this.handler = route.resolveHandler()
|
||||
this.postflight = route.resolvePostflight()
|
||||
this.preflight = route.getPreflight().toArray()
|
||||
this.handler = route.handler
|
||||
this.postflight = route.getPostflight().toArray()
|
||||
this.parameters = route.getParameters().toArray() as HandlerParamProviders<THandlerParams>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ export abstract class Middleware extends CanonicalItemClass {
|
||||
protected readonly request: Request,
|
||||
) {
|
||||
super()
|
||||
if ( !request ) {
|
||||
throw new Error('Middleware constructed without request')
|
||||
}
|
||||
}
|
||||
|
||||
protected container(): Container {
|
||||
|
||||
@@ -1,56 +1,40 @@
|
||||
import {AppClass} from '../../lifecycle/AppClass'
|
||||
import {HTTPMethod, Request} from '../lifecycle/Request'
|
||||
import {Application} from '../../lifecycle/Application'
|
||||
import {RouteGroup} from './RouteGroup'
|
||||
import {Collection, Either, ErrorWithContext, Pipeline, PrefixTypeArray, right} from '../../util'
|
||||
import {ResponseFactory} from '../response/ResponseFactory'
|
||||
import {Response} from '../lifecycle/Response'
|
||||
import {Controllers} from '../../service/Controllers'
|
||||
import {ErrorWithContext, Collection} from '../../util'
|
||||
import {Container} from '../../di'
|
||||
import {Controller} from '../Controller'
|
||||
import {Middlewares} from '../../service/Middlewares'
|
||||
import {HTTPMethod, Request} from '../lifecycle/Request'
|
||||
import {constructable, Constructable, Container, Instantiable, isInstantiableOf, TypedDependencyKey} from '../../di'
|
||||
import {Middleware} from './Middleware'
|
||||
import {Valid, Validator, ValidatorFactory} from '../../validation/Validator'
|
||||
import {validateMiddleware} from '../../validation/middleware'
|
||||
import {RouteGroup} from './RouteGroup'
|
||||
import {Config} from '../../service/Config'
|
||||
import {Validator} from '../../validation/Validator'
|
||||
import {Application} from '../../lifecycle/Application'
|
||||
|
||||
/**
|
||||
* Type alias for an item that is a valid response object, or lack thereof.
|
||||
*/
|
||||
export type ResponseObject = ResponseFactory | string | number | void | any | Promise<ResponseObject>
|
||||
|
||||
/**
|
||||
* Type alias for an item that defines a direct route handler.
|
||||
*/
|
||||
export type RouteHandler = ((request: Request, response: Response) => ResponseObject) | ((request: Request) => ResponseObject) | (() => ResponseObject) | string
|
||||
|
||||
/**
|
||||
* Type alias for a function that applies a route handler to the request.
|
||||
* The goal is to transform RouteHandlers to ResolvedRouteHandler.
|
||||
*/
|
||||
export type ResolvedRouteHandler = (request: Request) => ResponseObject
|
||||
|
||||
export type ParameterProvidingMiddleware<T> = (request: Request) => Either<ResponseObject, T>
|
||||
|
||||
// TODO domains, named routes - support this on groups as well
|
||||
export interface HandledRoute<TReturn extends ResponseObject, THandlerParams extends unknown[] = []> {
|
||||
handler: Constructable<(...x: THandlerParams) => TReturn>
|
||||
|
||||
/**
|
||||
* A class that can be used to build and reference dynamic routes in the application.
|
||||
*
|
||||
* Routes can be defined in nested groups, with prefixes and middleware handlers.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* Route.post('/api/v1/ping', (request: Request) => {
|
||||
* return 'pong!'
|
||||
* })
|
||||
*
|
||||
* Route.group('/api/v2', () => {
|
||||
* Route.get('/status', 'controller::api:v2:Status.getStatus').pre('auth:UserOnly')
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export class Route extends AppClass {
|
||||
/**
|
||||
* Set a programmatic name for this route.
|
||||
* @param name
|
||||
*/
|
||||
alias(name: string): this
|
||||
}
|
||||
|
||||
export class Route<TReturn extends ResponseObject, THandlerParams extends unknown[] = []> {
|
||||
/** Routes that have been created and registered in the application. */
|
||||
private static registeredRoutes: Route[] = []
|
||||
private static registeredRoutes: Route<unknown, unknown[]>[] = []
|
||||
|
||||
/** Groups of routes that have been registered with the application. */
|
||||
private static registeredGroups: RouteGroup[] = []
|
||||
@@ -73,7 +57,7 @@ export class Route extends AppClass {
|
||||
* This function attempts to resolve the route handlers ahead of time to cache
|
||||
* them and also expose any handler resolution errors that might happen at runtime.
|
||||
*/
|
||||
public static async compile(): Promise<Route[]> {
|
||||
public static async compile(): Promise<Route<unknown, unknown[]>[]> {
|
||||
let registeredRoutes = this.registeredRoutes
|
||||
const registeredGroups = this.registeredGroups
|
||||
|
||||
@@ -87,55 +71,41 @@ export class Route extends AppClass {
|
||||
for ( const route of registeredRoutes ) {
|
||||
for ( const group of stack ) {
|
||||
route.prepend(group.prefix)
|
||||
group.getGroupMiddlewareDefinitions()
|
||||
.where('stage', '=', 'pre')
|
||||
.each(def => {
|
||||
route.prependMiddleware(def)
|
||||
})
|
||||
group.getPreflight()
|
||||
.each(def => route.preflight.prepend(def))
|
||||
}
|
||||
|
||||
for ( const group of this.compiledGroupStack ) {
|
||||
group.getGroupMiddlewareDefinitions()
|
||||
.where('stage', '=', 'post')
|
||||
.each(def => route.appendMiddleware(def))
|
||||
group.getPostflight()
|
||||
.each(def => route.postflight.push(def))
|
||||
}
|
||||
|
||||
// Add the global pre- and post- middleware
|
||||
if ( Array.isArray(globalMiddleware?.pre) ) {
|
||||
const globalPre = [...globalMiddleware.pre].reverse()
|
||||
for ( const item of globalPre ) {
|
||||
if ( typeof item !== 'string' ) {
|
||||
throw new ErrorWithContext(`Invalid global pre-middleware definition. Global middleware must be string-references.`, {
|
||||
if ( !isInstantiableOf(item, Middleware) ) {
|
||||
throw new ErrorWithContext(`Invalid global pre-middleware definition. Global middleware must be static references to Middleware implementations.`, {
|
||||
configKey: 'server.middleware.global.pre',
|
||||
})
|
||||
}
|
||||
|
||||
route.prependMiddleware({
|
||||
stage: 'pre',
|
||||
handler: item,
|
||||
})
|
||||
route.preflight.prepend(request => request.make<Middleware>(item, request).apply())
|
||||
}
|
||||
}
|
||||
|
||||
if ( Array.isArray(globalMiddleware?.post) ) {
|
||||
const globalPost = [...globalMiddleware.post]
|
||||
for ( const item of globalPost ) {
|
||||
if ( typeof item !== 'string' ) {
|
||||
throw new ErrorWithContext(`Invalid global post-middleware definition. Global middleware must be string-references.`, {
|
||||
if ( !isInstantiableOf(item, Middleware) ) {
|
||||
throw new ErrorWithContext(`Invalid global post-middleware definition. Global middleware must be static references to Middleware implementations.`, {
|
||||
configKey: 'server.middleware.global.post',
|
||||
})
|
||||
}
|
||||
|
||||
route.appendMiddleware({
|
||||
stage: 'post',
|
||||
handler: item,
|
||||
})
|
||||
route.postflight.push(request => request.make<Middleware>(item, request).apply())
|
||||
}
|
||||
}
|
||||
|
||||
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 ) {
|
||||
@@ -151,50 +121,46 @@ export class Route extends AppClass {
|
||||
return registeredRoutes
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a new route on the given endpoint for the given HTTP verb.
|
||||
* @param method
|
||||
* @param definition
|
||||
* @param handler
|
||||
* @param endpoint
|
||||
*/
|
||||
public static endpoint(method: HTTPMethod | HTTPMethod[], definition: string, handler: RouteHandler): Route {
|
||||
const route = new Route(method, handler, definition)
|
||||
this.registeredRoutes.push(route)
|
||||
return route
|
||||
public static endpoint(method: HTTPMethod | HTTPMethod[], endpoint: string): Route<ResponseObject> {
|
||||
return new Route(method, endpoint)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new GET route on the given endpoint.
|
||||
* @param definition
|
||||
* @param handler
|
||||
*/
|
||||
public static get(definition: string, handler: RouteHandler): Route {
|
||||
return this.endpoint('get', definition, handler)
|
||||
public static get(endpoint: string): Route<ResponseObject> {
|
||||
return this.endpoint('get', endpoint)
|
||||
}
|
||||
|
||||
/** Create a new POST route on the given endpoint. */
|
||||
public static post(definition: string, handler: RouteHandler): Route {
|
||||
return this.endpoint('post', definition, handler)
|
||||
public static post(endpoint: string): Route<ResponseObject> {
|
||||
return this.endpoint('post', endpoint)
|
||||
}
|
||||
|
||||
/** Create a new PUT route on the given endpoint. */
|
||||
public static put(definition: string, handler: RouteHandler): Route {
|
||||
return this.endpoint('put', definition, handler)
|
||||
public static put(endpoint: string): Route<ResponseObject> {
|
||||
return this.endpoint('put', endpoint)
|
||||
}
|
||||
|
||||
/** Create a new PATCH route on the given endpoint. */
|
||||
public static patch(definition: string, handler: RouteHandler): Route {
|
||||
return this.endpoint('patch', definition, handler)
|
||||
public static patch(endpoint: string): Route<ResponseObject> {
|
||||
return this.endpoint('patch', endpoint)
|
||||
}
|
||||
|
||||
/** Create a new DELETE route on the given endpoint. */
|
||||
public static delete(definition: string, handler: RouteHandler): Route {
|
||||
return this.endpoint('delete', definition, handler)
|
||||
public static delete(endpoint: string): Route<ResponseObject> {
|
||||
return this.endpoint('delete', endpoint)
|
||||
}
|
||||
|
||||
/** Create a new route on all HTTP verbs, on the given endpoint. */
|
||||
public static any(definition: string, handler: RouteHandler): Route {
|
||||
return this.endpoint(['get', 'put', 'patch', 'post', 'delete'], definition, handler)
|
||||
public static any(endpoint: string): Route<ResponseObject> {
|
||||
return this.endpoint(['get', 'put', 'patch', 'post', 'delete'], endpoint)
|
||||
}
|
||||
|
||||
/** Create a new route group with the given prefix. */
|
||||
@@ -204,35 +170,20 @@ export class Route extends AppClass {
|
||||
return grp
|
||||
}
|
||||
|
||||
/** Middlewares that should be applied to this route. */
|
||||
protected middlewares: Collection<{ stage: 'pre' | 'post', handler: RouteHandler }> = new Collection<{stage: 'pre' | 'post'; handler: RouteHandler}>()
|
||||
protected preflight: Collection<ResolvedRouteHandler> = new Collection<ResolvedRouteHandler>()
|
||||
|
||||
/** Pre-compiled route handlers for the pre-middleware for this route. */
|
||||
protected compiledPreflight?: ResolvedRouteHandler[]
|
||||
protected parameters: Collection<ParameterProvidingMiddleware<unknown>> = new Collection<ParameterProvidingMiddleware<unknown>>()
|
||||
|
||||
/** Pre-compiled route handlers for the post-middleware for this route. */
|
||||
protected compiledHandler?: ResolvedRouteHandler
|
||||
protected postflight: Collection<ResolvedRouteHandler> = new Collection<ResolvedRouteHandler>()
|
||||
|
||||
/** Pre-compiled route handler for the main route handler for this route. */
|
||||
protected compiledPostflight?: ResolvedRouteHandler[]
|
||||
protected aliases: Collection<string> = new Collection<string>()
|
||||
|
||||
protected validator?: Validator<unknown>
|
||||
|
||||
/** Programmatic aliases of this route. */
|
||||
public aliases: string[] = []
|
||||
handler?: Constructable<(...x: THandlerParams) => TReturn>
|
||||
|
||||
constructor(
|
||||
/** The HTTP method(s) that this route listens on. */
|
||||
protected method: HTTPMethod | HTTPMethod[],
|
||||
|
||||
/** The primary handler of this route. */
|
||||
protected readonly handler: RouteHandler,
|
||||
|
||||
/** The route path this route listens on. */
|
||||
protected route: string,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Set a programmatic name for this route.
|
||||
@@ -251,24 +202,32 @@ export class Route extends AppClass {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the string-form method of the route.
|
||||
* Get the string-form methods supported by the route.
|
||||
*/
|
||||
public getMethod(): HTTPMethod | HTTPMethod[] {
|
||||
public getMethods(): HTTPMethod[] {
|
||||
if ( !Array.isArray(this.method) ) {
|
||||
return [this.method]
|
||||
}
|
||||
|
||||
return this.method
|
||||
}
|
||||
|
||||
/**
|
||||
* Get collection of applied middlewares.
|
||||
* Get preflight middleware for this route.
|
||||
*/
|
||||
public getMiddlewares(): Collection<{ stage: 'pre' | 'post', handler: RouteHandler }> {
|
||||
return this.middlewares.clone()
|
||||
public getPreflight(): Collection<ResolvedRouteHandler> {
|
||||
return this.preflight.clone()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the string-form of the route handler.
|
||||
* Get postflight middleware for this route.
|
||||
*/
|
||||
public getDisplayableHandler(): string {
|
||||
return typeof this.handler === 'string' ? this.handler : '(anonymous function)'
|
||||
public getPostflight(): Collection<ResolvedRouteHandler> {
|
||||
return this.postflight.clone()
|
||||
}
|
||||
|
||||
public getParameters(): Collection<ParameterProvidingMiddleware<unknown>> {
|
||||
return this.parameters.clone()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -337,66 +296,80 @@ export class Route extends AppClass {
|
||||
return params
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to pre-compile and return the preflight handlers for this route.
|
||||
*/
|
||||
public resolvePreflight(): ResolvedRouteHandler[] {
|
||||
if ( !this.compiledPreflight ) {
|
||||
this.compiledPreflight = this.resolveMiddlewareHandlersForStage('pre')
|
||||
}
|
||||
public parameterMiddleware<T>(
|
||||
handler: ParameterProvidingMiddleware<T>,
|
||||
): Route<TReturn, PrefixTypeArray<T, THandlerParams>> {
|
||||
const route = new Route<TReturn, PrefixTypeArray<T, THandlerParams>>(
|
||||
this.method,
|
||||
this.route,
|
||||
)
|
||||
|
||||
return this.compiledPreflight
|
||||
route.copyFrom(this)
|
||||
route.parameters.push(handler)
|
||||
return route
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to pre-compile and return the postflight handlers for this route.
|
||||
*/
|
||||
public resolvePostflight(): ResolvedRouteHandler[] {
|
||||
if ( !this.compiledPostflight ) {
|
||||
this.compiledPostflight = this.resolveMiddlewareHandlersForStage('post')
|
||||
}
|
||||
|
||||
return this.compiledPostflight
|
||||
private copyFrom(other: Route<TReturn, any>) {
|
||||
this.preflight = other.preflight.clone()
|
||||
this.postflight = other.postflight.clone()
|
||||
this.aliases = other.aliases.clone()
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to pre-compile and return the main handler for this route.
|
||||
*/
|
||||
public resolveHandler(): ResolvedRouteHandler {
|
||||
if ( !this.compiledHandler ) {
|
||||
this.compiledHandler = this.compileResolvedHandler()
|
||||
}
|
||||
public calls<TKey>(
|
||||
key: TypedDependencyKey<TKey>,
|
||||
selector: (x: TKey) => (...params: THandlerParams) => TReturn,
|
||||
): HandledRoute<TReturn, THandlerParams> {
|
||||
this.handler = constructable<TKey>(key)
|
||||
.tap(inst => Function.prototype.bind.call(selector(inst), inst as any) as ((...params: THandlerParams) => TReturn))
|
||||
|
||||
return this.compiledHandler
|
||||
Route.registeredRoutes.push(this as unknown as Route<unknown, unknown[]>) // man this is stupid
|
||||
return this as HandledRoute<TReturn, THandlerParams>
|
||||
}
|
||||
|
||||
/** Register the given middleware as a preflight handler for this route. */
|
||||
pre(middleware: RouteHandler): this {
|
||||
this.middlewares.push({
|
||||
stage: 'pre',
|
||||
handler: middleware,
|
||||
})
|
||||
public handledBy(
|
||||
handler: (...params: THandlerParams) => TReturn,
|
||||
): HandledRoute<TReturn, THandlerParams> {
|
||||
this.handler = Pipeline.id<Container>()
|
||||
.tap(() => handler)
|
||||
|
||||
Route.registeredRoutes.push(this as unknown as Route<unknown, unknown[]>)
|
||||
return this as HandledRoute<TReturn, THandlerParams>
|
||||
}
|
||||
|
||||
public pre(middleware: Instantiable<Middleware>): this {
|
||||
this.preflight.push(request => request.make<Middleware>(middleware, request).apply())
|
||||
return this
|
||||
}
|
||||
|
||||
/** Register the given middleware as a postflight handler for this route. */
|
||||
post(middleware: RouteHandler): this {
|
||||
this.middlewares.push({
|
||||
stage: 'post',
|
||||
handler: middleware,
|
||||
})
|
||||
|
||||
public post(middleware: Instantiable<Middleware>): this {
|
||||
this.postflight.push(request => request.make<Middleware>(middleware, request).apply())
|
||||
return this
|
||||
}
|
||||
|
||||
input(validator: Validator<any>): this {
|
||||
if ( !this.validator ) {
|
||||
//
|
||||
public input<T extends Validator<T>>(validator: ValidatorFactory<T>): Route<TReturn, PrefixTypeArray<Valid<T>, THandlerParams>> {
|
||||
if ( !(validator instanceof Validator) ) {
|
||||
validator = validator()
|
||||
}
|
||||
|
||||
this.validator = validator
|
||||
return this
|
||||
return this.parameterMiddleware(validateMiddleware(validator))
|
||||
}
|
||||
|
||||
public passingRequest(): Route<TReturn, PrefixTypeArray<Request, THandlerParams>> {
|
||||
return this.parameterMiddleware(request => right(request))
|
||||
}
|
||||
|
||||
hasAlias(name: string): boolean {
|
||||
return this.aliases.includes(name)
|
||||
}
|
||||
|
||||
isHandled(): this is HandledRoute<TReturn, THandlerParams> {
|
||||
return Boolean(this.handler)
|
||||
}
|
||||
|
||||
/** Cast the route to an intelligible string. */
|
||||
toString(): string {
|
||||
const method = Array.isArray(this.method) ? this.method : [this.method]
|
||||
return `${method.join('|')} -> ${this.route}`
|
||||
}
|
||||
|
||||
/** Prefix the route's path with the given prefix, normalizing `/` characters. */
|
||||
@@ -410,111 +383,4 @@ export class Route extends AppClass {
|
||||
this.route = `${prefix}${this.route}`
|
||||
return this
|
||||
}
|
||||
|
||||
/** Add the given middleware item to the beginning of the preflight handlers. */
|
||||
private prependMiddleware(def: { stage: 'pre' | 'post', handler: RouteHandler }): void {
|
||||
this.middlewares.prepend(def)
|
||||
}
|
||||
|
||||
/** Add the given middleware item to the end of the postflight handlers. */
|
||||
private appendMiddleware(def: { stage: 'pre' | 'post', handler: RouteHandler }): void {
|
||||
this.middlewares.push(def)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve and return the route handler for this route.
|
||||
* @private
|
||||
*/
|
||||
private compileResolvedHandler(): ResolvedRouteHandler {
|
||||
const handler = this.handler
|
||||
if ( typeof handler !== 'string' ) {
|
||||
return (request: Request) => {
|
||||
return handler(request, request.response)
|
||||
}
|
||||
} else {
|
||||
const parts = handler.split('.')
|
||||
if ( parts.length < 2 ) {
|
||||
const e = new ErrorWithContext('Route handler does not specify a method name.')
|
||||
e.context = {
|
||||
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,
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve and return the route handlers for the given pre- or post-flight stage.
|
||||
* @param stage
|
||||
* @private
|
||||
*/
|
||||
private resolveMiddlewareHandlersForStage(stage: 'pre' | 'post'): ResolvedRouteHandler[] {
|
||||
return this.middlewares.where('stage', '=', stage)
|
||||
.map<ResolvedRouteHandler>(def => {
|
||||
const handler = def.handler
|
||||
if ( typeof handler !== 'string' ) {
|
||||
return (request: Request) => {
|
||||
return handler(request, request.response)
|
||||
}
|
||||
} else {
|
||||
const parts = handler.split('.')
|
||||
if ( parts.length < 2 ) {
|
||||
parts.push('apply') // default middleware method name, if none provided
|
||||
}
|
||||
|
||||
const [middlewareName, methodName] = parts
|
||||
|
||||
const middlewaresService = <Middlewares> this.make(Middlewares)
|
||||
const middlewareClass = middlewaresService.get(middlewareName)
|
||||
if ( !middlewareClass ) {
|
||||
const e = new ErrorWithContext('Middleware not found for route handler.')
|
||||
e.context = {
|
||||
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 = <Middleware> request.make(middlewareClass, request)
|
||||
const method = middleware.getBoundMethod(methodName)
|
||||
return method()
|
||||
}
|
||||
}
|
||||
})
|
||||
.toArray()
|
||||
}
|
||||
|
||||
/** Cast the route to an intelligible string. */
|
||||
toString(): string {
|
||||
const method = Array.isArray(this.method) ? this.method : [this.method]
|
||||
return `${method.join('|')} -> ${this.route}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
import {Collection, Either, PrefixTypeArray} from '../../util'
|
||||
import {ResponseFactory} from '../response/ResponseFactory'
|
||||
import {HTTPMethod, Request} from '../lifecycle/Request'
|
||||
import {TypedDependencyKey, constructable, Constructable, Instantiable} from '../../di'
|
||||
import {Middleware} from './Middleware'
|
||||
|
||||
/**
|
||||
* Type alias for an item that is a valid response object, or lack thereof.
|
||||
*/
|
||||
export type ResponseObject = ResponseFactory | string | number | void | any | Promise<ResponseObject>
|
||||
|
||||
/**
|
||||
* Type alias for a function that applies a route handler to the request.
|
||||
* The goal is to transform RouteHandlers to ResolvedRouteHandler.
|
||||
*/
|
||||
export type ResolvedRouteHandler = (request: Request) => ResponseObject
|
||||
|
||||
export type ParameterProvidingMiddleware<T> = (request: Request) => Either<ResponseObject, T>
|
||||
|
||||
export interface HandledRoute<TReturn extends ResponseObject, THandlerParams extends unknown[] = []> {
|
||||
/**
|
||||
* Set a programmatic name for this route.
|
||||
* @param name
|
||||
*/
|
||||
alias(name: string): this
|
||||
}
|
||||
|
||||
export class Route<TReturn extends ResponseObject, THandlerParams extends unknown[] = []> {
|
||||
protected preflight: Collection<ResolvedRouteHandler> = new Collection<ResolvedRouteHandler>()
|
||||
|
||||
protected parameters: Collection<ParameterProvidingMiddleware<unknown>> = new Collection<ParameterProvidingMiddleware<unknown>>()
|
||||
|
||||
protected postflight: Collection<ResolvedRouteHandler> = new Collection<ResolvedRouteHandler>()
|
||||
|
||||
protected aliases: Collection<string> = new Collection<string>()
|
||||
|
||||
protected handler?: Constructable<(...x: THandlerParams) => TReturn>
|
||||
|
||||
constructor(
|
||||
protected method: HTTPMethod | HTTPMethod[],
|
||||
protected route: string,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Set a programmatic name for this route.
|
||||
* @param name
|
||||
*/
|
||||
public alias(name: string): this {
|
||||
this.aliases.push(name)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the string-form of the route.
|
||||
*/
|
||||
public getRoute(): string {
|
||||
return this.route
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the string-form methods supported by the route.
|
||||
*/
|
||||
public getMethods(): HTTPMethod[] {
|
||||
if ( !Array.isArray(this.method) ) {
|
||||
return [this.method]
|
||||
}
|
||||
|
||||
return this.method
|
||||
}
|
||||
|
||||
/**
|
||||
* Get preflight middleware for this route.
|
||||
*/
|
||||
public getPreflight(): Collection<ResolvedRouteHandler> {
|
||||
return this.preflight.clone()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get postflight middleware for this route.
|
||||
*/
|
||||
public getPostflight(): Collection<ResolvedRouteHandler> {
|
||||
return this.postflight.clone()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this route matches the given HTTP verb and request path.
|
||||
* @param method
|
||||
* @param potential
|
||||
*/
|
||||
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 Boolean(this.extract(potential))
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a request path, try to extract this route's paramters from the path string.
|
||||
*
|
||||
* @example
|
||||
* For route `/foo/:bar/baz` and input `/foo/bob/baz`, extracts:
|
||||
*
|
||||
* ```typescript
|
||||
* {
|
||||
* bar: 'bob'
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param 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 parameterMiddleware<T>(
|
||||
handler: ParameterProvidingMiddleware<T>,
|
||||
): Route<TReturn, PrefixTypeArray<T, THandlerParams>> {
|
||||
const route = new Route<TReturn, PrefixTypeArray<T, THandlerParams>>(
|
||||
this.method,
|
||||
this.route,
|
||||
)
|
||||
|
||||
route.copyFrom(this)
|
||||
route.parameters.push(handler)
|
||||
return route
|
||||
}
|
||||
|
||||
private copyFrom(other: Route<TReturn, any>) {
|
||||
this.preflight = other.preflight.clone()
|
||||
this.postflight = other.postflight.clone()
|
||||
this.aliases = other.aliases.clone()
|
||||
}
|
||||
|
||||
public calls<TKey>(
|
||||
key: TypedDependencyKey<TKey>,
|
||||
selector: (x: TKey) => (...params: THandlerParams) => TReturn,
|
||||
): HandledRoute<TReturn, THandlerParams> {
|
||||
this.handler = constructable<TKey>(key)
|
||||
.tap(inst => Function.prototype.bind.call(inst as any, selector(inst)) as ((...params: THandlerParams) => TReturn))
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public pre(middleware: Instantiable<Middleware>): this {
|
||||
this.preflight.push(request => request.make<Middleware>(middleware).apply())
|
||||
return this
|
||||
}
|
||||
|
||||
public post(middleware: Instantiable<Middleware>): this {
|
||||
this.postflight.push(request => request.make<Middleware>(middleware).apply())
|
||||
return this
|
||||
}
|
||||
|
||||
// validator
|
||||
|
||||
/** Cast the route to an intelligible string. */
|
||||
toString(): string {
|
||||
const method = Array.isArray(this.method) ? this.method : [this.method]
|
||||
return `${method.join('|')} -> ${this.route}`
|
||||
}
|
||||
|
||||
/** Prefix the route's path with the given prefix, normalizing `/` characters. */
|
||||
private prepend(prefix: string): this {
|
||||
if ( !prefix.endsWith('/') ) {
|
||||
prefix = `${prefix}/`
|
||||
}
|
||||
if ( this.route.startsWith('/') ) {
|
||||
this.route = this.route.substring(1)
|
||||
}
|
||||
this.route = `${prefix}${this.route}`
|
||||
return this
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import {Collection, ErrorWithContext} from '../../util'
|
||||
import {AppClass} from '../../lifecycle/AppClass'
|
||||
import {RouteHandler} from './Route'
|
||||
import {ResolvedRouteHandler} from './Route'
|
||||
import {Container} from '../../di'
|
||||
import {Logging} from '../../service/Logging'
|
||||
|
||||
@@ -8,6 +8,10 @@ import {Logging} from '../../service/Logging'
|
||||
* Class that defines a group of Routes in the application, with a prefix.
|
||||
*/
|
||||
export class RouteGroup extends AppClass {
|
||||
protected preflight: Collection<ResolvedRouteHandler> = new Collection<ResolvedRouteHandler>()
|
||||
|
||||
protected postflight: Collection<ResolvedRouteHandler> = new Collection<ResolvedRouteHandler>()
|
||||
|
||||
/**
|
||||
* The current set of nested groups. This is used when compiling route groups.
|
||||
* @private
|
||||
@@ -20,12 +24,6 @@ export class RouteGroup extends AppClass {
|
||||
*/
|
||||
protected static namedGroups: {[key: string]: () => void } = {}
|
||||
|
||||
/**
|
||||
* Array of middlewares that should apply to all routes in this group.
|
||||
* @protected
|
||||
*/
|
||||
protected middlewares: Collection<{ stage: 'pre' | 'post', handler: RouteHandler }> = new Collection<{stage: 'pre' | 'post'; handler: RouteHandler}>()
|
||||
|
||||
/**
|
||||
* Get the current group nesting.
|
||||
*/
|
||||
@@ -89,27 +87,23 @@ export class RouteGroup extends AppClass {
|
||||
}
|
||||
|
||||
/** Register the given middleware to be applied before all routes in this group. */
|
||||
pre(middleware: RouteHandler): this {
|
||||
this.middlewares.push({
|
||||
stage: 'pre',
|
||||
handler: middleware,
|
||||
})
|
||||
|
||||
pre(middleware: ResolvedRouteHandler): this {
|
||||
this.preflight.push(middleware)
|
||||
return this
|
||||
}
|
||||
|
||||
/** Register the given middleware to be applied after all routes in this group. */
|
||||
post(middleware: RouteHandler): this {
|
||||
this.middlewares.push({
|
||||
stage: 'post',
|
||||
handler: middleware,
|
||||
})
|
||||
|
||||
post(middleware: ResolvedRouteHandler): this {
|
||||
this.postflight.push(middleware)
|
||||
return this
|
||||
}
|
||||
|
||||
/** Return the middlewares that apply to this group. */
|
||||
getGroupMiddlewareDefinitions(): Collection<{ stage: 'pre' | 'post', handler: RouteHandler }> {
|
||||
return this.middlewares
|
||||
getPreflight(): Collection<ResolvedRouteHandler> {
|
||||
return this.preflight
|
||||
}
|
||||
|
||||
getPostflight(): Collection<ResolvedRouteHandler> {
|
||||
return this.postflight
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@ import {HTTPError} from '../HTTPError'
|
||||
import {view, ViewResponseFactory} from '../response/ViewResponseFactory'
|
||||
import {redirect} from '../response/RedirectResponseFactory'
|
||||
import {file} from '../response/FileResponseFactory'
|
||||
import {RouteHandler} from '../routing/Route'
|
||||
import {ResponseObject} from '../routing/Route'
|
||||
import {Logging} from '../../service/Logging'
|
||||
|
||||
/**
|
||||
* Defines the behavior of the static server.
|
||||
@@ -109,11 +110,12 @@ function getBasePath(appPath: UniversalPath, basePath?: string | string[] | Univ
|
||||
* Get a route handler that serves a directory as static files.
|
||||
* @param options
|
||||
*/
|
||||
export function staticServer(options: StaticServerOptions = {}): RouteHandler {
|
||||
export function staticServer(options: StaticServerOptions = {}): (request: Request) => Promise<ResponseObject> {
|
||||
return async (request: Request) => {
|
||||
const config = <Config> request.make(Config)
|
||||
const route = <ActivatedRoute> request.make(ActivatedRoute)
|
||||
const route = <ActivatedRoute<unknown, unknown[]>> request.make(ActivatedRoute)
|
||||
const app = <Application> request.make(Application)
|
||||
const logging = <Logging> request.make(Logging)
|
||||
|
||||
const staticConfig = config.get('server.builtIns.static', {})
|
||||
const mergedOptions = {
|
||||
@@ -183,6 +185,7 @@ export function staticServer(options: StaticServerOptions = {}): RouteHandler {
|
||||
}
|
||||
|
||||
// Otherwise, just send the file as the response body
|
||||
logging.verbose(`Sending file: ${filePath}`)
|
||||
return file(filePath)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user