Make new routing system the default
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
parent
8cf19792a6
commit
dc16dfdb81
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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