TypeDoc all the thngs
This commit is contained in:
@@ -1,14 +1,47 @@
|
||||
import {ErrorWithContext} from "@extollo/util";
|
||||
import {ResolvedRouteHandler, Route} from "./Route";
|
||||
|
||||
/**
|
||||
* Class representing a resolved route that a request is mounted to.
|
||||
*/
|
||||
export class ActivatedRoute {
|
||||
/**
|
||||
* The parsed params from the route definition.
|
||||
*
|
||||
* @example
|
||||
* If the route definition is like `/something/something/:paramName1/:paramName2/etc`
|
||||
* and the request came in on `/something/something/foo/bar/etc`, then the params
|
||||
* would be:
|
||||
*
|
||||
* ```typescript
|
||||
* {
|
||||
* paramName1: 'foo',
|
||||
* paramName2: 'bar',
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
public readonly params: {[key: string]: string}
|
||||
|
||||
/**
|
||||
* The resolved function that should handle the request for this route.
|
||||
*/
|
||||
public readonly handler: ResolvedRouteHandler
|
||||
|
||||
/**
|
||||
* Pre-middleware that should be applied to the request on this route.
|
||||
*/
|
||||
public readonly preflight: ResolvedRouteHandler[]
|
||||
|
||||
/**
|
||||
* Post-middleware that should be applied to the request on this route.
|
||||
*/
|
||||
public readonly postflight: ResolvedRouteHandler[]
|
||||
|
||||
constructor(
|
||||
/** The route this ActivatedRoute refers to. */
|
||||
public readonly route: Route,
|
||||
|
||||
/** The request path that activated that route. */
|
||||
public readonly path: string
|
||||
) {
|
||||
const params = route.extract(path)
|
||||
|
||||
@@ -2,8 +2,12 @@ import {AppClass} from "../../lifecycle/AppClass"
|
||||
import {Request} from "../lifecycle/Request"
|
||||
import {ResponseObject} from "./Route"
|
||||
|
||||
/**
|
||||
* Base class representing a middleware handler that can be applied to routes.
|
||||
*/
|
||||
export abstract class Middleware extends AppClass {
|
||||
constructor(
|
||||
/** The request that will be handled by this middleware. */
|
||||
protected readonly request: Request
|
||||
) { super() }
|
||||
|
||||
@@ -11,6 +15,13 @@ export abstract class Middleware extends AppClass {
|
||||
return this.request
|
||||
}
|
||||
|
||||
// Return void | Promise<void> to continue request
|
||||
/**
|
||||
* Apply the middleware to the request.
|
||||
* If this returns a response factory or similar item, that will be sent
|
||||
* as a response.
|
||||
*
|
||||
* If this returns `void | Promise<void>`, the request will continue to the
|
||||
* next handler.
|
||||
*/
|
||||
public abstract apply(): ResponseObject
|
||||
}
|
||||
|
||||
@@ -12,23 +12,66 @@ import {Middlewares} from "../../service/Middlewares";
|
||||
import {Middleware} from "./Middleware";
|
||||
import {Config} from "../../service/Config";
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
|
||||
// TODO domains, named routes - support this on groups as well
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
/** Routes that have been created and registered in the application. */
|
||||
private static registeredRoutes: Route[] = []
|
||||
|
||||
/** Groups of routes that have been registered with the application. */
|
||||
private static registeredGroups: RouteGroup[] = []
|
||||
|
||||
/**
|
||||
* The current nested group stack. This is used internally when compiling the routes by nested group.
|
||||
* @private
|
||||
*/
|
||||
private static compiledGroupStack: RouteGroup[] = []
|
||||
|
||||
/** Register a route group handler. */
|
||||
public static registerGroup(group: RouteGroup) {
|
||||
this.registeredGroups.push(group)
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and compile all of the registered routes and their groups, accounting
|
||||
* for nested groups and resolving handlers.
|
||||
*
|
||||
* 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[]> {
|
||||
let registeredRoutes = this.registeredRoutes
|
||||
const registeredGroups = this.registeredGroups
|
||||
@@ -103,53 +146,87 @@ 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
|
||||
*/
|
||||
public static endpoint(method: HTTPMethod | HTTPMethod[], definition: string, handler: RouteHandler) {
|
||||
const route = new Route(method, handler, definition)
|
||||
this.registeredRoutes.push(route)
|
||||
return route
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new GET route on the given endpoint.
|
||||
* @param definition
|
||||
* @param handler
|
||||
*/
|
||||
public static get(definition: string, handler: RouteHandler) {
|
||||
return this.endpoint('get', definition, handler)
|
||||
}
|
||||
|
||||
/** Create a new POST route on the given endpoint. */
|
||||
public static post(definition: string, handler: RouteHandler) {
|
||||
return this.endpoint('post', definition, handler)
|
||||
}
|
||||
|
||||
/** Create a new PUT route on the given endpoint. */
|
||||
public static put(definition: string, handler: RouteHandler) {
|
||||
return this.endpoint('put', definition, handler)
|
||||
}
|
||||
|
||||
/** Create a new PATCH route on the given endpoint. */
|
||||
public static patch(definition: string, handler: RouteHandler) {
|
||||
return this.endpoint('patch', definition, handler)
|
||||
}
|
||||
|
||||
/** Create a new DELETE route on the given endpoint. */
|
||||
public static delete(definition: string, handler: RouteHandler) {
|
||||
return this.endpoint('delete', definition, handler)
|
||||
}
|
||||
|
||||
/** Create a new route on all HTTP verbs, on the given endpoint. */
|
||||
public static any(definition: string, handler: RouteHandler) {
|
||||
return this.endpoint(['get', 'put', 'patch', 'post', 'delete'], definition, handler)
|
||||
}
|
||||
|
||||
/** Create a new route group with the given prefix. */
|
||||
public static group(prefix: string, group: () => void | Promise<void>) {
|
||||
const grp = <RouteGroup> Application.getApplication().make(RouteGroup, group, prefix)
|
||||
this.registeredGroups.push(grp)
|
||||
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}>()
|
||||
|
||||
/** Pre-compiled route handlers for the pre-middleware for this route. */
|
||||
protected _compiledPreflight?: ResolvedRouteHandler[]
|
||||
|
||||
/** Pre-compiled route handlers for the post-middleware for this route. */
|
||||
protected _compiledHandler?: ResolvedRouteHandler
|
||||
|
||||
/** Pre-compiled route handler for the main route handler for this route. */
|
||||
protected _compiledPostflight?: ResolvedRouteHandler[]
|
||||
|
||||
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() }
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -157,6 +234,20 @@ export class Route extends AppClass {
|
||||
return !!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('/')
|
||||
@@ -192,6 +283,9 @@ 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')
|
||||
@@ -200,6 +294,9 @@ export class Route extends AppClass {
|
||||
return this._compiledPreflight
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to pre-compile and return the postflight handlers for this route.
|
||||
*/
|
||||
public resolvePostflight(): ResolvedRouteHandler[] {
|
||||
if ( !this._compiledPostflight ) {
|
||||
this._compiledPostflight = this.resolveMiddlewareHandlersForStage('post')
|
||||
@@ -208,6 +305,9 @@ export class Route extends AppClass {
|
||||
return this._compiledPostflight
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to pre-compile and return the main handler for this route.
|
||||
*/
|
||||
public resolveHandler(): ResolvedRouteHandler {
|
||||
if ( !this._compiledHandler ) {
|
||||
this._compiledHandler = this._resolveHandler()
|
||||
@@ -216,6 +316,7 @@ export class Route extends AppClass {
|
||||
return this._compiledHandler
|
||||
}
|
||||
|
||||
/** Register the given middleware as a preflight handler for this route. */
|
||||
pre(middleware: RouteHandler) {
|
||||
this.middlewares.push({
|
||||
stage: 'pre',
|
||||
@@ -225,6 +326,7 @@ export class Route extends AppClass {
|
||||
return this
|
||||
}
|
||||
|
||||
/** Register the given middleware as a postflight handler for this route. */
|
||||
post(middleware: RouteHandler) {
|
||||
this.middlewares.push({
|
||||
stage: 'post',
|
||||
@@ -234,20 +336,27 @@ export class Route extends AppClass {
|
||||
return this
|
||||
}
|
||||
|
||||
/** Prefix the route's path with the given prefix, normalizing `/` characters. */
|
||||
private prepend(prefix: string) {
|
||||
if ( !prefix.endsWith('/') ) prefix = `${prefix}/`
|
||||
if ( this.route.startsWith('/') ) this.route = this.route.substring(1)
|
||||
this.route = `${prefix}${this.route}`
|
||||
}
|
||||
|
||||
/** Add the given middleware item to the beginning of the preflight handlers. */
|
||||
private prependMiddleware(def: { stage: 'pre' | 'post', handler: RouteHandler }) {
|
||||
this.middlewares.prepend(def)
|
||||
}
|
||||
|
||||
/** Add the given middleware item to the end of the postflight handlers. */
|
||||
private appendMiddleware(def: { stage: 'pre' | 'post', handler: RouteHandler }) {
|
||||
this.middlewares.push(def)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve and return the route handler for this route.
|
||||
* @private
|
||||
*/
|
||||
private _resolveHandler(): ResolvedRouteHandler {
|
||||
if ( typeof this.handler !== 'string' ) {
|
||||
return (request: Request) => {
|
||||
@@ -289,6 +398,11 @@ export class Route extends AppClass {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 => {
|
||||
@@ -330,6 +444,7 @@ export class Route extends AppClass {
|
||||
.toArray()
|
||||
}
|
||||
|
||||
/** Cast the route to an intelligible string. */
|
||||
toString() {
|
||||
const method = Array.isArray(this.method) ? this.method : [this.method]
|
||||
return `${method.join('|')} -> ${this.route}`
|
||||
|
||||
@@ -4,17 +4,50 @@ import {RouteHandler} from "./Route"
|
||||
import {Container} from "@extollo/di"
|
||||
import {Logging} from "../../service/Logging";
|
||||
|
||||
/**
|
||||
* Class that defines a group of Routes in the application, with a prefix.
|
||||
*/
|
||||
export class RouteGroup extends AppClass {
|
||||
/**
|
||||
* The current set of nested groups. This is used when compiling route groups.
|
||||
* @private
|
||||
*/
|
||||
private static currentGroupNesting: RouteGroup[] = []
|
||||
|
||||
/**
|
||||
* Mapping of group names to group registration functions.
|
||||
* @protected
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
public static getCurrentGroupHierarchy(): RouteGroup[] {
|
||||
return [...this.currentGroupNesting]
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new named group that can be registered at a later time, by name.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* RouteGroup.named('auth', () => {
|
||||
* Route.group('/auth', () => {
|
||||
* Route.get('/login', 'auth:Forms.getLogin')
|
||||
* })
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* @param name
|
||||
* @param define
|
||||
*/
|
||||
public static named(name: string, define: () => void) {
|
||||
if ( this.namedGroups[name] ) {
|
||||
Container.getContainer()
|
||||
@@ -25,6 +58,17 @@ export class RouteGroup extends AppClass {
|
||||
this.namedGroups[name] = define
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the routes from a named group by calling its registration function.
|
||||
*
|
||||
* @example
|
||||
* From the example above, we can register the auth `/auth/*` routes, like so:
|
||||
* ```typescript
|
||||
* RouteGroup.include('auth')
|
||||
* ```
|
||||
*
|
||||
* @param name
|
||||
*/
|
||||
public static include(name: string) {
|
||||
if (!this.namedGroups[name]) {
|
||||
throw new ErrorWithContext(`No route group exists with name: ${name}`, {name})
|
||||
@@ -35,10 +79,14 @@ export class RouteGroup extends AppClass {
|
||||
|
||||
|
||||
constructor(
|
||||
/** Function to register routes for this group. */
|
||||
public readonly group: () => void | Promise<void>,
|
||||
|
||||
/** The route prefix of this group. */
|
||||
public readonly prefix: string
|
||||
) { super() }
|
||||
|
||||
/** Register the given middleware to be applied before all routes in this group. */
|
||||
pre(middleware: RouteHandler) {
|
||||
this.middlewares.push({
|
||||
stage: 'pre',
|
||||
@@ -48,6 +96,7 @@ export class RouteGroup extends AppClass {
|
||||
return this
|
||||
}
|
||||
|
||||
/** Register the given middleware to be applied after all routes in this group. */
|
||||
post(middleware: RouteHandler) {
|
||||
this.middlewares.push({
|
||||
stage: 'post',
|
||||
@@ -57,6 +106,7 @@ export class RouteGroup extends AppClass {
|
||||
return this
|
||||
}
|
||||
|
||||
/** Return the middlewares that apply to this group. */
|
||||
getGroupMiddlewareDefinitions() {
|
||||
return this.middlewares
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user