Add support for middleware

r0.1.5
Garrett Mills 3 years ago
parent 2868ca1910
commit 4f23ac7156
Signed by: garrettmills
GPG Key ID: D2BF5FBA8298F246

@ -58,18 +58,30 @@ export class HTTPKernel extends AppClass {
public async handle(request: Request): Promise<Request> {
try {
for (const module of this.preflight.toArray()) {
this.logging.verbose(`Applying pre-flight HTTP kernel module: ${module.constructor.name}`)
request = await module.apply(request)
if ( !request.response.blockingWriteback() || module.executeWithBlockingWriteback ) {
this.logging.verbose(`Applying pre-flight HTTP kernel module: ${module.constructor.name}`)
request = await module.apply(request)
} else {
this.logging.verbose(`Skipping pre-flight HTTP kernel module because of blocking write-back: ${module.constructor.name}`)
}
}
if (this.inflight) {
this.logging.verbose(`Applying core HTTP kernel module: ${this.inflight.constructor.name}`)
request = await this.inflight.apply(request)
if ( !request.response.blockingWriteback() || this.inflight.executeWithBlockingWriteback ) {
this.logging.verbose(`Applying core HTTP kernel module: ${this.inflight.constructor.name}`)
request = await this.inflight.apply(request)
} else {
this.logging.verbose(`Skipping core HTTP kernel module because of blocking write-back: ${this.inflight.constructor.name}`)
}
}
for (const module of this.postflight.toArray()) {
this.logging.verbose(`Applying post-flight HTTP kernel module: ${module.constructor.name}`)
request = await module.apply(request)
if ( !request.response.blockingWriteback() || module.executeWithBlockingWriteback ) {
this.logging.verbose(`Applying post-flight HTTP kernel module: ${module.constructor.name}`)
request = await module.apply(request)
} else {
this.logging.verbose(`Skipping post-flight HTTP kernel module because of blocking write-back: ${module.constructor.name}`)
}
}
} catch (e: any) {
this.logging.error(e)

@ -5,6 +5,8 @@ import {Request} from "../lifecycle/Request";
@Injectable()
export class HTTPKernelModule extends AppClass {
public readonly executeWithBlockingWriteback: boolean = false
/**
* Returns true if the given module should be applied to the incoming request.
* @param {Request} request

@ -0,0 +1,22 @@
import {HTTPKernelModule} from "../HTTPKernelModule";
import {ResponseObject} from "../../routing/Route";
import {Request} from "../../lifecycle/Request";
import {plaintext} from "../../response/StringResponseFactory";
import {ResponseFactory} from "../../response/ResponseFactory";
import {json} from "../../response/JSONResponseFactory";
export abstract class AbstractResolvedRouteHandlerHTTPModule extends HTTPKernelModule {
protected async applyResponseObject(object: ResponseObject, request: Request) {
if ( (typeof object === 'string') || (typeof object === 'number') ) {
object = plaintext(String(object))
}
if ( object instanceof ResponseFactory ) {
await object.write(request)
} else if ( typeof object !== 'undefined' ) {
await json(object).write(request)
} else {
await plaintext('').write(request)
}
}
}

@ -1,15 +1,12 @@
import {HTTPKernelModule} from "../HTTPKernelModule";
import {HTTPKernel} from "../HTTPKernel";
import {Request} from "../../lifecycle/Request";
import {ActivatedRoute} from "../../routing/ActivatedRoute";
import {ResponseObject} from "../../routing/Route";
import {plaintext} from "../../response/StringResponseFactory";
import {ResponseFactory} from "../../response/ResponseFactory";
import {json} from "../../response/JSONResponseFactory";
import {http} from "../../response/HTTPErrorResponseFactory";
import {HTTPStatus} from "@extollo/util";
import {AbstractResolvedRouteHandlerHTTPModule} from "./AbstractResolvedRouteHandlerHTTPModule";
export class ExecuteResolvedRouteHandlerHTTPModule extends HTTPKernelModule {
export class ExecuteResolvedRouteHandlerHTTPModule extends AbstractResolvedRouteHandlerHTTPModule {
public static register(kernel: HTTPKernel) {
kernel.register(this).core()
}
@ -19,19 +16,10 @@ export class ExecuteResolvedRouteHandlerHTTPModule extends HTTPKernelModule {
const route = <ActivatedRoute> request.make(ActivatedRoute)
let object: ResponseObject = await route.handler(request)
if ( (typeof object === 'string') || (typeof object === 'number') ) {
object = plaintext(String(object))
}
if ( object instanceof ResponseFactory ) {
await object.write(request)
} else if ( typeof object !== 'undefined' ) {
await json(object).write(request)
} else {
await plaintext('').write(request)
}
await this.applyResponseObject(object, request)
} else {
await http(HTTPStatus.NOT_FOUND).write(request)
request.response.blockingWriteback(true)
}
return request

@ -0,0 +1,29 @@
import {HTTPKernel} from "../HTTPKernel";
import {Request} from "../../lifecycle/Request";
import {ActivatedRoute} from "../../routing/ActivatedRoute";
import {ResponseObject} from "../../routing/Route";
import {AbstractResolvedRouteHandlerHTTPModule} from "./AbstractResolvedRouteHandlerHTTPModule";
import {PersistSessionHTTPModule} from "./PersistSessionHTTPModule";
export class ExecuteResolvedRoutePostflightHTTPModule extends AbstractResolvedRouteHandlerHTTPModule {
public static register(kernel: HTTPKernel) {
kernel.register(this).before(PersistSessionHTTPModule)
}
public async apply(request: Request) {
if ( request.hasInstance(ActivatedRoute) ) {
const route = <ActivatedRoute> request.make(ActivatedRoute)
const postflight = route.postflight
for ( const handler of postflight ) {
const result: ResponseObject = await handler(request)
if ( typeof result !== "undefined" ) {
await this.applyResponseObject(result, request)
request.response.blockingWriteback(true)
}
}
}
return request
}
}

@ -0,0 +1,29 @@
import {HTTPKernel} from "../HTTPKernel";
import {MountActivatedRouteHTTPModule} from "./MountActivatedRouteHTTPModule";
import {Request} from "../../lifecycle/Request";
import {ActivatedRoute} from "../../routing/ActivatedRoute";
import {ResponseObject} from "../../routing/Route";
import {AbstractResolvedRouteHandlerHTTPModule} from "./AbstractResolvedRouteHandlerHTTPModule";
export class ExecuteResolvedRoutePreflightHTTPModule extends AbstractResolvedRouteHandlerHTTPModule {
public static register(kernel: HTTPKernel) {
kernel.register(this).after(MountActivatedRouteHTTPModule)
}
public async apply(request: Request) {
if ( request.hasInstance(ActivatedRoute) ) {
const route = <ActivatedRoute> request.make(ActivatedRoute)
const preflight = route.preflight
for ( const handler of preflight ) {
const result: ResponseObject = await handler(request)
if ( typeof result !== "undefined" ) {
await this.applyResponseObject(result, request)
request.response.blockingWriteback(true)
}
}
}
return request
}
}

@ -9,6 +9,8 @@ import {Session} from "../../session/Session";
@Injectable()
export class InjectSessionHTTPModule extends HTTPKernelModule {
public readonly executeWithBlockingWriteback = true
public static register(kernel: HTTPKernel) {
kernel.register(this).after(SetSessionCookieHTTPModule)
}

@ -8,6 +8,8 @@ import {Logging} from "../../../service/Logging";
@Injectable()
export class MountActivatedRouteHTTPModule extends HTTPKernelModule {
public readonly executeWithBlockingWriteback = true
@Inject()
protected readonly routing!: Routing

@ -6,6 +6,8 @@ import {Session} from "../../session/Session";
@Injectable()
export class PersistSessionHTTPModule extends HTTPKernelModule {
public readonly executeWithBlockingWriteback = true
public static register(kernel: HTTPKernel) {
kernel.register(this).last()
}

@ -6,6 +6,8 @@ import {Config} from "../../../service/Config";
@Injectable()
export class PoweredByHeaderInjectionHTTPModule extends HTTPKernelModule {
public readonly executeWithBlockingWriteback = true
@Inject()
protected readonly config!: Config;

@ -7,6 +7,8 @@ import {Logging} from "../../../service/Logging";
@Injectable()
export class SetSessionCookieHTTPModule extends HTTPKernelModule {
public readonly executeWithBlockingWriteback = true
@Inject()
protected readonly logging!: Logging

@ -20,6 +20,7 @@ export class Response {
private _sentHeaders: boolean = false
private _responseEnded: boolean = false
private _status: HTTPStatus = HTTPStatus.OK
private _blockingWriteback: boolean = false
public body: string = ''
public readonly sending$: BehaviorSubject<Response> = new BehaviorSubject<Response>()
public readonly sent$: BehaviorSubject<Response> = new BehaviorSubject<Response>()
@ -88,6 +89,18 @@ export class Response {
return this._sentHeaders
}
public hasBody() {
return !!this.body
}
public blockingWriteback(set?: boolean) {
if ( typeof set !== 'undefined' ) {
this._blockingWriteback = set
}
return this._blockingWriteback
}
public async write(data: any) {
return new Promise<void>((res, rej) => {
if ( !this._sentHeaders ) this.sendHeaders()

@ -4,6 +4,8 @@ import {ResolvedRouteHandler, Route} from "./Route";
export class ActivatedRoute {
public readonly params: {[key: string]: string}
public readonly handler: ResolvedRouteHandler
public readonly preflight: ResolvedRouteHandler[]
public readonly postflight: ResolvedRouteHandler[]
constructor(
public readonly route: Route,
@ -20,6 +22,8 @@ export class ActivatedRoute {
}
this.params = params
this.preflight = route.resolvePreflight()
this.handler = route.resolveHandler()
this.postflight = route.resolvePostflight()
}
}

@ -0,0 +1,16 @@
import {AppClass} from "../../lifecycle/AppClass"
import {Request} from "../lifecycle/Request"
import {ResponseObject} from "./Route"
export abstract class Middleware extends AppClass {
constructor(
protected readonly request: Request
) { super() }
protected container() {
return this.request
}
// Return void | Promise<void> to continue request
public abstract apply(): ResponseObject
}

@ -5,15 +5,17 @@ import {RouteGroup} from "./RouteGroup";
import {ResponseFactory} from "../response/ResponseFactory";
import {Response} from "../lifecycle/Response";
import {Controllers} from "../../service/Controllers";
import {ErrorWithContext} from "@extollo/util";
import {ErrorWithContext, Collection} from "@extollo/util";
import {Controller} from "../Controller";
import {Middlewares} from "../../service/Middlewares";
import {Middleware} from "./Middleware";
export type ResponseObject = ResponseFactory | string | number | void | any | Promise<ResponseObject>
export type RouteHandler = ((request: Request, response: Response) => ResponseObject) | ((request: Request) => ResponseObject) | (() => ResponseObject) | string
export type ResolvedRouteHandler = (request: Request) => ResponseObject
// TODO middleware, domains, named routes - support this on groups as well
// TODO domains, named routes - support this on groups as well
export class Route extends AppClass {
private static registeredRoutes: Route[] = []
@ -36,9 +38,18 @@ export class Route extends AppClass {
for ( const route of registeredRoutes ) {
for ( const group of stack ) {
route.prepend(group.prefix)
group.getGroupMiddlewareDefinitions()
.each(def => route.prependMiddleware(def))
}
route.resolveHandler() // Try to resolve here to catch any errors at boot-time
for ( const group of this.compiledGroupStack ) {
group.getGroupMiddlewareDefinitions()
.each(def => route.appendMiddleware(def))
}
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 ) {
@ -90,6 +101,11 @@ export class Route extends AppClass {
return grp
}
protected middlewares: Collection<{ stage: 'pre' | 'post', handler: RouteHandler }> = new Collection<{stage: "pre" | "post"; handler: RouteHandler}>()
protected _compiledPreflight?: ResolvedRouteHandler[]
protected _compiledHandler?: ResolvedRouteHandler
protected _compiledPostflight?: ResolvedRouteHandler[]
constructor(
protected method: HTTPMethod | HTTPMethod[],
protected readonly handler: RouteHandler,
@ -138,7 +154,63 @@ export class Route extends AppClass {
return params
}
public resolvePreflight(): ResolvedRouteHandler[] {
if ( !this._compiledPreflight ) {
this._compiledPreflight = this.resolveMiddlewareHandlersForStage('pre')
}
return this._compiledPreflight
}
public resolvePostflight(): ResolvedRouteHandler[] {
if ( !this._compiledPostflight ) {
this._compiledPostflight = this.resolveMiddlewareHandlersForStage('post')
}
return this._compiledPostflight
}
public resolveHandler(): ResolvedRouteHandler {
if ( !this._compiledHandler ) {
this._compiledHandler = this._resolveHandler()
}
return this._compiledHandler
}
pre(middleware: RouteHandler) {
this.middlewares.push({
stage: 'pre',
handler: middleware
})
return this
}
post(middleware: RouteHandler) {
this.middlewares.push({
stage: 'post',
handler: middleware,
})
return this
}
private prepend(prefix: string) {
if ( !prefix.endsWith('/') ) prefix = `${prefix}/`
if ( this.route.startsWith('/') ) this.route = this.route.substring(1)
this.route = `${prefix}${this.route}`
}
private prependMiddleware(def: { stage: 'pre' | 'post', handler: RouteHandler }) {
this.middlewares.prepend(def)
}
private appendMiddleware(def: { stage: 'pre' | 'post', handler: RouteHandler }) {
this.middlewares.push(def)
}
private _resolveHandler(): ResolvedRouteHandler {
if ( typeof this.handler !== 'string' ) {
return (request: Request) => {
// @ts-ignore
@ -146,7 +218,7 @@ export class Route extends AppClass {
}
} else {
const parts = this.handler.split('.')
if ( parts.length < 1 ) {
if ( parts.length < 2 ) {
const e = new ErrorWithContext('Route handler does not specify a method name.')
e.context = {
handler: this.handler
@ -179,10 +251,45 @@ export class Route extends AppClass {
}
}
private prepend(prefix: string) {
if ( !prefix.endsWith('/') ) prefix = `${prefix}/`
if ( this.route.startsWith('/') ) this.route = this.route.substring(1)
this.route = `${prefix}${this.route}`
private resolveMiddlewareHandlersForStage(stage: 'pre' | 'post'): ResolvedRouteHandler[] {
return this.middlewares.where('stage', '=', stage)
.map<ResolvedRouteHandler>(def => {
if ( typeof def.handler !== 'string' ) {
return (request: Request) => {
// @ts-ignore
return def.handler(request, request.response)
}
} else {
const parts = def.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: def.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()
}
toString() {

@ -1,8 +1,12 @@
import {AppClass} from "../../lifecycle/AppClass";
import {Collection} from "@extollo/util"
import {AppClass} from "../../lifecycle/AppClass"
import {RouteHandler} from "./Route"
export class RouteGroup extends AppClass {
private static currentGroupNesting: RouteGroup[] = []
protected middlewares: Collection<{ stage: 'pre' | 'post', handler: RouteHandler }> = new Collection<{stage: "pre" | "post"; handler: RouteHandler}>()
public static getCurrentGroupHierarchy(): RouteGroup[] {
return [...this.currentGroupNesting]
}
@ -11,4 +15,26 @@ export class RouteGroup extends AppClass {
public readonly group: () => void | Promise<void>,
public readonly prefix: string
) { super() }
pre(middleware: RouteHandler) {
this.middlewares.push({
stage: 'pre',
handler: middleware
})
return this
}
post(middleware: RouteHandler) {
this.middlewares.push({
stage: 'post',
handler: middleware,
})
return this
}
getGroupMiddlewareDefinitions() {
return this.middlewares
}
}

@ -12,6 +12,10 @@ export * from './http/kernel/module/MountActivatedRouteHTTPModule'
export * from './http/kernel/module/PersistSessionHTTPModule'
export * from './http/kernel/module/PoweredByHeaderInjectionHTTPModule'
export * from './http/kernel/module/SetSessionCookieHTTPModule'
export * from './http/kernel/module/AbstractResolvedRouteHandlerHTTPModule'
export * from './http/kernel/module/ExecuteResolvedRoutePreflightHTTPModule'
export * from './http/kernel/module/ExecuteResolvedRouteHandlerHTTPModule'
export * from './http/kernel/module/ExecuteResolvedRoutePostflightHTTPModule'
export * from './http/kernel/HTTPKernel'
export * from './http/kernel/HTTPKernelModule'
@ -34,6 +38,7 @@ export * from './http/response/ViewResponseFactory'
export * from './http/routing/ActivatedRoute'
export * from './http/routing/Route'
export * from './http/routing/RouteGroup'
export * from './http/routing/Middleware'
export * from './http/kernel/module/ExecuteResolvedRouteHandlerHTTPModule'
@ -52,6 +57,7 @@ export * from './service/Config'
export * from './service/Controllers'
export * from './service/HTTPServer'
export * from './service/Routing'
export * from './service/Middlewares'
export * from './views/ViewEngine'
export * from './views/ViewEngineFactory'

@ -12,6 +12,8 @@ import {PersistSessionHTTPModule} from "../http/kernel/module/PersistSessionHTTP
import {MountActivatedRouteHTTPModule} from "../http/kernel/module/MountActivatedRouteHTTPModule";
import {ExecuteResolvedRouteHandlerHTTPModule} from "../http/kernel/module/ExecuteResolvedRouteHandlerHTTPModule";
import {error} from "../http/response/ErrorResponseFactory";
import {ExecuteResolvedRoutePreflightHTTPModule} from "../http/kernel/module/ExecuteResolvedRoutePreflightHTTPModule";
import {ExecuteResolvedRoutePostflightHTTPModule} from "../http/kernel/module/ExecuteResolvedRoutePostflightHTTPModule";
@Singleton()
export class HTTPServer extends Unit {
@ -33,6 +35,8 @@ export class HTTPServer extends Unit {
PersistSessionHTTPModule.register(this.kernel)
MountActivatedRouteHTTPModule.register(this.kernel)
ExecuteResolvedRouteHandlerHTTPModule.register(this.kernel)
ExecuteResolvedRoutePreflightHTTPModule.register(this.kernel)
ExecuteResolvedRoutePostflightHTTPModule.register(this.kernel)
await new Promise<void>((res, rej) => {
this.server = createServer(this.handler)

@ -0,0 +1,20 @@
import {CanonicalStatic} from "./CanonicalStatic";
import {Singleton, Instantiable} from "@extollo/di";
import {CanonicalDefinition} from "./Canonical";
import {Middleware} from "../http/routing/Middleware";
@Singleton()
export class Middlewares extends CanonicalStatic<Instantiable<Middleware>, Middleware> {
protected appPath = ['http', 'middlewares']
protected canonicalItem = 'middleware'
protected suffix = '.middleware.js'
public async initCanonicalItem(definition: CanonicalDefinition) {
const item = await super.initCanonicalItem(definition)
if ( !(item.prototype instanceof Middleware) ) {
throw new TypeError(`Invalid middleware definition: ${definition.originalName}. Controllers must extend from @extollo/lib.http.routing.Middleware.`)
}
return item
}
}
Loading…
Cancel
Save