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