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,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}`
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user