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:
parent
8cf19792a6
commit
dc16dfdb81
@ -2,7 +2,6 @@ import {Directive, OptionDefinition} from '../Directive'
|
|||||||
import {Inject, Injectable} from '../../di'
|
import {Inject, Injectable} from '../../di'
|
||||||
import {Routing} from '../../service/Routing'
|
import {Routing} from '../../service/Routing'
|
||||||
import Table = require('cli-table')
|
import Table = require('cli-table')
|
||||||
import {RouteHandler} from '../../http/routing/Route'
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RouteDirective extends Directive {
|
export class RouteDirective extends Directive {
|
||||||
@ -33,7 +32,7 @@ export class RouteDirective extends Directive {
|
|||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.trim()
|
.trim()
|
||||||
|
|
||||||
this.routing.getCompiled()
|
/* this.routing.getCompiled()
|
||||||
.filter(match => match.getRoute().trim() === route && (!method || match.getMethod() === method))
|
.filter(match => match.getRoute().trim() === route && (!method || match.getMethod() === method))
|
||||||
.tap(matches => {
|
.tap(matches => {
|
||||||
if ( !matches.length ) {
|
if ( !matches.length ) {
|
||||||
@ -42,8 +41,7 @@ export class RouteDirective extends Directive {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.each(match => {
|
.each(match => {
|
||||||
const pre = match.getMiddlewares()
|
const pre = match.getPreflight()
|
||||||
.where('stage', '=', 'pre')
|
|
||||||
.map<[string, string]>(ware => [ware.stage, this.handlerToString(ware.handler)])
|
.map<[string, string]>(ware => [ware.stage, this.handlerToString(ware.handler)])
|
||||||
|
|
||||||
const post = match.getMiddlewares()
|
const post = match.getMiddlewares()
|
||||||
@ -62,10 +60,6 @@ export class RouteDirective extends Directive {
|
|||||||
table.push(...post.toArray())
|
table.push(...post.toArray())
|
||||||
|
|
||||||
this.info(`\nRoute: ${match}\n\n${table}`)
|
this.info(`\nRoute: ${match}\n\n${table}`)
|
||||||
})
|
})*/
|
||||||
}
|
|
||||||
|
|
||||||
protected handlerToString(handler: RouteHandler): string {
|
|
||||||
return typeof handler === 'string' ? handler : '(anonymous function)'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ export class RoutesDirective extends Directive {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handle(): Promise<void> {
|
async handle(): Promise<void> {
|
||||||
const maxRouteLength = this.routing.getCompiled().max(route => String(route).length)
|
/* const maxRouteLength = this.routing.getCompiled().max(route => String(route).length)
|
||||||
const maxHandlerLength = this.routing.getCompiled().max(route => route.getDisplayableHandler().length)
|
const maxHandlerLength = this.routing.getCompiled().max(route => route.getDisplayableHandler().length)
|
||||||
const rows = this.routing.getCompiled().map<[string, string]>(route => [String(route), route.getDisplayableHandler()])
|
const rows = this.routing.getCompiled().map<[string, string]>(route => [String(route), route.getDisplayableHandler()])
|
||||||
|
|
||||||
@ -28,6 +28,6 @@ export class RoutesDirective extends Directive {
|
|||||||
|
|
||||||
table.push(...rows.toArray())
|
table.push(...rows.toArray())
|
||||||
|
|
||||||
this.info('\n' + table)
|
this.info('\n' + table)*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import {HTTPKernel} from '../HTTPKernel'
|
import {HTTPKernel} from '../HTTPKernel'
|
||||||
import {Request} from '../../lifecycle/Request'
|
import {Request} from '../../lifecycle/Request'
|
||||||
import {ActivatedRoute} from '../../routing/ActivatedRoute'
|
import {ActivatedRoute} from '../../routing/ActivatedRoute'
|
||||||
import {ResponseObject} from '../../routing/Route'
|
|
||||||
import {http} from '../../response/HTTPErrorResponseFactory'
|
import {http} from '../../response/HTTPErrorResponseFactory'
|
||||||
import {HTTPStatus} from '../../../util'
|
import {HTTPStatus} from '../../../util'
|
||||||
import {AbstractResolvedRouteHandlerHTTPModule} from './AbstractResolvedRouteHandlerHTTPModule'
|
import {AbstractResolvedRouteHandlerHTTPModule} from './AbstractResolvedRouteHandlerHTTPModule'
|
||||||
@ -18,10 +17,17 @@ export class ExecuteResolvedRouteHandlerHTTPModule extends AbstractResolvedRoute
|
|||||||
|
|
||||||
public async apply(request: Request): Promise<Request> {
|
public async apply(request: Request): Promise<Request> {
|
||||||
if ( request.hasInstance(ActivatedRoute) ) {
|
if ( request.hasInstance(ActivatedRoute) ) {
|
||||||
const route = <ActivatedRoute> request.make(ActivatedRoute)
|
const route = <ActivatedRoute<unknown, unknown[]>> request.make(ActivatedRoute)
|
||||||
const object: ResponseObject = await route.handler(request)
|
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 {
|
} else {
|
||||||
await http(HTTPStatus.NOT_FOUND).write(request)
|
await http(HTTPStatus.NOT_FOUND).write(request)
|
||||||
request.response.blockingWriteback(true)
|
request.response.blockingWriteback(true)
|
||||||
|
@ -17,7 +17,7 @@ export class ExecuteResolvedRoutePostflightHTTPModule extends AbstractResolvedRo
|
|||||||
|
|
||||||
public async apply(request: Request): Promise<Request> {
|
public async apply(request: Request): Promise<Request> {
|
||||||
if ( request.hasInstance(ActivatedRoute) ) {
|
if ( request.hasInstance(ActivatedRoute) ) {
|
||||||
const route = <ActivatedRoute> request.make(ActivatedRoute)
|
const route = <ActivatedRoute<unknown, unknown[]>> request.make(ActivatedRoute)
|
||||||
const postflight = route.postflight
|
const postflight = route.postflight
|
||||||
|
|
||||||
for ( const handler of postflight ) {
|
for ( const handler of postflight ) {
|
||||||
|
@ -4,6 +4,7 @@ import {Request} from '../../lifecycle/Request'
|
|||||||
import {ActivatedRoute} from '../../routing/ActivatedRoute'
|
import {ActivatedRoute} from '../../routing/ActivatedRoute'
|
||||||
import {ResponseObject} from '../../routing/Route'
|
import {ResponseObject} from '../../routing/Route'
|
||||||
import {AbstractResolvedRouteHandlerHTTPModule} from './AbstractResolvedRouteHandlerHTTPModule'
|
import {AbstractResolvedRouteHandlerHTTPModule} from './AbstractResolvedRouteHandlerHTTPModule'
|
||||||
|
import {collect, isLeft, unleft, unright} from '../../../util'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP Kernel module that executes the preflight handlers for the route.
|
* 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> {
|
public async apply(request: Request): Promise<Request> {
|
||||||
if ( request.hasInstance(ActivatedRoute) ) {
|
if ( request.hasInstance(ActivatedRoute) ) {
|
||||||
const route = <ActivatedRoute> request.make(ActivatedRoute)
|
const route = <ActivatedRoute<unknown, unknown[]>> request.make(ActivatedRoute)
|
||||||
const preflight = route.preflight
|
const preflight = route.preflight
|
||||||
|
|
||||||
for ( const handler of preflight ) {
|
for ( const handler of preflight ) {
|
||||||
@ -27,6 +28,16 @@ export class ExecuteResolvedRoutePreflightHTTPModule extends AbstractResolvedRou
|
|||||||
request.response.blockingWriteback(true)
|
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
|
return request
|
||||||
|
@ -28,8 +28,8 @@ export class MountActivatedRouteHTTPModule extends HTTPKernelModule {
|
|||||||
const route = this.routing.match(request.method, request.path)
|
const route = this.routing.match(request.method, request.path)
|
||||||
if ( route ) {
|
if ( route ) {
|
||||||
this.logging.verbose(`Mounting activated route: ${request.path} -> ${route}`)
|
this.logging.verbose(`Mounting activated route: ${request.path} -> ${route}`)
|
||||||
const activated = <ActivatedRoute> request.make(ActivatedRoute, route, request.path)
|
const activated = <ActivatedRoute<unknown, unknown[]>> request.make(ActivatedRoute, route, request.path)
|
||||||
request.registerSingletonInstance<ActivatedRoute>(ActivatedRoute, activated)
|
request.registerSingletonInstance<ActivatedRoute<unknown, unknown[]>>(ActivatedRoute, activated)
|
||||||
} else {
|
} else {
|
||||||
this.logging.debug(`No matching route found for: ${request.method} -> ${request.path}`)
|
this.logging.debug(`No matching route found for: ${request.method} -> ${request.path}`)
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
import {ErrorWithContext} from '../../util'
|
import {ErrorWithContext} from '../../util'
|
||||||
import {ResolvedRouteHandler, Route} from './Route'
|
import {ParameterProvidingMiddleware, ResolvedRouteHandler, Route} from './Route'
|
||||||
import {Injectable} from '../../di'
|
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.
|
* Class representing a resolved route that a request is mounted to.
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ActivatedRoute {
|
export class ActivatedRoute<TReturn, THandlerParams extends unknown[]> {
|
||||||
/**
|
/**
|
||||||
* The parsed params from the route definition.
|
* 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.
|
* 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.
|
* 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 postflight: ResolvedRouteHandler[]
|
||||||
|
|
||||||
|
public readonly parameters: HandlerParamProviders<THandlerParams>
|
||||||
|
|
||||||
|
public resolvedParameters?: THandlerParams
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
/** The route this ActivatedRoute refers to. */
|
/** The route this ActivatedRoute refers to. */
|
||||||
public readonly route: Route,
|
public readonly route: Route<TReturn, THandlerParams>,
|
||||||
|
|
||||||
/** The request path that activated that route. */
|
/** The request path that activated that route. */
|
||||||
public readonly path: string,
|
public readonly path: string,
|
||||||
@ -56,9 +64,17 @@ export class ActivatedRoute {
|
|||||||
throw error
|
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.params = params
|
||||||
this.preflight = route.resolvePreflight()
|
this.preflight = route.getPreflight().toArray()
|
||||||
this.handler = route.resolveHandler()
|
this.handler = route.handler
|
||||||
this.postflight = route.resolvePostflight()
|
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,
|
protected readonly request: Request,
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
|
if ( !request ) {
|
||||||
|
throw new Error('Middleware constructed without request')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected container(): Container {
|
protected container(): Container {
|
||||||
|
@ -1,56 +1,40 @@
|
|||||||
import {AppClass} from '../../lifecycle/AppClass'
|
import {Collection, Either, ErrorWithContext, Pipeline, PrefixTypeArray, right} from '../../util'
|
||||||
import {HTTPMethod, Request} from '../lifecycle/Request'
|
|
||||||
import {Application} from '../../lifecycle/Application'
|
|
||||||
import {RouteGroup} from './RouteGroup'
|
|
||||||
import {ResponseFactory} from '../response/ResponseFactory'
|
import {ResponseFactory} from '../response/ResponseFactory'
|
||||||
import {Response} from '../lifecycle/Response'
|
import {HTTPMethod, Request} from '../lifecycle/Request'
|
||||||
import {Controllers} from '../../service/Controllers'
|
import {constructable, Constructable, Container, Instantiable, isInstantiableOf, TypedDependencyKey} from '../../di'
|
||||||
import {ErrorWithContext, Collection} from '../../util'
|
|
||||||
import {Container} from '../../di'
|
|
||||||
import {Controller} from '../Controller'
|
|
||||||
import {Middlewares} from '../../service/Middlewares'
|
|
||||||
import {Middleware} from './Middleware'
|
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 {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.
|
* Type alias for an item that is a valid response object, or lack thereof.
|
||||||
*/
|
*/
|
||||||
export type ResponseObject = ResponseFactory | string | number | void | any | Promise<ResponseObject>
|
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.
|
* Type alias for a function that applies a route handler to the request.
|
||||||
* The goal is to transform RouteHandlers to ResolvedRouteHandler.
|
* The goal is to transform RouteHandlers to ResolvedRouteHandler.
|
||||||
*/
|
*/
|
||||||
export type ResolvedRouteHandler = (request: Request) => ResponseObject
|
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.
|
* Set a programmatic name for this route.
|
||||||
*
|
* @param name
|
||||||
* Routes can be defined in nested groups, with prefixes and middleware handlers.
|
*/
|
||||||
*
|
alias(name: string): this
|
||||||
* @example
|
}
|
||||||
* ```typescript
|
|
||||||
* Route.post('/api/v1/ping', (request: Request) => {
|
export class Route<TReturn extends ResponseObject, THandlerParams extends unknown[] = []> {
|
||||||
* return 'pong!'
|
|
||||||
* })
|
|
||||||
*
|
|
||||||
* Route.group('/api/v2', () => {
|
|
||||||
* Route.get('/status', 'controller::api:v2:Status.getStatus').pre('auth:UserOnly')
|
|
||||||
* })
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export class Route extends AppClass {
|
|
||||||
/** Routes that have been created and registered in the application. */
|
/** 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. */
|
/** Groups of routes that have been registered with the application. */
|
||||||
private static registeredGroups: RouteGroup[] = []
|
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
|
* 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.
|
* 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
|
let registeredRoutes = this.registeredRoutes
|
||||||
const registeredGroups = this.registeredGroups
|
const registeredGroups = this.registeredGroups
|
||||||
|
|
||||||
@ -87,55 +71,41 @@ export class Route extends AppClass {
|
|||||||
for ( const route of registeredRoutes ) {
|
for ( const route of registeredRoutes ) {
|
||||||
for ( const group of stack ) {
|
for ( const group of stack ) {
|
||||||
route.prepend(group.prefix)
|
route.prepend(group.prefix)
|
||||||
group.getGroupMiddlewareDefinitions()
|
group.getPreflight()
|
||||||
.where('stage', '=', 'pre')
|
.each(def => route.preflight.prepend(def))
|
||||||
.each(def => {
|
|
||||||
route.prependMiddleware(def)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for ( const group of this.compiledGroupStack ) {
|
for ( const group of this.compiledGroupStack ) {
|
||||||
group.getGroupMiddlewareDefinitions()
|
group.getPostflight()
|
||||||
.where('stage', '=', 'post')
|
.each(def => route.postflight.push(def))
|
||||||
.each(def => route.appendMiddleware(def))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the global pre- and post- middleware
|
// Add the global pre- and post- middleware
|
||||||
if ( Array.isArray(globalMiddleware?.pre) ) {
|
if ( Array.isArray(globalMiddleware?.pre) ) {
|
||||||
const globalPre = [...globalMiddleware.pre].reverse()
|
const globalPre = [...globalMiddleware.pre].reverse()
|
||||||
for ( const item of globalPre ) {
|
for ( const item of globalPre ) {
|
||||||
if ( typeof item !== 'string' ) {
|
if ( !isInstantiableOf(item, Middleware) ) {
|
||||||
throw new ErrorWithContext(`Invalid global pre-middleware definition. Global middleware must be string-references.`, {
|
throw new ErrorWithContext(`Invalid global pre-middleware definition. Global middleware must be static references to Middleware implementations.`, {
|
||||||
configKey: 'server.middleware.global.pre',
|
configKey: 'server.middleware.global.pre',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
route.prependMiddleware({
|
route.preflight.prepend(request => request.make<Middleware>(item, request).apply())
|
||||||
stage: 'pre',
|
|
||||||
handler: item,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( Array.isArray(globalMiddleware?.post) ) {
|
if ( Array.isArray(globalMiddleware?.post) ) {
|
||||||
const globalPost = [...globalMiddleware.post]
|
const globalPost = [...globalMiddleware.post]
|
||||||
for ( const item of globalPost ) {
|
for ( const item of globalPost ) {
|
||||||
if ( typeof item !== 'string' ) {
|
if ( !isInstantiableOf(item, Middleware) ) {
|
||||||
throw new ErrorWithContext(`Invalid global post-middleware definition. Global middleware must be string-references.`, {
|
throw new ErrorWithContext(`Invalid global post-middleware definition. Global middleware must be static references to Middleware implementations.`, {
|
||||||
configKey: 'server.middleware.global.post',
|
configKey: 'server.middleware.global.post',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
route.appendMiddleware({
|
route.postflight.push(request => request.make<Middleware>(item, request).apply())
|
||||||
stage: 'post',
|
|
||||||
handler: item,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 ) {
|
for ( const group of registeredGroups ) {
|
||||||
@ -151,50 +121,46 @@ export class Route extends AppClass {
|
|||||||
return registeredRoutes
|
return registeredRoutes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new route on the given endpoint for the given HTTP verb.
|
* Create a new route on the given endpoint for the given HTTP verb.
|
||||||
* @param method
|
* @param method
|
||||||
* @param definition
|
* @param endpoint
|
||||||
* @param handler
|
|
||||||
*/
|
*/
|
||||||
public static endpoint(method: HTTPMethod | HTTPMethod[], definition: string, handler: RouteHandler): Route {
|
public static endpoint(method: HTTPMethod | HTTPMethod[], endpoint: string): Route<ResponseObject> {
|
||||||
const route = new Route(method, handler, definition)
|
return new Route(method, endpoint)
|
||||||
this.registeredRoutes.push(route)
|
|
||||||
return route
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new GET route on the given endpoint.
|
* Create a new GET route on the given endpoint.
|
||||||
* @param definition
|
|
||||||
* @param handler
|
|
||||||
*/
|
*/
|
||||||
public static get(definition: string, handler: RouteHandler): Route {
|
public static get(endpoint: string): Route<ResponseObject> {
|
||||||
return this.endpoint('get', definition, handler)
|
return this.endpoint('get', endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Create a new POST route on the given endpoint. */
|
/** Create a new POST route on the given endpoint. */
|
||||||
public static post(definition: string, handler: RouteHandler): Route {
|
public static post(endpoint: string): Route<ResponseObject> {
|
||||||
return this.endpoint('post', definition, handler)
|
return this.endpoint('post', endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Create a new PUT route on the given endpoint. */
|
/** Create a new PUT route on the given endpoint. */
|
||||||
public static put(definition: string, handler: RouteHandler): Route {
|
public static put(endpoint: string): Route<ResponseObject> {
|
||||||
return this.endpoint('put', definition, handler)
|
return this.endpoint('put', endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Create a new PATCH route on the given endpoint. */
|
/** Create a new PATCH route on the given endpoint. */
|
||||||
public static patch(definition: string, handler: RouteHandler): Route {
|
public static patch(endpoint: string): Route<ResponseObject> {
|
||||||
return this.endpoint('patch', definition, handler)
|
return this.endpoint('patch', endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Create a new DELETE route on the given endpoint. */
|
/** Create a new DELETE route on the given endpoint. */
|
||||||
public static delete(definition: string, handler: RouteHandler): Route {
|
public static delete(endpoint: string): Route<ResponseObject> {
|
||||||
return this.endpoint('delete', definition, handler)
|
return this.endpoint('delete', endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Create a new route on all HTTP verbs, on the given endpoint. */
|
/** Create a new route on all HTTP verbs, on the given endpoint. */
|
||||||
public static any(definition: string, handler: RouteHandler): Route {
|
public static any(endpoint: string): Route<ResponseObject> {
|
||||||
return this.endpoint(['get', 'put', 'patch', 'post', 'delete'], definition, handler)
|
return this.endpoint(['get', 'put', 'patch', 'post', 'delete'], endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Create a new route group with the given prefix. */
|
/** Create a new route group with the given prefix. */
|
||||||
@ -204,35 +170,20 @@ export class Route extends AppClass {
|
|||||||
return grp
|
return grp
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Middlewares that should be applied to this route. */
|
protected preflight: Collection<ResolvedRouteHandler> = new Collection<ResolvedRouteHandler>()
|
||||||
protected middlewares: Collection<{ stage: 'pre' | 'post', handler: RouteHandler }> = new Collection<{stage: 'pre' | 'post'; handler: RouteHandler}>()
|
|
||||||
|
|
||||||
/** Pre-compiled route handlers for the pre-middleware for this route. */
|
protected parameters: Collection<ParameterProvidingMiddleware<unknown>> = new Collection<ParameterProvidingMiddleware<unknown>>()
|
||||||
protected compiledPreflight?: ResolvedRouteHandler[]
|
|
||||||
|
|
||||||
/** Pre-compiled route handlers for the post-middleware for this route. */
|
protected postflight: Collection<ResolvedRouteHandler> = new Collection<ResolvedRouteHandler>()
|
||||||
protected compiledHandler?: ResolvedRouteHandler
|
|
||||||
|
|
||||||
/** Pre-compiled route handler for the main route handler for this route. */
|
protected aliases: Collection<string> = new Collection<string>()
|
||||||
protected compiledPostflight?: ResolvedRouteHandler[]
|
|
||||||
|
|
||||||
protected validator?: Validator<unknown>
|
handler?: Constructable<(...x: THandlerParams) => TReturn>
|
||||||
|
|
||||||
/** Programmatic aliases of this route. */
|
|
||||||
public aliases: string[] = []
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
/** The HTTP method(s) that this route listens on. */
|
|
||||||
protected method: HTTPMethod | HTTPMethod[],
|
protected method: HTTPMethod | HTTPMethod[],
|
||||||
|
|
||||||
/** The primary handler of this route. */
|
|
||||||
protected readonly handler: RouteHandler,
|
|
||||||
|
|
||||||
/** The route path this route listens on. */
|
|
||||||
protected route: string,
|
protected route: string,
|
||||||
) {
|
) {}
|
||||||
super()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set a programmatic name for this route.
|
* 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
|
return this.method
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get collection of applied middlewares.
|
* Get preflight middleware for this route.
|
||||||
*/
|
*/
|
||||||
public getMiddlewares(): Collection<{ stage: 'pre' | 'post', handler: RouteHandler }> {
|
public getPreflight(): Collection<ResolvedRouteHandler> {
|
||||||
return this.middlewares.clone()
|
return this.preflight.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the string-form of the route handler.
|
* Get postflight middleware for this route.
|
||||||
*/
|
*/
|
||||||
public getDisplayableHandler(): string {
|
public getPostflight(): Collection<ResolvedRouteHandler> {
|
||||||
return typeof this.handler === 'string' ? this.handler : '(anonymous function)'
|
return this.postflight.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
public getParameters(): Collection<ParameterProvidingMiddleware<unknown>> {
|
||||||
|
return this.parameters.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -337,66 +296,80 @@ export class Route extends AppClass {
|
|||||||
return params
|
return params
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public parameterMiddleware<T>(
|
||||||
* Try to pre-compile and return the preflight handlers for this route.
|
handler: ParameterProvidingMiddleware<T>,
|
||||||
*/
|
): Route<TReturn, PrefixTypeArray<T, THandlerParams>> {
|
||||||
public resolvePreflight(): ResolvedRouteHandler[] {
|
const route = new Route<TReturn, PrefixTypeArray<T, THandlerParams>>(
|
||||||
if ( !this.compiledPreflight ) {
|
this.method,
|
||||||
this.compiledPreflight = this.resolveMiddlewareHandlersForStage('pre')
|
this.route,
|
||||||
}
|
)
|
||||||
|
|
||||||
return this.compiledPreflight
|
route.copyFrom(this)
|
||||||
|
route.parameters.push(handler)
|
||||||
|
return route
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private copyFrom(other: Route<TReturn, any>) {
|
||||||
* Try to pre-compile and return the postflight handlers for this route.
|
this.preflight = other.preflight.clone()
|
||||||
*/
|
this.postflight = other.postflight.clone()
|
||||||
public resolvePostflight(): ResolvedRouteHandler[] {
|
this.aliases = other.aliases.clone()
|
||||||
if ( !this.compiledPostflight ) {
|
|
||||||
this.compiledPostflight = this.resolveMiddlewareHandlersForStage('post')
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.compiledPostflight
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public calls<TKey>(
|
||||||
* Try to pre-compile and return the main handler for this route.
|
key: TypedDependencyKey<TKey>,
|
||||||
*/
|
selector: (x: TKey) => (...params: THandlerParams) => TReturn,
|
||||||
public resolveHandler(): ResolvedRouteHandler {
|
): HandledRoute<TReturn, THandlerParams> {
|
||||||
if ( !this.compiledHandler ) {
|
this.handler = constructable<TKey>(key)
|
||||||
this.compiledHandler = this.compileResolvedHandler()
|
.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. */
|
public handledBy(
|
||||||
pre(middleware: RouteHandler): this {
|
handler: (...params: THandlerParams) => TReturn,
|
||||||
this.middlewares.push({
|
): HandledRoute<TReturn, THandlerParams> {
|
||||||
stage: 'pre',
|
this.handler = Pipeline.id<Container>()
|
||||||
handler: middleware,
|
.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
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Register the given middleware as a postflight handler for this route. */
|
public post(middleware: Instantiable<Middleware>): this {
|
||||||
post(middleware: RouteHandler): this {
|
this.postflight.push(request => request.make<Middleware>(middleware, request).apply())
|
||||||
this.middlewares.push({
|
|
||||||
stage: 'post',
|
|
||||||
handler: middleware,
|
|
||||||
})
|
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
input(validator: Validator<any>): this {
|
public input<T extends Validator<T>>(validator: ValidatorFactory<T>): Route<TReturn, PrefixTypeArray<Valid<T>, THandlerParams>> {
|
||||||
if ( !this.validator ) {
|
if ( !(validator instanceof Validator) ) {
|
||||||
//
|
validator = validator()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.validator = validator
|
return this.parameterMiddleware(validateMiddleware(validator))
|
||||||
return this
|
}
|
||||||
|
|
||||||
|
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. */
|
/** 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}`
|
this.route = `${prefix}${this.route}`
|
||||||
return this
|
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 {Collection, ErrorWithContext} from '../../util'
|
||||||
import {AppClass} from '../../lifecycle/AppClass'
|
import {AppClass} from '../../lifecycle/AppClass'
|
||||||
import {RouteHandler} from './Route'
|
import {ResolvedRouteHandler} from './Route'
|
||||||
import {Container} from '../../di'
|
import {Container} from '../../di'
|
||||||
import {Logging} from '../../service/Logging'
|
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.
|
* Class that defines a group of Routes in the application, with a prefix.
|
||||||
*/
|
*/
|
||||||
export class RouteGroup extends AppClass {
|
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.
|
* The current set of nested groups. This is used when compiling route groups.
|
||||||
* @private
|
* @private
|
||||||
@ -20,12 +24,6 @@ export class RouteGroup extends AppClass {
|
|||||||
*/
|
*/
|
||||||
protected static namedGroups: {[key: string]: () => void } = {}
|
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.
|
* 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. */
|
/** Register the given middleware to be applied before all routes in this group. */
|
||||||
pre(middleware: RouteHandler): this {
|
pre(middleware: ResolvedRouteHandler): this {
|
||||||
this.middlewares.push({
|
this.preflight.push(middleware)
|
||||||
stage: 'pre',
|
|
||||||
handler: middleware,
|
|
||||||
})
|
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Register the given middleware to be applied after all routes in this group. */
|
/** Register the given middleware to be applied after all routes in this group. */
|
||||||
post(middleware: RouteHandler): this {
|
post(middleware: ResolvedRouteHandler): this {
|
||||||
this.middlewares.push({
|
this.postflight.push(middleware)
|
||||||
stage: 'post',
|
|
||||||
handler: middleware,
|
|
||||||
})
|
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Return the middlewares that apply to this group. */
|
getPreflight(): Collection<ResolvedRouteHandler> {
|
||||||
getGroupMiddlewareDefinitions(): Collection<{ stage: 'pre' | 'post', handler: RouteHandler }> {
|
return this.preflight
|
||||||
return this.middlewares
|
}
|
||||||
|
|
||||||
|
getPostflight(): Collection<ResolvedRouteHandler> {
|
||||||
|
return this.postflight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,8 @@ import {HTTPError} from '../HTTPError'
|
|||||||
import {view, ViewResponseFactory} from '../response/ViewResponseFactory'
|
import {view, ViewResponseFactory} from '../response/ViewResponseFactory'
|
||||||
import {redirect} from '../response/RedirectResponseFactory'
|
import {redirect} from '../response/RedirectResponseFactory'
|
||||||
import {file} from '../response/FileResponseFactory'
|
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.
|
* 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.
|
* Get a route handler that serves a directory as static files.
|
||||||
* @param options
|
* @param options
|
||||||
*/
|
*/
|
||||||
export function staticServer(options: StaticServerOptions = {}): RouteHandler {
|
export function staticServer(options: StaticServerOptions = {}): (request: Request) => Promise<ResponseObject> {
|
||||||
return async (request: Request) => {
|
return async (request: Request) => {
|
||||||
const config = <Config> request.make(Config)
|
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 app = <Application> request.make(Application)
|
||||||
|
const logging = <Logging> request.make(Logging)
|
||||||
|
|
||||||
const staticConfig = config.get('server.builtIns.static', {})
|
const staticConfig = config.get('server.builtIns.static', {})
|
||||||
const mergedOptions = {
|
const mergedOptions = {
|
||||||
@ -183,6 +185,7 @@ export function staticServer(options: StaticServerOptions = {}): RouteHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, just send the file as the response body
|
// Otherwise, just send the file as the response body
|
||||||
|
logging.verbose(`Sending file: ${filePath}`)
|
||||||
return file(filePath)
|
return file(filePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ export class Routing extends Unit {
|
|||||||
@Inject()
|
@Inject()
|
||||||
protected readonly bus!: EventBus
|
protected readonly bus!: EventBus
|
||||||
|
|
||||||
protected compiledRoutes: Collection<Route> = new Collection<Route>()
|
protected compiledRoutes: Collection<Route<unknown, unknown[]>> = new Collection<Route<unknown, unknown[]>>()
|
||||||
|
|
||||||
public async up(): Promise<void> {
|
public async up(): Promise<void> {
|
||||||
this.app().registerFactory(new ViewEngineFactory())
|
this.app().registerFactory(new ViewEngineFactory())
|
||||||
@ -47,7 +47,7 @@ export class Routing extends Unit {
|
|||||||
await this.registerBuiltIns()
|
await this.registerBuiltIns()
|
||||||
|
|
||||||
this.logging.info('Compiling routes...')
|
this.logging.info('Compiling routes...')
|
||||||
this.compiledRoutes = new Collection<Route>(await Route.compile())
|
this.compiledRoutes = new Collection<Route<unknown, unknown[]>>(await Route.compile())
|
||||||
|
|
||||||
this.logging.info(`Compiled ${this.compiledRoutes.length} route(s).`)
|
this.logging.info(`Compiled ${this.compiledRoutes.length} route(s).`)
|
||||||
this.compiledRoutes.each(route => {
|
this.compiledRoutes.each(route => {
|
||||||
@ -85,7 +85,7 @@ export class Routing extends Unit {
|
|||||||
*/
|
*/
|
||||||
public async recompile(): Promise<void> {
|
public async recompile(): Promise<void> {
|
||||||
this.logging.debug('Recompiling routes...')
|
this.logging.debug('Recompiling routes...')
|
||||||
this.compiledRoutes = this.compiledRoutes.concat(new Collection<Route>(await Route.compile()))
|
this.compiledRoutes = this.compiledRoutes.concat(new Collection<Route<unknown, unknown[]>>(await Route.compile()))
|
||||||
|
|
||||||
this.logging.debug(`Re-compiled ${this.compiledRoutes.length} route(s).`)
|
this.logging.debug(`Re-compiled ${this.compiledRoutes.length} route(s).`)
|
||||||
this.compiledRoutes.each(route => {
|
this.compiledRoutes.each(route => {
|
||||||
@ -99,7 +99,7 @@ export class Routing extends Unit {
|
|||||||
* @param method
|
* @param method
|
||||||
* @param path
|
* @param path
|
||||||
*/
|
*/
|
||||||
public match(method: HTTPMethod, path: string): Route | undefined {
|
public match(method: HTTPMethod, path: string): Route<unknown, unknown[]> | undefined {
|
||||||
return this.compiledRoutes.firstWhere(route => {
|
return this.compiledRoutes.firstWhere(route => {
|
||||||
return route.match(method, path)
|
return route.match(method, path)
|
||||||
})
|
})
|
||||||
@ -115,7 +115,7 @@ export class Routing extends Unit {
|
|||||||
/**
|
/**
|
||||||
* Get the collection of compiled routes.
|
* Get the collection of compiled routes.
|
||||||
*/
|
*/
|
||||||
public getCompiled(): Collection<Route> {
|
public getCompiled(): Collection<Route<unknown, unknown[]>> {
|
||||||
return this.compiledRoutes
|
return this.compiledRoutes
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,9 +158,9 @@ export class Routing extends Unit {
|
|||||||
return Boolean(this.getByName(name))
|
return Boolean(this.getByName(name))
|
||||||
}
|
}
|
||||||
|
|
||||||
public getByName(name: string): Maybe<Route> {
|
public getByName(name: string): Maybe<Route<unknown, unknown[]>> {
|
||||||
return this.compiledRoutes
|
return this.compiledRoutes
|
||||||
.firstWhere(route => route.aliases.includes(name))
|
.firstWhere(route => route.hasAlias(name))
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAppUrl(): UniversalPath {
|
public getAppUrl(): UniversalPath {
|
||||||
@ -204,10 +204,12 @@ export class Routing extends Unit {
|
|||||||
const prefix = this.config.get('server.builtIns.vendor.prefix', '/vendor')
|
const prefix = this.config.get('server.builtIns.vendor.prefix', '/vendor')
|
||||||
Route.group(prefix, () => {
|
Route.group(prefix, () => {
|
||||||
Route.group(packageName, () => {
|
Route.group(packageName, () => {
|
||||||
Route.get('/**', staticServer({
|
Route.get('/**')
|
||||||
basePath,
|
.passingRequest()
|
||||||
directoryListing: false,
|
.handledBy(staticServer({
|
||||||
}))
|
basePath,
|
||||||
|
directoryListing: false,
|
||||||
|
}))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -235,10 +237,12 @@ export class Routing extends Unit {
|
|||||||
this.logging.debug(`Registering built-in assets server with prefix: ${prefix}`)
|
this.logging.debug(`Registering built-in assets server with prefix: ${prefix}`)
|
||||||
await this.registerRoutes(() => {
|
await this.registerRoutes(() => {
|
||||||
Route.group(prefix, () => {
|
Route.group(prefix, () => {
|
||||||
Route.get('/**', staticServer({
|
Route.get('/**')
|
||||||
directoryListing: false,
|
.passingRequest()
|
||||||
basePath: ['resources', 'assets'],
|
.handledBy(staticServer({
|
||||||
}))
|
directoryListing: false,
|
||||||
|
basePath: ['resources', 'assets'],
|
||||||
|
}))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ type MaybeCollectionIndex = CollectionIndex | undefined
|
|||||||
type ComparisonFunction<T> = (item: CollectionItem<T>, otherItem: CollectionItem<T>) => number
|
type ComparisonFunction<T> = (item: CollectionItem<T>, otherItem: CollectionItem<T>) => number
|
||||||
|
|
||||||
import { WhereOperator, applyWhere, whereMatch } from './where'
|
import { WhereOperator, applyWhere, whereMatch } from './where'
|
||||||
|
import {Awaitable, Either, isLeft, right, unright} from '../support/types'
|
||||||
|
|
||||||
const collect = <T>(items: CollectionItem<T>[]): Collection<T> => Collection.collect(items)
|
const collect = <T>(items: CollectionItem<T>[]): Collection<T> => Collection.collect(items)
|
||||||
const toString = (item: unknown): string => String(item)
|
const toString = (item: unknown): string => String(item)
|
||||||
@ -316,6 +317,44 @@ class Collection<T> {
|
|||||||
return new Collection<T2>(newItems)
|
return new Collection<T2>(newItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new collection by mapping the items in this collection using the given function
|
||||||
|
* where the function returns an Either. The collection is all Right instances. If a Left
|
||||||
|
* is encountered, that value is returned.
|
||||||
|
* @param func
|
||||||
|
*/
|
||||||
|
mapRight<TLeft, TRight>(func: KeyFunction<T, Either<TLeft, TRight>>): Either<TLeft, Collection<TRight>> {
|
||||||
|
const newItems: CollectionItem<TRight>[] = []
|
||||||
|
for ( let i = 0; i < this.length; i += 1 ) {
|
||||||
|
const result = func(this.storedItems[i], i)
|
||||||
|
if ( isLeft(result) ) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
newItems.push(unright(result))
|
||||||
|
}
|
||||||
|
return right(new Collection<TRight>(newItems))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new collection by mapping the items in this collection using the given function
|
||||||
|
* where the function returns an Either. The collection is all Right instances. If a Left
|
||||||
|
* is encountered, that value is returned.
|
||||||
|
* @param func
|
||||||
|
*/
|
||||||
|
async asyncMapRight<TLeft, TRight>(func: KeyFunction<T, Awaitable<Either<TLeft, TRight>>>): Promise<Either<TLeft, Collection<TRight>>> {
|
||||||
|
const newItems: CollectionItem<TRight>[] = []
|
||||||
|
for ( let i = 0; i < this.length; i += 1 ) {
|
||||||
|
const result = await func(this.storedItems[i], i)
|
||||||
|
if ( isLeft(result) ) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
newItems.push(unright(result))
|
||||||
|
}
|
||||||
|
return right(new Collection<TRight>(newItems))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new collection by mapping the items in this collection using the given function,
|
* Create a new collection by mapping the items in this collection using the given function,
|
||||||
* excluding any for which the function returns undefined.
|
* excluding any for which the function returns undefined.
|
||||||
|
@ -4,6 +4,10 @@ export type Awaitable<T> = T | Promise<T>
|
|||||||
/** Type alias for something that may be undefined. */
|
/** Type alias for something that may be undefined. */
|
||||||
export type Maybe<T> = T | undefined
|
export type Maybe<T> = T | undefined
|
||||||
|
|
||||||
|
export type MaybeArr<T extends [...any[]]> = {
|
||||||
|
[Index in keyof T]: Maybe<T[Index]>
|
||||||
|
} & {length: T['length']}
|
||||||
|
|
||||||
export type Either<T1, T2> = Left<T1> | Right<T2>
|
export type Either<T1, T2> = Left<T1> | Right<T2>
|
||||||
|
|
||||||
export type Left<T> = [T, undefined]
|
export type Left<T> = [T, undefined]
|
||||||
@ -26,6 +30,14 @@ export function right<T>(what: T): Right<T> {
|
|||||||
return [undefined, what]
|
return [undefined, what]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function unleft<T>(what: Left<T>): T {
|
||||||
|
return what[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unright<T>(what: Right<T>): T {
|
||||||
|
return what[1]
|
||||||
|
}
|
||||||
|
|
||||||
/** Type alias for a callback that accepts a typed argument. */
|
/** Type alias for a callback that accepts a typed argument. */
|
||||||
export type ParameterizedCallback<T> = ((arg: T) => any)
|
export type ParameterizedCallback<T> = ((arg: T) => any)
|
||||||
|
|
||||||
|
@ -8,6 +8,8 @@ import {Logging} from '../service/Logging'
|
|||||||
/** Type tag for a validated runtime type. */
|
/** Type tag for a validated runtime type. */
|
||||||
export type Valid<T> = TypeTag<'@extollo/lib:Valid'> & T
|
export type Valid<T> = TypeTag<'@extollo/lib:Valid'> & T
|
||||||
|
|
||||||
|
export type ValidatorFactory<T extends Validator<T>> = T | (() => T)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Error thrown if the schema for a validator cannot be located.
|
* Error thrown if the schema for a validator cannot be located.
|
||||||
*/
|
*/
|
||||||
|
22
src/validation/middleware.ts
Normal file
22
src/validation/middleware.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import {Request} from '../http/lifecycle/Request'
|
||||||
|
import {Validator} from './Validator'
|
||||||
|
import {ZodError} from 'zod'
|
||||||
|
import {HTTPStatus, left, right} from '../util'
|
||||||
|
import {json} from '../http/response/JSONResponseFactory'
|
||||||
|
|
||||||
|
export function validateMiddleware<T>(validator: Validator<T>) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
|
return (request: Request) => {
|
||||||
|
try {
|
||||||
|
const data = validator.parse(request.parsedInput)
|
||||||
|
return right(data)
|
||||||
|
} catch (e) {
|
||||||
|
if ( e instanceof ZodError ) {
|
||||||
|
// FIXME render this better
|
||||||
|
return left(json(e.formErrors).status(HTTPStatus.BAD_REQUEST))
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user