TypeDoc all the thngs
This commit is contained in:
parent
7cb0546b01
commit
fad1184afe
@ -1,6 +1,10 @@
|
|||||||
import {AppClass} from "../lifecycle/AppClass";
|
import {AppClass} from "../lifecycle/AppClass";
|
||||||
import {Request} from "./lifecycle/Request";
|
import {Request} from "./lifecycle/Request";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for controllers that define methods that
|
||||||
|
* handle HTTP requests.
|
||||||
|
*/
|
||||||
export class Controller extends AppClass {
|
export class Controller extends AppClass {
|
||||||
constructor(
|
constructor(
|
||||||
protected readonly request: Request
|
protected readonly request: Request
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
import {ErrorWithContext, HTTPStatus, HTTPMessage} from "@extollo/util"
|
import {ErrorWithContext, HTTPStatus, HTTPMessage} from "@extollo/util"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An error class that has an associated HTTP status.
|
||||||
|
*
|
||||||
|
* When thrown inside the request lifecycle, this will result in the HTTP
|
||||||
|
* status code being applied to the response.
|
||||||
|
*/
|
||||||
export class HTTPError extends ErrorWithContext {
|
export class HTTPError extends ErrorWithContext {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly status: HTTPStatus = 500,
|
public readonly status: HTTPStatus = 500,
|
||||||
|
@ -12,8 +12,14 @@ export interface HTTPCookie {
|
|||||||
options?: HTTPCookieOptions,
|
options?: HTTPCookieOptions,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type alias for something that is either an HTTP cookie, or undefined.
|
||||||
|
*/
|
||||||
export type MaybeHTTPCookie = HTTPCookie | undefined;
|
export type MaybeHTTPCookie = HTTPCookie | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface describing the available cookie options.
|
||||||
|
*/
|
||||||
export interface HTTPCookieOptions {
|
export interface HTTPCookieOptions {
|
||||||
domain?: string,
|
domain?: string,
|
||||||
expires?: Date, // encodeURIComponent
|
expires?: Date, // encodeURIComponent
|
||||||
@ -25,21 +31,36 @@ export interface HTTPCookieOptions {
|
|||||||
sameSite?: 'strict' | 'lax' | 'none-secure',
|
sameSite?: 'strict' | 'lax' | 'none-secure',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for accessing and managing cookies in the associated request.
|
||||||
|
*/
|
||||||
export class HTTPCookieJar {
|
export class HTTPCookieJar {
|
||||||
|
/** The cookies parsed from the request. */
|
||||||
protected parsed: {[key: string]: HTTPCookie} = {}
|
protected parsed: {[key: string]: HTTPCookie} = {}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
/** The request whose cookies should be loaded. */
|
||||||
protected request: Request,
|
protected request: Request,
|
||||||
) {
|
) {
|
||||||
this.parseCookies()
|
this.parseCookies()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the HTTPCookie by name, if it exists.
|
||||||
|
* @param name
|
||||||
|
*/
|
||||||
get(name: string): MaybeHTTPCookie {
|
get(name: string): MaybeHTTPCookie {
|
||||||
if ( name in this.parsed ) {
|
if ( name in this.parsed ) {
|
||||||
return this.parsed[name]
|
return this.parsed[name]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a new cookie using the specified options.
|
||||||
|
* @param name
|
||||||
|
* @param value
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
set(name: string, value: any, options?: HTTPCookieOptions) {
|
set(name: string, value: any, options?: HTTPCookieOptions) {
|
||||||
this.parsed[name] = {
|
this.parsed[name] = {
|
||||||
key: name,
|
key: name,
|
||||||
@ -50,10 +71,23 @@ export class HTTPCookieJar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if a cookie exists with the given name.
|
||||||
|
* @param name
|
||||||
|
*/
|
||||||
has(name: string) {
|
has(name: string) {
|
||||||
return !!this.parsed[name]
|
return !!this.parsed[name]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the given cookie.
|
||||||
|
*
|
||||||
|
* Important: if the cookie was set with any `options`, the SAME options
|
||||||
|
* must be provided here in order for the cookie to be cleared on the client.
|
||||||
|
*
|
||||||
|
* @param name
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
clear(name: string, options?: HTTPCookieOptions) {
|
clear(name: string, options?: HTTPCookieOptions) {
|
||||||
if ( !options ) options = {}
|
if ( !options ) options = {}
|
||||||
options.expires = new Date(0)
|
options.expires = new Date(0)
|
||||||
@ -67,6 +101,9 @@ export class HTTPCookieJar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an array of `Set-Cookie` headers to include in the response.
|
||||||
|
*/
|
||||||
getSetCookieHeaders(): string[] {
|
getSetCookieHeaders(): string[] {
|
||||||
const headers: string[] = []
|
const headers: string[] = []
|
||||||
|
|
||||||
@ -119,6 +156,7 @@ export class HTTPCookieJar {
|
|||||||
return headers
|
return headers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Parse the cookies from the request. */
|
||||||
private parseCookies() {
|
private parseCookies() {
|
||||||
const cookies = String(this.request.getHeader('cookie'))
|
const cookies = String(this.request.getHeader('cookie'))
|
||||||
cookies.split(';').forEach(cookie => {
|
cookies.split(';').forEach(cookie => {
|
||||||
|
@ -10,10 +10,27 @@ import {error} from "../response/ErrorResponseFactory";
|
|||||||
* Interface for fluently registering kernel modules into the kernel.
|
* Interface for fluently registering kernel modules into the kernel.
|
||||||
*/
|
*/
|
||||||
export interface ModuleRegistrationFluency {
|
export interface ModuleRegistrationFluency {
|
||||||
|
/**
|
||||||
|
* If no argument is provided, the module will be registered before the core module.
|
||||||
|
* If an argument is provided, the module will be registered before the other specified module.
|
||||||
|
* @param other
|
||||||
|
*/
|
||||||
before: (other?: Instantiable<HTTPKernelModule>) => HTTPKernel,
|
before: (other?: Instantiable<HTTPKernelModule>) => HTTPKernel,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If no argument is provided, the module will be registered after the core module.
|
||||||
|
* If an argument is provided, the module will be registered after the other specified module.
|
||||||
|
* @param other
|
||||||
|
*/
|
||||||
after: (other?: Instantiable<HTTPKernelModule>) => HTTPKernel,
|
after: (other?: Instantiable<HTTPKernelModule>) => HTTPKernel,
|
||||||
|
|
||||||
|
/** The module will be registered as the first module in the preflight. */
|
||||||
first: () => HTTPKernel,
|
first: () => HTTPKernel,
|
||||||
|
|
||||||
|
/** The module will be registered as the last module in the postflight. */
|
||||||
last: () => HTTPKernel,
|
last: () => HTTPKernel,
|
||||||
|
|
||||||
|
/** The module will be registered as the core handler for the request. */
|
||||||
core: () => HTTPKernel,
|
core: () => HTTPKernel,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,6 +44,9 @@ export class KernelModuleNotFoundError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A singleton class that handles requests, applying logic in modular layers.
|
||||||
|
*/
|
||||||
@Singleton()
|
@Singleton()
|
||||||
export class HTTPKernel extends AppClass {
|
export class HTTPKernel extends AppClass {
|
||||||
@Inject()
|
@Inject()
|
||||||
|
@ -3,8 +3,19 @@ import {AppClass} from "../../lifecycle/AppClass";
|
|||||||
import {HTTPKernel} from "./HTTPKernel";
|
import {HTTPKernel} from "./HTTPKernel";
|
||||||
import {Request} from "../lifecycle/Request";
|
import {Request} from "../lifecycle/Request";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for modules that define logic that is applied to requests
|
||||||
|
* handled by the HTTP kernel.
|
||||||
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class HTTPKernelModule extends AppClass {
|
export class HTTPKernelModule extends AppClass {
|
||||||
|
/**
|
||||||
|
* By default, if a kernel module interrupts the request flow to send a response
|
||||||
|
* (for example, if an error occurs), subsequent modules are skipped.
|
||||||
|
*
|
||||||
|
* However, a module can override this property to be true and its logic will still
|
||||||
|
* be applied, even after a module has interrupted the request flow.
|
||||||
|
*/
|
||||||
public readonly executeWithBlockingWriteback: boolean = false
|
public readonly executeWithBlockingWriteback: boolean = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -5,7 +5,16 @@ import {plaintext} from "../../response/StringResponseFactory";
|
|||||||
import {ResponseFactory} from "../../response/ResponseFactory";
|
import {ResponseFactory} from "../../response/ResponseFactory";
|
||||||
import {json} from "../../response/JSONResponseFactory";
|
import {json} from "../../response/JSONResponseFactory";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for HTTP kernel modules that apply some response from a route handler to the request.
|
||||||
|
*/
|
||||||
export abstract class AbstractResolvedRouteHandlerHTTPModule extends HTTPKernelModule {
|
export abstract class AbstractResolvedRouteHandlerHTTPModule extends HTTPKernelModule {
|
||||||
|
/**
|
||||||
|
* Given a response object, write the response to the request in the appropriate format.
|
||||||
|
* @param object
|
||||||
|
* @param request
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
protected async applyResponseObject(object: ResponseObject, request: Request) {
|
protected async applyResponseObject(object: ResponseObject, request: Request) {
|
||||||
if ( (typeof object === 'string') || (typeof object === 'number') ) {
|
if ( (typeof object === 'string') || (typeof object === 'number') ) {
|
||||||
object = plaintext(String(object))
|
object = plaintext(String(object))
|
||||||
|
@ -6,6 +6,11 @@ import {http} from "../../response/HTTPErrorResponseFactory";
|
|||||||
import {HTTPStatus} from "@extollo/util";
|
import {HTTPStatus} from "@extollo/util";
|
||||||
import {AbstractResolvedRouteHandlerHTTPModule} from "./AbstractResolvedRouteHandlerHTTPModule";
|
import {AbstractResolvedRouteHandlerHTTPModule} from "./AbstractResolvedRouteHandlerHTTPModule";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP kernel module that runs the handler for the request's route.
|
||||||
|
*
|
||||||
|
* In most cases, this is the controller method defined by the route.
|
||||||
|
*/
|
||||||
export class ExecuteResolvedRouteHandlerHTTPModule extends AbstractResolvedRouteHandlerHTTPModule {
|
export class ExecuteResolvedRouteHandlerHTTPModule extends AbstractResolvedRouteHandlerHTTPModule {
|
||||||
public static register(kernel: HTTPKernel) {
|
public static register(kernel: HTTPKernel) {
|
||||||
kernel.register(this).core()
|
kernel.register(this).core()
|
||||||
|
@ -5,6 +5,11 @@ import {ResponseObject} from "../../routing/Route";
|
|||||||
import {AbstractResolvedRouteHandlerHTTPModule} from "./AbstractResolvedRouteHandlerHTTPModule";
|
import {AbstractResolvedRouteHandlerHTTPModule} from "./AbstractResolvedRouteHandlerHTTPModule";
|
||||||
import {PersistSessionHTTPModule} from "./PersistSessionHTTPModule";
|
import {PersistSessionHTTPModule} from "./PersistSessionHTTPModule";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP kernel module that executes the postflight handlers for the route.
|
||||||
|
*
|
||||||
|
* Usually, this is post middleware.
|
||||||
|
*/
|
||||||
export class ExecuteResolvedRoutePostflightHTTPModule extends AbstractResolvedRouteHandlerHTTPModule {
|
export class ExecuteResolvedRoutePostflightHTTPModule extends AbstractResolvedRouteHandlerHTTPModule {
|
||||||
public static register(kernel: HTTPKernel) {
|
public static register(kernel: HTTPKernel) {
|
||||||
kernel.register(this).before(PersistSessionHTTPModule)
|
kernel.register(this).before(PersistSessionHTTPModule)
|
||||||
|
@ -5,6 +5,11 @@ import {ActivatedRoute} from "../../routing/ActivatedRoute";
|
|||||||
import {ResponseObject} from "../../routing/Route";
|
import {ResponseObject} from "../../routing/Route";
|
||||||
import {AbstractResolvedRouteHandlerHTTPModule} from "./AbstractResolvedRouteHandlerHTTPModule";
|
import {AbstractResolvedRouteHandlerHTTPModule} from "./AbstractResolvedRouteHandlerHTTPModule";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP Kernel module that executes the preflight handlers for the route.
|
||||||
|
*
|
||||||
|
* Usually, this is the pre middleware.
|
||||||
|
*/
|
||||||
export class ExecuteResolvedRoutePreflightHTTPModule extends AbstractResolvedRouteHandlerHTTPModule {
|
export class ExecuteResolvedRoutePreflightHTTPModule extends AbstractResolvedRouteHandlerHTTPModule {
|
||||||
public static register(kernel: HTTPKernel) {
|
public static register(kernel: HTTPKernel) {
|
||||||
kernel.register(this).after(MountActivatedRouteHTTPModule)
|
kernel.register(this).after(MountActivatedRouteHTTPModule)
|
||||||
|
@ -7,6 +7,10 @@ import {SetSessionCookieHTTPModule} from "./SetSessionCookieHTTPModule";
|
|||||||
import {SessionFactory} from "../../session/SessionFactory";
|
import {SessionFactory} from "../../session/SessionFactory";
|
||||||
import {Session} from "../../session/Session";
|
import {Session} from "../../session/Session";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP kernel middleware that creates the session using the configured driver
|
||||||
|
* and loads its data using the request's session cookie.
|
||||||
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class InjectSessionHTTPModule extends HTTPKernelModule {
|
export class InjectSessionHTTPModule extends HTTPKernelModule {
|
||||||
public readonly executeWithBlockingWriteback = true
|
public readonly executeWithBlockingWriteback = true
|
||||||
|
@ -6,6 +6,10 @@ import {Routing} from "../../../service/Routing";
|
|||||||
import {ActivatedRoute} from "../../routing/ActivatedRoute";
|
import {ActivatedRoute} from "../../routing/ActivatedRoute";
|
||||||
import {Logging} from "../../../service/Logging";
|
import {Logging} from "../../../service/Logging";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP kernel middleware that tries to find a registered route matching the request's
|
||||||
|
* path and creates an ActivatedRoute instance from it.
|
||||||
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MountActivatedRouteHTTPModule extends HTTPKernelModule {
|
export class MountActivatedRouteHTTPModule extends HTTPKernelModule {
|
||||||
public readonly executeWithBlockingWriteback = true
|
public readonly executeWithBlockingWriteback = true
|
||||||
|
@ -4,6 +4,10 @@ import {HTTPKernel} from "../HTTPKernel";
|
|||||||
import {Request} from "../../lifecycle/Request";
|
import {Request} from "../../lifecycle/Request";
|
||||||
import {Session} from "../../session/Session";
|
import {Session} from "../../session/Session";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP kernel module that runs after the main logic in the request to persist
|
||||||
|
* the session data to the driver's backend.
|
||||||
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PersistSessionHTTPModule extends HTTPKernelModule {
|
export class PersistSessionHTTPModule extends HTTPKernelModule {
|
||||||
public readonly executeWithBlockingWriteback = true
|
public readonly executeWithBlockingWriteback = true
|
||||||
|
@ -4,6 +4,9 @@ import {Injectable, Inject} from "@extollo/di"
|
|||||||
import {HTTPKernel} from "../HTTPKernel";
|
import {HTTPKernel} from "../HTTPKernel";
|
||||||
import {Config} from "../../../service/Config";
|
import {Config} from "../../../service/Config";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP kernel middleware that sets the `X-Powered-By` header.
|
||||||
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PoweredByHeaderInjectionHTTPModule extends HTTPKernelModule {
|
export class PoweredByHeaderInjectionHTTPModule extends HTTPKernelModule {
|
||||||
public readonly executeWithBlockingWriteback = true
|
public readonly executeWithBlockingWriteback = true
|
||||||
|
@ -5,6 +5,10 @@ import {HTTPKernel} from "../HTTPKernel";
|
|||||||
import {Request} from "../../lifecycle/Request";
|
import {Request} from "../../lifecycle/Request";
|
||||||
import {Logging} from "../../../service/Logging";
|
import {Logging} from "../../../service/Logging";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP kernel middleware that tries to look up the session ID from the request.
|
||||||
|
* If none exists, generates a new one and sets the cookie.
|
||||||
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SetSessionCookieHTTPModule extends HTTPKernelModule {
|
export class SetSessionCookieHTTPModule extends HTTPKernelModule {
|
||||||
public readonly executeWithBlockingWriteback = true
|
public readonly executeWithBlockingWriteback = true
|
||||||
|
@ -7,44 +7,88 @@ import * as url from "url";
|
|||||||
import {Response} from "./Response";
|
import {Response} from "./Response";
|
||||||
import * as Negotiator from "negotiator";
|
import * as Negotiator from "negotiator";
|
||||||
|
|
||||||
// FIXME - add others?
|
/**
|
||||||
|
* Enumeration of different HTTP verbs.
|
||||||
|
* @todo add others?
|
||||||
|
*/
|
||||||
export type HTTPMethod = 'post' | 'get' | 'patch' | 'put' | 'delete' | 'unknown';
|
export type HTTPMethod = 'post' | 'get' | 'patch' | 'put' | 'delete' | 'unknown';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the given item is a valid HTTP verb.
|
||||||
|
* @param what
|
||||||
|
*/
|
||||||
export function isHTTPMethod(what: any): what is HTTPMethod {
|
export function isHTTPMethod(what: any): what is HTTPMethod {
|
||||||
return ['post', 'get', 'patch', 'put', 'delete'].includes(what)
|
return ['post', 'get', 'patch', 'put', 'delete'].includes(what)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface that describes the HTTP protocol version.
|
||||||
|
*/
|
||||||
export interface HTTPProtocol {
|
export interface HTTPProtocol {
|
||||||
string: string,
|
string: string,
|
||||||
major: number,
|
major: number,
|
||||||
minor: number,
|
minor: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface that describes the origin IP address of a request.
|
||||||
|
*/
|
||||||
export interface HTTPSourceAddress {
|
export interface HTTPSourceAddress {
|
||||||
address: string;
|
address: string;
|
||||||
family: 'IPv4' | 'IPv6';
|
family: 'IPv4' | 'IPv6';
|
||||||
port: number;
|
port: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class that represents an HTTP request from a client.
|
||||||
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class Request extends ScopedContainer {
|
export class Request extends ScopedContainer {
|
||||||
|
|
||||||
|
/** The cookie manager for the request. */
|
||||||
public readonly cookies: HTTPCookieJar;
|
public readonly cookies: HTTPCookieJar;
|
||||||
|
|
||||||
|
/** The URL suffix of the request. */
|
||||||
public readonly url: string;
|
public readonly url: string;
|
||||||
|
|
||||||
|
/** The fully-qualified URL of the request. */
|
||||||
public readonly fullUrl: string;
|
public readonly fullUrl: string;
|
||||||
|
|
||||||
|
/** The HTTP verb of the request. */
|
||||||
public readonly method: HTTPMethod;
|
public readonly method: HTTPMethod;
|
||||||
|
|
||||||
|
/** True if the request was made via TLS. */
|
||||||
public readonly secure: boolean;
|
public readonly secure: boolean;
|
||||||
|
|
||||||
|
/** The request HTTP protocol version. */
|
||||||
public readonly protocol: HTTPProtocol;
|
public readonly protocol: HTTPProtocol;
|
||||||
|
|
||||||
|
/** The URL path, stripped of query params. */
|
||||||
public readonly path: string;
|
public readonly path: string;
|
||||||
|
|
||||||
|
/** The raw parsed query data from the request. */
|
||||||
public readonly rawQueryData: {[key: string]: string | string[] | undefined};
|
public readonly rawQueryData: {[key: string]: string | string[] | undefined};
|
||||||
|
|
||||||
|
/** The inferred query data. */
|
||||||
public readonly query: {[key: string]: any};
|
public readonly query: {[key: string]: any};
|
||||||
|
|
||||||
|
/** True if the request was made via XMLHttpRequest. */
|
||||||
public readonly isXHR: boolean;
|
public readonly isXHR: boolean;
|
||||||
|
|
||||||
|
/** The origin IP address of the request. */
|
||||||
public readonly address: HTTPSourceAddress;
|
public readonly address: HTTPSourceAddress;
|
||||||
|
|
||||||
|
/** The associated response. */
|
||||||
public readonly response: Response;
|
public readonly response: Response;
|
||||||
|
|
||||||
|
/** The media types accepted by the client. */
|
||||||
public readonly mediaTypes: string[];
|
public readonly mediaTypes: string[];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
/** The native Node.js request. */
|
||||||
protected clientRequest: IncomingMessage,
|
protected clientRequest: IncomingMessage,
|
||||||
|
|
||||||
|
/** The native Node.js response. */
|
||||||
protected serverResponse: ServerResponse,
|
protected serverResponse: ServerResponse,
|
||||||
) {
|
) {
|
||||||
super(Container.getContainer())
|
super(Container.getContainer())
|
||||||
@ -103,20 +147,30 @@ export class Request extends ScopedContainer {
|
|||||||
this.response = new Response(this, serverResponse)
|
this.response = new Response(this, serverResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get the value of a header, if it exists. */
|
||||||
public getHeader(name: string) {
|
public getHeader(name: string) {
|
||||||
return this.clientRequest.headers[name.toLowerCase()]
|
return this.clientRequest.headers[name.toLowerCase()]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get the native Node.js IncomingMessage object. */
|
||||||
public toNative() {
|
public toNative() {
|
||||||
return this.clientRequest
|
return this.clientRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the value of an input field on the request. Spans multiple input sources.
|
||||||
|
* @param key
|
||||||
|
*/
|
||||||
public input(key: string) {
|
public input(key: string) {
|
||||||
if ( key in this.query ) {
|
if ( key in this.query ) {
|
||||||
return this.query[key]
|
return this.query[key]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the request accepts the given media type.
|
||||||
|
* @param type - a mimetype, or the short forms json, xml, or html
|
||||||
|
*/
|
||||||
accepts(type: string) {
|
accepts(type: string) {
|
||||||
if ( type === 'json' ) type = 'application/json'
|
if ( type === 'json' ) type = 'application/json'
|
||||||
else if ( type === 'xml' ) type = 'application/xml'
|
else if ( type === 'xml' ) type = 'application/xml'
|
||||||
@ -133,6 +187,9 @@ export class Request extends ScopedContainer {
|
|||||||
return this.mediaTypes.some(media => possible.includes(media.toLowerCase()))
|
return this.mediaTypes.some(media => possible.includes(media.toLowerCase()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the short form of the content type the client has requested.
|
||||||
|
*/
|
||||||
wants(): 'html' | 'json' | 'xml' | 'unknown' {
|
wants(): 'html' | 'json' | 'xml' | 'unknown' {
|
||||||
const jsonIdx = this.mediaTypes.indexOf('application/json') ?? this.mediaTypes.indexOf('application/*') ?? this.mediaTypes.indexOf('*/*')
|
const jsonIdx = this.mediaTypes.indexOf('application/json') ?? this.mediaTypes.indexOf('application/*') ?? this.mediaTypes.indexOf('*/*')
|
||||||
const xmlIdx = this.mediaTypes.indexOf('application/xml') ?? this.mediaTypes.indexOf('application/*') ?? this.mediaTypes.indexOf('*/*')
|
const xmlIdx = this.mediaTypes.indexOf('application/xml') ?? this.mediaTypes.indexOf('application/*') ?? this.mediaTypes.indexOf('*/*')
|
||||||
|
@ -2,6 +2,9 @@ import {Request} from "./Request";
|
|||||||
import {ErrorWithContext, HTTPStatus, BehaviorSubject} from "@extollo/util"
|
import {ErrorWithContext, HTTPStatus, BehaviorSubject} from "@extollo/util"
|
||||||
import {ServerResponse} from "http"
|
import {ServerResponse} from "http"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error thrown when the server tries to re-send headers after they have been sent once.
|
||||||
|
*/
|
||||||
export class HeadersAlreadySentError extends ErrorWithContext {
|
export class HeadersAlreadySentError extends ErrorWithContext {
|
||||||
constructor(response: Response, headerName?: string) {
|
constructor(response: Response, headerName?: string) {
|
||||||
super(`Cannot modify or re-send headers for this request as they have already been sent.`);
|
super(`Cannot modify or re-send headers for this request as they have already been sent.`);
|
||||||
@ -9,56 +12,103 @@ export class HeadersAlreadySentError extends ErrorWithContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error thrown when the server tries to re-send a response that has already been sent.
|
||||||
|
*/
|
||||||
export class ResponseAlreadySentError extends ErrorWithContext {
|
export class ResponseAlreadySentError extends ErrorWithContext {
|
||||||
constructor(response: Response) {
|
constructor(response: Response) {
|
||||||
super(`Cannot modify or re-send response as it has already ended.`);
|
super(`Cannot modify or re-send response as it has already ended.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class representing an HTTP response to a client.
|
||||||
|
*/
|
||||||
export class Response {
|
export class Response {
|
||||||
|
/** Mapping of headers that should be sent back to the client. */
|
||||||
private headers: {[key: string]: string | string[]} = {}
|
private headers: {[key: string]: string | string[]} = {}
|
||||||
|
|
||||||
|
/** True if the headers have been sent. */
|
||||||
private _sentHeaders: boolean = false
|
private _sentHeaders: boolean = false
|
||||||
|
|
||||||
|
/** True if the response has been sent and closed. */
|
||||||
private _responseEnded: boolean = false
|
private _responseEnded: boolean = false
|
||||||
|
|
||||||
|
/** The HTTP status code that should be sent to the client. */
|
||||||
private _status: HTTPStatus = HTTPStatus.OK
|
private _status: HTTPStatus = HTTPStatus.OK
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this is true, then some module in the kernel has flagged the response
|
||||||
|
* as being interrupted and handled. Subsequent modules should NOT overwrite
|
||||||
|
* the response.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
private _blockingWriteback: boolean = false
|
private _blockingWriteback: boolean = false
|
||||||
|
|
||||||
|
/** The body contents that should be written to the response. */
|
||||||
public body: string = ''
|
public body: string = ''
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Behavior subject fired right before the response content is written.
|
||||||
|
*/
|
||||||
public readonly sending$: BehaviorSubject<Response> = new BehaviorSubject<Response>()
|
public readonly sending$: BehaviorSubject<Response> = new BehaviorSubject<Response>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Behavior subject fired right after the response content is written.
|
||||||
|
*/
|
||||||
public readonly sent$: BehaviorSubject<Response> = new BehaviorSubject<Response>()
|
public readonly sent$: BehaviorSubject<Response> = new BehaviorSubject<Response>()
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
/** The associated request object. */
|
||||||
public readonly request: Request,
|
public readonly request: Request,
|
||||||
|
|
||||||
|
/** The native Node.js ServerResponse. */
|
||||||
protected readonly serverResponse: ServerResponse,
|
protected readonly serverResponse: ServerResponse,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
|
/** Get the currently set response status. */
|
||||||
public getStatus() {
|
public getStatus() {
|
||||||
return this._status
|
return this._status
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Set a new response status. */
|
||||||
public setStatus(status: HTTPStatus) {
|
public setStatus(status: HTTPStatus) {
|
||||||
if ( this._sentHeaders ) throw new HeadersAlreadySentError(this, 'status')
|
if ( this._sentHeaders ) throw new HeadersAlreadySentError(this, 'status')
|
||||||
this._status = status
|
this._status = status
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get the HTTPCookieJar for the client. */
|
||||||
public get cookies() {
|
public get cookies() {
|
||||||
return this.request.cookies
|
return this.request.cookies
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get the value of the response header, if it exists. */
|
||||||
public getHeader(name: string): string | string[] | undefined {
|
public getHeader(name: string): string | string[] | undefined {
|
||||||
return this.headers[name]
|
return this.headers[name]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Set the value of the response header. */
|
||||||
public setHeader(name: string, value: string | string[]) {
|
public setHeader(name: string, value: string | string[]) {
|
||||||
if ( this._sentHeaders ) throw new HeadersAlreadySentError(this, name)
|
if ( this._sentHeaders ) throw new HeadersAlreadySentError(this, name)
|
||||||
this.headers[name] = value
|
this.headers[name] = value
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bulk set the specified headers in the response.
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
public setHeaders(data: {[name: string]: string | string[]}) {
|
public setHeaders(data: {[name: string]: string | string[]}) {
|
||||||
if ( this._sentHeaders ) throw new HeadersAlreadySentError(this)
|
if ( this._sentHeaders ) throw new HeadersAlreadySentError(this)
|
||||||
this.headers = {...this.headers, ...data}
|
this.headers = {...this.headers, ...data}
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the given value as a header, appending it to an existing header if one exists.
|
||||||
|
* @param name
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
public appendHeader(name: string, value: string | string[]) {
|
public appendHeader(name: string, value: string | string[]) {
|
||||||
if ( this._sentHeaders ) throw new HeadersAlreadySentError(this, name)
|
if ( this._sentHeaders ) throw new HeadersAlreadySentError(this, name)
|
||||||
if ( !Array.isArray(value) ) value = [value]
|
if ( !Array.isArray(value) ) value = [value]
|
||||||
@ -70,6 +120,9 @@ export class Response {
|
|||||||
this.headers[name] = existing
|
this.headers[name] = existing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write the headers to the client.
|
||||||
|
*/
|
||||||
public sendHeaders() {
|
public sendHeaders() {
|
||||||
const headers = {} as any
|
const headers = {} as any
|
||||||
|
|
||||||
@ -85,14 +138,20 @@ export class Response {
|
|||||||
this._sentHeaders = true
|
this._sentHeaders = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns true if the headers have been sent. */
|
||||||
public hasSentHeaders() {
|
public hasSentHeaders() {
|
||||||
return this._sentHeaders
|
return this._sentHeaders
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns true if a body has been set in the response. */
|
||||||
public hasBody() {
|
public hasBody() {
|
||||||
return !!this.body
|
return !!this.body
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or set the flag for whether the writeback should be blocked.
|
||||||
|
* @param set - if this is specified, the value will be set.
|
||||||
|
*/
|
||||||
public blockingWriteback(set?: boolean) {
|
public blockingWriteback(set?: boolean) {
|
||||||
if ( typeof set !== 'undefined' ) {
|
if ( typeof set !== 'undefined' ) {
|
||||||
this._blockingWriteback = set
|
this._blockingWriteback = set
|
||||||
@ -101,6 +160,10 @@ export class Response {
|
|||||||
return this._blockingWriteback
|
return this._blockingWriteback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write the headers and specified data to the client.
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
public async write(data: any) {
|
public async write(data: any) {
|
||||||
return new Promise<void>((res, rej) => {
|
return new Promise<void>((res, rej) => {
|
||||||
if ( !this._sentHeaders ) this.sendHeaders()
|
if ( !this._sentHeaders ) this.sendHeaders()
|
||||||
@ -111,6 +174,9 @@ export class Response {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the response to the client, writing the headers and configured body.
|
||||||
|
*/
|
||||||
public async send() {
|
public async send() {
|
||||||
await this.sending$.next(this)
|
await this.sending$.next(this)
|
||||||
this.setHeader('Content-Length', String(this.body?.length ?? 0))
|
this.setHeader('Content-Length', String(this.body?.length ?? 0))
|
||||||
@ -119,6 +185,9 @@ export class Response {
|
|||||||
await this.sent$.next(this)
|
await this.sent$.next(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark the response as ended and close the socket.
|
||||||
|
*/
|
||||||
public end() {
|
public end() {
|
||||||
if ( this._responseEnded ) throw new ResponseAlreadySentError(this)
|
if ( this._responseEnded ) throw new ResponseAlreadySentError(this)
|
||||||
this._sentHeaders = true
|
this._sentHeaders = true
|
||||||
|
@ -2,10 +2,17 @@ import {ResponseFactory} from "./ResponseFactory"
|
|||||||
import {Rehydratable} from "@extollo/util"
|
import {Rehydratable} from "@extollo/util"
|
||||||
import {Request} from "../lifecycle/Request";
|
import {Request} from "../lifecycle/Request";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function that creates a DehydratedStateResponseFactory.
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
export function dehydrate(value: Rehydratable): DehydratedStateResponseFactory {
|
export function dehydrate(value: Rehydratable): DehydratedStateResponseFactory {
|
||||||
return new DehydratedStateResponseFactory(value)
|
return new DehydratedStateResponseFactory(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response factor that sends a Rehydratable class' data as JSON.
|
||||||
|
*/
|
||||||
export class DehydratedStateResponseFactory extends ResponseFactory {
|
export class DehydratedStateResponseFactory extends ResponseFactory {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly rehydratable: Rehydratable
|
public readonly rehydratable: Rehydratable
|
||||||
|
@ -3,6 +3,12 @@ import {ErrorWithContext, HTTPStatus} from "@extollo/util"
|
|||||||
import {Request} from "../lifecycle/Request";
|
import {Request} from "../lifecycle/Request";
|
||||||
import * as api from "./api"
|
import * as api from "./api"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to create a new ErrorResponseFactory, with the given HTTP status and output format.
|
||||||
|
* @param error
|
||||||
|
* @param status
|
||||||
|
* @param output
|
||||||
|
*/
|
||||||
export function error(
|
export function error(
|
||||||
error: Error | string,
|
error: Error | string,
|
||||||
status: HTTPStatus = HTTPStatus.INTERNAL_SERVER_ERROR,
|
status: HTTPStatus = HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
@ -12,6 +18,9 @@ export function error(
|
|||||||
return new ErrorResponseFactory(error, status, output)
|
return new ErrorResponseFactory(error, status, output)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response factory that renders an Error object to the client in a specified format.
|
||||||
|
*/
|
||||||
export class ErrorResponseFactory extends ResponseFactory {
|
export class ErrorResponseFactory extends ResponseFactory {
|
||||||
protected targetMode: 'json' | 'html' | 'auto' = 'auto'
|
protected targetMode: 'json' | 'html' | 'auto' = 'auto'
|
||||||
|
|
||||||
|
@ -1,10 +1,17 @@
|
|||||||
import {ResponseFactory} from "./ResponseFactory";
|
import {ResponseFactory} from "./ResponseFactory";
|
||||||
import {Request} from "../lifecycle/Request";
|
import {Request} from "../lifecycle/Request";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function that creates a new HTMLResponseFactory.
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
export function html(value: string): HTMLResponseFactory {
|
export function html(value: string): HTMLResponseFactory {
|
||||||
return new HTMLResponseFactory(value)
|
return new HTMLResponseFactory(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response factory that writes a string to the response as HTML.
|
||||||
|
*/
|
||||||
export class HTMLResponseFactory extends ResponseFactory {
|
export class HTMLResponseFactory extends ResponseFactory {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly value: string,
|
public readonly value: string,
|
||||||
|
@ -2,10 +2,19 @@ import {ErrorResponseFactory} from "./ErrorResponseFactory";
|
|||||||
import {HTTPError} from "../HTTPError";
|
import {HTTPError} from "../HTTPError";
|
||||||
import {HTTPStatus} from "@extollo/util"
|
import {HTTPStatus} from "@extollo/util"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper that generates a new HTTPErrorResponseFactory given the HTTP status and message.
|
||||||
|
* @param status
|
||||||
|
* @param message
|
||||||
|
* @param output
|
||||||
|
*/
|
||||||
export function http(status: HTTPStatus, message?: string, output: 'json' | 'html' | 'auto' = 'auto'): HTTPErrorResponseFactory {
|
export function http(status: HTTPStatus, message?: string, output: 'json' | 'html' | 'auto' = 'auto'): HTTPErrorResponseFactory {
|
||||||
return new HTTPErrorResponseFactory(new HTTPError(status, message), output)
|
return new HTTPErrorResponseFactory(new HTTPError(status, message), output)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response factory that renders the given HTTPError in the specified output format.
|
||||||
|
*/
|
||||||
export class HTTPErrorResponseFactory extends ErrorResponseFactory {
|
export class HTTPErrorResponseFactory extends ErrorResponseFactory {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly error: HTTPError,
|
public readonly error: HTTPError,
|
||||||
|
@ -1,10 +1,17 @@
|
|||||||
import {ResponseFactory} from "./ResponseFactory";
|
import {ResponseFactory} from "./ResponseFactory";
|
||||||
import {Request} from "../lifecycle/Request";
|
import {Request} from "../lifecycle/Request";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to create a new JSONResponseFactory of the given value.
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
export function json(value: any): JSONResponseFactory {
|
export function json(value: any): JSONResponseFactory {
|
||||||
return new JSONResponseFactory(value)
|
return new JSONResponseFactory(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response factory that writes the given object as JSON to the response.
|
||||||
|
*/
|
||||||
export class JSONResponseFactory extends ResponseFactory {
|
export class JSONResponseFactory extends ResponseFactory {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly value: any
|
public readonly value: any
|
||||||
|
@ -1,14 +1,24 @@
|
|||||||
import {HTTPStatus} from "@extollo/util"
|
import {HTTPStatus} from "@extollo/util"
|
||||||
import {Request} from "../lifecycle/Request"
|
import {Request} from "../lifecycle/Request"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract class that defines "factory" that knows how to write a particular
|
||||||
|
* response to the response object.
|
||||||
|
*/
|
||||||
export abstract class ResponseFactory {
|
export abstract class ResponseFactory {
|
||||||
|
/** The status that should be set on the response. */
|
||||||
protected targetStatus: HTTPStatus = HTTPStatus.OK
|
protected targetStatus: HTTPStatus = HTTPStatus.OK
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called to write the response data to the HTTP response object.
|
||||||
|
* @param request
|
||||||
|
*/
|
||||||
public async write(request: Request): Promise<Request> {
|
public async write(request: Request): Promise<Request> {
|
||||||
request.response.setStatus(this.targetStatus)
|
request.response.setStatus(this.targetStatus)
|
||||||
return request
|
return request
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Set the target status of this factory. */
|
||||||
public status(status: HTTPStatus) {
|
public status(status: HTTPStatus) {
|
||||||
this.targetStatus = status
|
this.targetStatus = status
|
||||||
return this
|
return this
|
||||||
|
@ -1,12 +1,20 @@
|
|||||||
import {ResponseFactory} from "./ResponseFactory";
|
import {ResponseFactory} from "./ResponseFactory";
|
||||||
import {Request} from "../lifecycle/Request";
|
import {Request} from "../lifecycle/Request";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function that creates a new StringResponseFactory for the given string value.
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
export function plaintext(value: string): StringResponseFactory {
|
export function plaintext(value: string): StringResponseFactory {
|
||||||
return new StringResponseFactory(value)
|
return new StringResponseFactory(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response factory that renders a given string as the response in plaintext.
|
||||||
|
*/
|
||||||
export class StringResponseFactory extends ResponseFactory {
|
export class StringResponseFactory extends ResponseFactory {
|
||||||
constructor(
|
constructor(
|
||||||
|
/** The string to write as the body. */
|
||||||
public readonly value: string,
|
public readonly value: string,
|
||||||
) { super() }
|
) { super() }
|
||||||
|
|
||||||
|
@ -2,14 +2,22 @@ import {ResponseFactory} from "./ResponseFactory";
|
|||||||
import {HTTPStatus} from "@extollo/util";
|
import {HTTPStatus} from "@extollo/util";
|
||||||
import {Request} from "../lifecycle/Request";
|
import {Request} from "../lifecycle/Request";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to create a new TemporaryRedirectResponseFactory to the given destination.
|
||||||
|
* @param destination
|
||||||
|
*/
|
||||||
export function redirect(destination: string): TemporaryRedirectResponseFactory {
|
export function redirect(destination: string): TemporaryRedirectResponseFactory {
|
||||||
return new TemporaryRedirectResponseFactory(destination)
|
return new TemporaryRedirectResponseFactory(destination)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response factory that sends an HTTP redirect to the given destination.
|
||||||
|
*/
|
||||||
export class TemporaryRedirectResponseFactory extends ResponseFactory {
|
export class TemporaryRedirectResponseFactory extends ResponseFactory {
|
||||||
protected targetStatus: HTTPStatus = HTTPStatus.TEMPORARY_REDIRECT
|
protected targetStatus: HTTPStatus = HTTPStatus.TEMPORARY_REDIRECT
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
/** THe URL where the client should redirect to. */
|
||||||
public readonly destination: string
|
public readonly destination: string
|
||||||
) { super() }
|
) { super() }
|
||||||
|
|
||||||
|
@ -3,13 +3,25 @@ import {ResponseFactory} from "./ResponseFactory";
|
|||||||
import {Request} from "../lifecycle/Request";
|
import {Request} from "../lifecycle/Request";
|
||||||
import {ViewEngine} from "../../views/ViewEngine";
|
import {ViewEngine} from "../../views/ViewEngine";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function that creates a new ViewResponseFactory to render the given view
|
||||||
|
* with the specified data.
|
||||||
|
* @param name
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
export function view(name: string, data?: {[key: string]: any}): ViewResponseFactory {
|
export function view(name: string, data?: {[key: string]: any}): ViewResponseFactory {
|
||||||
return new ViewResponseFactory(name, data)
|
return new ViewResponseFactory(name, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP response factory that uses the ViewEngine service to render a view
|
||||||
|
* and send it as HTML.
|
||||||
|
*/
|
||||||
export class ViewResponseFactory extends ResponseFactory {
|
export class ViewResponseFactory extends ResponseFactory {
|
||||||
constructor(
|
constructor(
|
||||||
|
/** The name of the view to render. */
|
||||||
public readonly viewName: string,
|
public readonly viewName: string,
|
||||||
|
/** Optional data that should be passed to the view engine as params. */
|
||||||
public readonly data?: {[key: string]: any}
|
public readonly data?: {[key: string]: any}
|
||||||
) { super() }
|
) { super() }
|
||||||
|
|
||||||
|
@ -1,14 +1,47 @@
|
|||||||
import {ErrorWithContext} from "@extollo/util";
|
import {ErrorWithContext} from "@extollo/util";
|
||||||
import {ResolvedRouteHandler, Route} from "./Route";
|
import {ResolvedRouteHandler, Route} from "./Route";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing a resolved route that a request is mounted to.
|
||||||
|
*/
|
||||||
export class ActivatedRoute {
|
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}
|
public readonly params: {[key: string]: string}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The resolved function that should handle the request for this route.
|
||||||
|
*/
|
||||||
public readonly handler: ResolvedRouteHandler
|
public readonly handler: ResolvedRouteHandler
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pre-middleware that should be applied to the request on this route.
|
||||||
|
*/
|
||||||
public readonly preflight: ResolvedRouteHandler[]
|
public readonly preflight: ResolvedRouteHandler[]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post-middleware that should be applied to the request on this route.
|
||||||
|
*/
|
||||||
public readonly postflight: ResolvedRouteHandler[]
|
public readonly postflight: ResolvedRouteHandler[]
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
/** The route this ActivatedRoute refers to. */
|
||||||
public readonly route: Route,
|
public readonly route: Route,
|
||||||
|
|
||||||
|
/** The request path that activated that route. */
|
||||||
public readonly path: string
|
public readonly path: string
|
||||||
) {
|
) {
|
||||||
const params = route.extract(path)
|
const params = route.extract(path)
|
||||||
|
@ -2,8 +2,12 @@ import {AppClass} from "../../lifecycle/AppClass"
|
|||||||
import {Request} from "../lifecycle/Request"
|
import {Request} from "../lifecycle/Request"
|
||||||
import {ResponseObject} from "./Route"
|
import {ResponseObject} from "./Route"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class representing a middleware handler that can be applied to routes.
|
||||||
|
*/
|
||||||
export abstract class Middleware extends AppClass {
|
export abstract class Middleware extends AppClass {
|
||||||
constructor(
|
constructor(
|
||||||
|
/** The request that will be handled by this middleware. */
|
||||||
protected readonly request: Request
|
protected readonly request: Request
|
||||||
) { super() }
|
) { super() }
|
||||||
|
|
||||||
@ -11,6 +15,13 @@ export abstract class Middleware extends AppClass {
|
|||||||
return this.request
|
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
|
public abstract apply(): ResponseObject
|
||||||
}
|
}
|
||||||
|
@ -12,23 +12,66 @@ import {Middlewares} from "../../service/Middlewares";
|
|||||||
import {Middleware} from "./Middleware";
|
import {Middleware} from "./Middleware";
|
||||||
import {Config} from "../../service/Config";
|
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>
|
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
|
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
|
export type ResolvedRouteHandler = (request: Request) => ResponseObject
|
||||||
|
|
||||||
|
|
||||||
// TODO domains, named routes - support this on groups as well
|
// 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 {
|
export class Route extends AppClass {
|
||||||
|
/** Routes that have been created and registered in the application. */
|
||||||
private static registeredRoutes: Route[] = []
|
private static registeredRoutes: Route[] = []
|
||||||
|
|
||||||
|
/** Groups of routes that have been registered with the application. */
|
||||||
private static registeredGroups: RouteGroup[] = []
|
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[] = []
|
private static compiledGroupStack: RouteGroup[] = []
|
||||||
|
|
||||||
|
/** Register a route group handler. */
|
||||||
public static registerGroup(group: RouteGroup) {
|
public static registerGroup(group: RouteGroup) {
|
||||||
this.registeredGroups.push(group)
|
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[]> {
|
public static async compile(): Promise<Route[]> {
|
||||||
let registeredRoutes = this.registeredRoutes
|
let registeredRoutes = this.registeredRoutes
|
||||||
const registeredGroups = this.registeredGroups
|
const registeredGroups = this.registeredGroups
|
||||||
@ -103,53 +146,87 @@ export class Route extends AppClass {
|
|||||||
return registeredRoutes
|
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) {
|
public static endpoint(method: HTTPMethod | HTTPMethod[], definition: string, handler: RouteHandler) {
|
||||||
const route = new Route(method, handler, definition)
|
const route = new Route(method, handler, definition)
|
||||||
this.registeredRoutes.push(route)
|
this.registeredRoutes.push(route)
|
||||||
return route
|
return route
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new GET route on the given endpoint.
|
||||||
|
* @param definition
|
||||||
|
* @param handler
|
||||||
|
*/
|
||||||
public static get(definition: string, handler: RouteHandler) {
|
public static get(definition: string, handler: RouteHandler) {
|
||||||
return this.endpoint('get', definition, handler)
|
return this.endpoint('get', definition, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Create a new POST route on the given endpoint. */
|
||||||
public static post(definition: string, handler: RouteHandler) {
|
public static post(definition: string, handler: RouteHandler) {
|
||||||
return this.endpoint('post', definition, handler)
|
return this.endpoint('post', definition, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Create a new PUT route on the given endpoint. */
|
||||||
public static put(definition: string, handler: RouteHandler) {
|
public static put(definition: string, handler: RouteHandler) {
|
||||||
return this.endpoint('put', definition, handler)
|
return this.endpoint('put', definition, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Create a new PATCH route on the given endpoint. */
|
||||||
public static patch(definition: string, handler: RouteHandler) {
|
public static patch(definition: string, handler: RouteHandler) {
|
||||||
return this.endpoint('patch', definition, handler)
|
return this.endpoint('patch', definition, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Create a new DELETE route on the given endpoint. */
|
||||||
public static delete(definition: string, handler: RouteHandler) {
|
public static delete(definition: string, handler: RouteHandler) {
|
||||||
return this.endpoint('delete', definition, handler)
|
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) {
|
public static any(definition: string, handler: RouteHandler) {
|
||||||
return this.endpoint(['get', 'put', 'patch', 'post', 'delete'], definition, handler)
|
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>) {
|
public static group(prefix: string, group: () => void | Promise<void>) {
|
||||||
const grp = <RouteGroup> Application.getApplication().make(RouteGroup, group, prefix)
|
const grp = <RouteGroup> Application.getApplication().make(RouteGroup, group, prefix)
|
||||||
this.registeredGroups.push(grp)
|
this.registeredGroups.push(grp)
|
||||||
return 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}>()
|
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[]
|
protected _compiledPreflight?: ResolvedRouteHandler[]
|
||||||
|
|
||||||
|
/** Pre-compiled route handlers for the post-middleware for this route. */
|
||||||
protected _compiledHandler?: ResolvedRouteHandler
|
protected _compiledHandler?: ResolvedRouteHandler
|
||||||
|
|
||||||
|
/** Pre-compiled route handler for the main route handler for this route. */
|
||||||
protected _compiledPostflight?: ResolvedRouteHandler[]
|
protected _compiledPostflight?: ResolvedRouteHandler[]
|
||||||
|
|
||||||
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,
|
protected readonly handler: RouteHandler,
|
||||||
|
|
||||||
|
/** The route path this route listens on. */
|
||||||
protected route: string
|
protected route: string
|
||||||
) { super() }
|
) { 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 {
|
public match(method: HTTPMethod, potential: string): boolean {
|
||||||
if ( Array.isArray(this.method) && !this.method.includes(method) ) return false
|
if ( Array.isArray(this.method) && !this.method.includes(method) ) return false
|
||||||
else if ( !Array.isArray(this.method) && this.method !== 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)
|
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 {
|
public extract(potential: string): {[key: string]: string} | undefined {
|
||||||
const routeParts = (this.route.startsWith('/') ? this.route.substr(1) : this.route).split('/')
|
const routeParts = (this.route.startsWith('/') ? this.route.substr(1) : this.route).split('/')
|
||||||
const potentialParts = (potential.startsWith('/') ? potential.substr(1) : potential).split('/')
|
const potentialParts = (potential.startsWith('/') ? potential.substr(1) : potential).split('/')
|
||||||
@ -192,6 +283,9 @@ export class Route extends AppClass {
|
|||||||
return params
|
return params
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to pre-compile and return the preflight handlers for this route.
|
||||||
|
*/
|
||||||
public resolvePreflight(): ResolvedRouteHandler[] {
|
public resolvePreflight(): ResolvedRouteHandler[] {
|
||||||
if ( !this._compiledPreflight ) {
|
if ( !this._compiledPreflight ) {
|
||||||
this._compiledPreflight = this.resolveMiddlewareHandlersForStage('pre')
|
this._compiledPreflight = this.resolveMiddlewareHandlersForStage('pre')
|
||||||
@ -200,6 +294,9 @@ export class Route extends AppClass {
|
|||||||
return this._compiledPreflight
|
return this._compiledPreflight
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to pre-compile and return the postflight handlers for this route.
|
||||||
|
*/
|
||||||
public resolvePostflight(): ResolvedRouteHandler[] {
|
public resolvePostflight(): ResolvedRouteHandler[] {
|
||||||
if ( !this._compiledPostflight ) {
|
if ( !this._compiledPostflight ) {
|
||||||
this._compiledPostflight = this.resolveMiddlewareHandlersForStage('post')
|
this._compiledPostflight = this.resolveMiddlewareHandlersForStage('post')
|
||||||
@ -208,6 +305,9 @@ export class Route extends AppClass {
|
|||||||
return this._compiledPostflight
|
return this._compiledPostflight
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to pre-compile and return the main handler for this route.
|
||||||
|
*/
|
||||||
public resolveHandler(): ResolvedRouteHandler {
|
public resolveHandler(): ResolvedRouteHandler {
|
||||||
if ( !this._compiledHandler ) {
|
if ( !this._compiledHandler ) {
|
||||||
this._compiledHandler = this._resolveHandler()
|
this._compiledHandler = this._resolveHandler()
|
||||||
@ -216,6 +316,7 @@ export class Route extends AppClass {
|
|||||||
return this._compiledHandler
|
return this._compiledHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Register the given middleware as a preflight handler for this route. */
|
||||||
pre(middleware: RouteHandler) {
|
pre(middleware: RouteHandler) {
|
||||||
this.middlewares.push({
|
this.middlewares.push({
|
||||||
stage: 'pre',
|
stage: 'pre',
|
||||||
@ -225,6 +326,7 @@ export class Route extends AppClass {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Register the given middleware as a postflight handler for this route. */
|
||||||
post(middleware: RouteHandler) {
|
post(middleware: RouteHandler) {
|
||||||
this.middlewares.push({
|
this.middlewares.push({
|
||||||
stage: 'post',
|
stage: 'post',
|
||||||
@ -234,20 +336,27 @@ export class Route extends AppClass {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Prefix the route's path with the given prefix, normalizing `/` characters. */
|
||||||
private prepend(prefix: string) {
|
private prepend(prefix: string) {
|
||||||
if ( !prefix.endsWith('/') ) prefix = `${prefix}/`
|
if ( !prefix.endsWith('/') ) prefix = `${prefix}/`
|
||||||
if ( this.route.startsWith('/') ) this.route = this.route.substring(1)
|
if ( this.route.startsWith('/') ) this.route = this.route.substring(1)
|
||||||
this.route = `${prefix}${this.route}`
|
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 }) {
|
private prependMiddleware(def: { stage: 'pre' | 'post', handler: RouteHandler }) {
|
||||||
this.middlewares.prepend(def)
|
this.middlewares.prepend(def)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Add the given middleware item to the end of the postflight handlers. */
|
||||||
private appendMiddleware(def: { stage: 'pre' | 'post', handler: RouteHandler }) {
|
private appendMiddleware(def: { stage: 'pre' | 'post', handler: RouteHandler }) {
|
||||||
this.middlewares.push(def)
|
this.middlewares.push(def)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve and return the route handler for this route.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
private _resolveHandler(): ResolvedRouteHandler {
|
private _resolveHandler(): ResolvedRouteHandler {
|
||||||
if ( typeof this.handler !== 'string' ) {
|
if ( typeof this.handler !== 'string' ) {
|
||||||
return (request: Request) => {
|
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[] {
|
private resolveMiddlewareHandlersForStage(stage: 'pre' | 'post'): ResolvedRouteHandler[] {
|
||||||
return this.middlewares.where('stage', '=', stage)
|
return this.middlewares.where('stage', '=', stage)
|
||||||
.map<ResolvedRouteHandler>(def => {
|
.map<ResolvedRouteHandler>(def => {
|
||||||
@ -330,6 +444,7 @@ export class Route extends AppClass {
|
|||||||
.toArray()
|
.toArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Cast the route to an intelligible string. */
|
||||||
toString() {
|
toString() {
|
||||||
const method = Array.isArray(this.method) ? this.method : [this.method]
|
const method = Array.isArray(this.method) ? this.method : [this.method]
|
||||||
return `${method.join('|')} -> ${this.route}`
|
return `${method.join('|')} -> ${this.route}`
|
||||||
|
@ -4,17 +4,50 @@ import {RouteHandler} from "./Route"
|
|||||||
import {Container} from "@extollo/di"
|
import {Container} from "@extollo/di"
|
||||||
import {Logging} from "../../service/Logging";
|
import {Logging} from "../../service/Logging";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class that defines a group of Routes in the application, with a prefix.
|
||||||
|
*/
|
||||||
export class RouteGroup extends AppClass {
|
export class RouteGroup extends AppClass {
|
||||||
|
/**
|
||||||
|
* The current set of nested groups. This is used when compiling route groups.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
private static currentGroupNesting: RouteGroup[] = []
|
private static currentGroupNesting: RouteGroup[] = []
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapping of group names to group registration functions.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
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}>()
|
protected middlewares: Collection<{ stage: 'pre' | 'post', handler: RouteHandler }> = new Collection<{stage: "pre" | "post"; handler: RouteHandler}>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current group nesting.
|
||||||
|
*/
|
||||||
public static getCurrentGroupHierarchy(): RouteGroup[] {
|
public static getCurrentGroupHierarchy(): RouteGroup[] {
|
||||||
return [...this.currentGroupNesting]
|
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) {
|
public static named(name: string, define: () => void) {
|
||||||
if ( this.namedGroups[name] ) {
|
if ( this.namedGroups[name] ) {
|
||||||
Container.getContainer()
|
Container.getContainer()
|
||||||
@ -25,6 +58,17 @@ export class RouteGroup extends AppClass {
|
|||||||
this.namedGroups[name] = define
|
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) {
|
public static include(name: string) {
|
||||||
if (!this.namedGroups[name]) {
|
if (!this.namedGroups[name]) {
|
||||||
throw new ErrorWithContext(`No route group exists with name: ${name}`, {name})
|
throw new ErrorWithContext(`No route group exists with name: ${name}`, {name})
|
||||||
@ -35,10 +79,14 @@ export class RouteGroup extends AppClass {
|
|||||||
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
/** Function to register routes for this group. */
|
||||||
public readonly group: () => void | Promise<void>,
|
public readonly group: () => void | Promise<void>,
|
||||||
|
|
||||||
|
/** The route prefix of this group. */
|
||||||
public readonly prefix: string
|
public readonly prefix: string
|
||||||
) { super() }
|
) { super() }
|
||||||
|
|
||||||
|
/** Register the given middleware to be applied before all routes in this group. */
|
||||||
pre(middleware: RouteHandler) {
|
pre(middleware: RouteHandler) {
|
||||||
this.middlewares.push({
|
this.middlewares.push({
|
||||||
stage: 'pre',
|
stage: 'pre',
|
||||||
@ -48,6 +96,7 @@ export class RouteGroup extends AppClass {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Register the given middleware to be applied after all routes in this group. */
|
||||||
post(middleware: RouteHandler) {
|
post(middleware: RouteHandler) {
|
||||||
this.middlewares.push({
|
this.middlewares.push({
|
||||||
stage: 'post',
|
stage: 'post',
|
||||||
@ -57,6 +106,7 @@ export class RouteGroup extends AppClass {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Return the middlewares that apply to this group. */
|
||||||
getGroupMiddlewareDefinitions() {
|
getGroupMiddlewareDefinitions() {
|
||||||
return this.middlewares
|
return this.middlewares
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,17 @@
|
|||||||
import {NoSessionKeyError, Session, SessionData, SessionNotLoadedError} from "./Session";
|
import {NoSessionKeyError, Session, SessionData, SessionNotLoadedError} from "./Session";
|
||||||
import {Injectable} from "@extollo/di";
|
import {Injectable} from "@extollo/di";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of the session driver that stores session data in memory.
|
||||||
|
* This is the default, for compatibility, but it is recommended that you replace
|
||||||
|
* this driver with one with a persistent backend.
|
||||||
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MemorySession extends Session {
|
export class MemorySession extends Session {
|
||||||
|
/** Mapping of session key to session data object. */
|
||||||
private static sessionsByID: {[key: string]: SessionData} = {}
|
private static sessionsByID: {[key: string]: SessionData} = {}
|
||||||
|
|
||||||
|
/** Get a particular session by ID. */
|
||||||
private static getSession(id: string) {
|
private static getSession(id: string) {
|
||||||
if ( !this.sessionsByID[id] ) {
|
if ( !this.sessionsByID[id] ) {
|
||||||
this.sessionsByID[id] = {} as SessionData
|
this.sessionsByID[id] = {} as SessionData
|
||||||
@ -13,11 +20,15 @@ export class MemorySession extends Session {
|
|||||||
return this.sessionsByID[id]
|
return this.sessionsByID[id]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Store the given session data by its ID. */
|
||||||
private static setSession(id: string, data: SessionData) {
|
private static setSession(id: string, data: SessionData) {
|
||||||
this.sessionsByID[id] = data
|
this.sessionsByID[id] = data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The ID of this session. */
|
||||||
protected sessionID?: string
|
protected sessionID?: string
|
||||||
|
|
||||||
|
/** The associated data for this session. */
|
||||||
protected data?: SessionData
|
protected data?: SessionData
|
||||||
|
|
||||||
constructor() { super() }
|
constructor() { super() }
|
||||||
|
@ -2,38 +2,59 @@ import {Injectable, Inject} from "@extollo/di"
|
|||||||
import {ErrorWithContext} from "@extollo/util"
|
import {ErrorWithContext} from "@extollo/util"
|
||||||
import {Request} from "../lifecycle/Request"
|
import {Request} from "../lifecycle/Request"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type alias describing some inflated session data.
|
||||||
|
*/
|
||||||
export type SessionData = {[key: string]: any}
|
export type SessionData = {[key: string]: any}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error thrown when a session is requested for a key that does not exist.
|
||||||
|
*/
|
||||||
export class NoSessionKeyError extends ErrorWithContext {
|
export class NoSessionKeyError extends ErrorWithContext {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('No session ID has been set.')
|
super('No session ID has been set.')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error thrown when a session operation is performed before the session has been loaded.
|
||||||
|
*/
|
||||||
export class SessionNotLoadedError extends ErrorWithContext {
|
export class SessionNotLoadedError extends ErrorWithContext {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('Cannot access session data; data is not loaded.')
|
super('Cannot access session data; data is not loaded.')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An abstract class representing a session driver.
|
||||||
|
* Some implementation of this is injected into the request.
|
||||||
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export abstract class Session {
|
export abstract class Session {
|
||||||
@Inject()
|
@Inject()
|
||||||
protected readonly request!: Request
|
protected readonly request!: Request
|
||||||
|
|
||||||
|
/** Get the unique key of this session. */
|
||||||
public abstract getKey(): string
|
public abstract getKey(): string
|
||||||
|
|
||||||
|
/** Set a unique key of this session. */
|
||||||
public abstract setKey(key: string): void
|
public abstract setKey(key: string): void
|
||||||
|
|
||||||
|
/** Load the session data from the respective backend. */
|
||||||
public abstract load(): void | Promise<void>
|
public abstract load(): void | Promise<void>
|
||||||
|
|
||||||
|
/** Save the session data into the respective backend. */
|
||||||
public abstract persist(): void | Promise<void>
|
public abstract persist(): void | Promise<void>
|
||||||
|
|
||||||
|
/** Get the loaded session data as an object. */
|
||||||
public abstract getData(): SessionData
|
public abstract getData(): SessionData
|
||||||
|
|
||||||
|
/** Bulk set an object as the session data. */
|
||||||
public abstract setData(data: SessionData): void
|
public abstract setData(data: SessionData): void
|
||||||
|
|
||||||
|
/** Get a value from the session by key. */
|
||||||
public abstract get(key: string, fallback?: any): any
|
public abstract get(key: string, fallback?: any): any
|
||||||
|
|
||||||
|
/** Set a value in the session by key. */
|
||||||
public abstract set(key: string, value: any): void
|
public abstract set(key: string, value: any): void
|
||||||
}
|
}
|
||||||
|
@ -13,10 +13,15 @@ import {Session} from "./Session";
|
|||||||
import {Logging} from "../../service/Logging";
|
import {Logging} from "../../service/Logging";
|
||||||
import {Config} from "../../service/Config";
|
import {Config} from "../../service/Config";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A dependency injection factory that matches the abstract Session class
|
||||||
|
* and produces an instance of the configured session driver implementation.
|
||||||
|
*/
|
||||||
export class SessionFactory extends AbstractFactory {
|
export class SessionFactory extends AbstractFactory {
|
||||||
protected readonly logging: Logging
|
protected readonly logging: Logging
|
||||||
protected readonly config: Config
|
protected readonly config: Config
|
||||||
|
|
||||||
|
/** True if we have printed the memory session warning at least once. */
|
||||||
private static loggedMemorySessionWarningOnce = false
|
private static loggedMemorySessionWarningOnce = false
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -52,6 +57,11 @@ export class SessionFactory extends AbstractFactory {
|
|||||||
return meta
|
return meta
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the instantiable class of the configured session backend.
|
||||||
|
* @protected
|
||||||
|
* @return Instantiable<Session>
|
||||||
|
*/
|
||||||
protected getSessionClass() {
|
protected getSessionClass() {
|
||||||
const SessionClass = this.config.get('server.session.driver', MemorySession)
|
const SessionClass = this.config.get('server.session.driver', MemorySession)
|
||||||
if ( SessionClass === MemorySession && !SessionFactory.loggedMemorySessionWarningOnce ) {
|
if ( SessionClass === MemorySession && !SessionFactory.loggedMemorySessionWarningOnce ) {
|
||||||
|
@ -22,21 +22,28 @@ export function isBindable(what: any): what is Bindable {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base for classes that gives access to the global application and container.
|
||||||
|
*/
|
||||||
export class AppClass {
|
export class AppClass {
|
||||||
|
/** The global application instance. */
|
||||||
private readonly appClassApplication!: Application;
|
private readonly appClassApplication!: Application;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.appClassApplication = Application.getApplication();
|
this.appClassApplication = Application.getApplication();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get the global Application. */
|
||||||
protected app(): Application {
|
protected app(): Application {
|
||||||
return this.appClassApplication;
|
return this.appClassApplication;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get the global Container. */
|
||||||
protected container(): Container {
|
protected container(): Container {
|
||||||
return this.appClassApplication;
|
return this.appClassApplication;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Call the `make()` method on the global container. */
|
||||||
protected make<T>(target: DependencyKey, ...parameters: any[]): T {
|
protected make<T>(target: DependencyKey, ...parameters: any[]): T {
|
||||||
return this.container().make<T>(target, ...parameters)
|
return this.container().make<T>(target, ...parameters)
|
||||||
}
|
}
|
||||||
|
@ -16,10 +16,21 @@ import {Unit, UnitStatus} from "./Unit";
|
|||||||
import * as dotenv from 'dotenv';
|
import * as dotenv from 'dotenv';
|
||||||
import {CacheFactory} from "../support/cache/CacheFactory";
|
import {CacheFactory} from "../support/cache/CacheFactory";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function that resolves and infers environment variable values.
|
||||||
|
*
|
||||||
|
* If none is found, returns `defaultValue`.
|
||||||
|
*
|
||||||
|
* @param key
|
||||||
|
* @param defaultValue
|
||||||
|
*/
|
||||||
export function env(key: string, defaultValue?: any): any {
|
export function env(key: string, defaultValue?: any): any {
|
||||||
return Application.getApplication().env(key, defaultValue)
|
return Application.getApplication().env(key, defaultValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The main application container.
|
||||||
|
*/
|
||||||
export class Application extends Container {
|
export class Application extends Container {
|
||||||
public static getContainer(): Container {
|
public static getContainer(): Container {
|
||||||
const existing = <Container | undefined> globalRegistry.getGlobal('extollo/injector')
|
const existing = <Container | undefined> globalRegistry.getGlobal('extollo/injector')
|
||||||
@ -32,6 +43,9 @@ export class Application extends Container {
|
|||||||
return existing as Container
|
return existing as Container
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the global application instance.
|
||||||
|
*/
|
||||||
public static getApplication(): Application {
|
public static getApplication(): Application {
|
||||||
const existing = <Container | undefined> globalRegistry.getGlobal('extollo/injector')
|
const existing = <Container | undefined> globalRegistry.getGlobal('extollo/injector')
|
||||||
if ( existing instanceof Application ) {
|
if ( existing instanceof Application ) {
|
||||||
@ -49,11 +63,34 @@ export class Application extends Container {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The fully-qualified path to the base directory of the app.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
protected baseDir!: string
|
protected baseDir!: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolved universal path to the base directory of the app.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
protected basePath!: UniversalPath
|
protected basePath!: UniversalPath
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Unit classes registered with the app.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
protected applicationUnits: (typeof Unit)[] = []
|
protected applicationUnits: (typeof Unit)[] = []
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instances of the units registered with this app.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
protected instantiatedUnits: Unit[] = []
|
protected instantiatedUnits: Unit[] = []
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If true, the "Starting Extollo..." messages will always
|
||||||
|
* be logged.
|
||||||
|
*/
|
||||||
public forceStartupMessage: boolean = true
|
public forceStartupMessage: boolean = true
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -72,36 +109,67 @@ export class Application extends Container {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the given unit class is registered with the application.
|
||||||
|
* @param unitClass
|
||||||
|
*/
|
||||||
public hasUnit(unitClass: typeof Unit) {
|
public hasUnit(unitClass: typeof Unit) {
|
||||||
return this.applicationUnits.includes(unitClass)
|
return this.applicationUnits.includes(unitClass)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a UniversalPath to the root of the application.
|
||||||
|
*/
|
||||||
get root() {
|
get root() {
|
||||||
return this.basePath.concat()
|
return this.basePath.concat()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a UniversalPath to the `app/` directory in the application.
|
||||||
|
*/
|
||||||
get appRoot() {
|
get appRoot() {
|
||||||
return this.basePath.concat('app')
|
return this.basePath.concat('app')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a path relative to the root of the application.
|
||||||
|
* @param parts
|
||||||
|
*/
|
||||||
path(...parts: PathLike[]) {
|
path(...parts: PathLike[]) {
|
||||||
return this.basePath.concat(...parts)
|
return this.basePath.concat(...parts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a path relative to the `app/` directory in the application.
|
||||||
|
* @param parts
|
||||||
|
*/
|
||||||
appPath(...parts: PathLike[]) {
|
appPath(...parts: PathLike[]) {
|
||||||
return this.basePath.concat('app', ...parts)
|
return this.basePath.concat('app', ...parts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an instance of the RunLevelErrorHandler.
|
||||||
|
*/
|
||||||
get errorHandler() {
|
get errorHandler() {
|
||||||
const rleh: RunLevelErrorHandler = this.make<RunLevelErrorHandler>(RunLevelErrorHandler)
|
const rleh: RunLevelErrorHandler = this.make<RunLevelErrorHandler>(RunLevelErrorHandler)
|
||||||
return rleh.handle
|
return rleh.handle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap a base Error instance into an ErrorWithContext.
|
||||||
|
* @param e
|
||||||
|
* @param context
|
||||||
|
*/
|
||||||
errorWrapContext(e: Error, context: {[key: string]: any}): ErrorWithContext {
|
errorWrapContext(e: Error, context: {[key: string]: any}): ErrorWithContext {
|
||||||
const rleh: RunLevelErrorHandler = this.make<RunLevelErrorHandler>(RunLevelErrorHandler)
|
const rleh: RunLevelErrorHandler = this.make<RunLevelErrorHandler>(RunLevelErrorHandler)
|
||||||
return rleh.wrapContext(e, context)
|
return rleh.wrapContext(e, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up the bare essentials to get the application up and running.
|
||||||
|
* @param absolutePathToApplicationRoot
|
||||||
|
* @param applicationUnits
|
||||||
|
*/
|
||||||
scaffold(absolutePathToApplicationRoot: string, applicationUnits: (typeof Unit)[]) {
|
scaffold(absolutePathToApplicationRoot: string, applicationUnits: (typeof Unit)[]) {
|
||||||
this.baseDir = absolutePathToApplicationRoot
|
this.baseDir = absolutePathToApplicationRoot
|
||||||
this.basePath = universalPath(absolutePathToApplicationRoot)
|
this.basePath = universalPath(absolutePathToApplicationRoot)
|
||||||
@ -115,6 +183,10 @@ export class Application extends Container {
|
|||||||
this.make<Logging>(Logging).debug(`Application root: ${this.baseDir}`)
|
this.make<Logging>(Logging).debug(`Application root: ${this.baseDir}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the logger and load the logging level from the environment.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
protected setupLogging() {
|
protected setupLogging() {
|
||||||
const standard: StandardLogger = this.make<StandardLogger>(StandardLogger)
|
const standard: StandardLogger = this.make<StandardLogger>(StandardLogger)
|
||||||
const logging: Logging = this.make<Logging>(Logging)
|
const logging: Logging = this.make<Logging>(Logging)
|
||||||
@ -134,16 +206,29 @@ export class Application extends Container {
|
|||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the environment variable library and read from the `.env` file.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
protected bootstrapEnvironment() {
|
protected bootstrapEnvironment() {
|
||||||
dotenv.config({
|
dotenv.config({
|
||||||
path: this.basePath.concat('.env').toLocal
|
path: this.basePath.concat('.env').toLocal
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a value from the loaded environment variables.
|
||||||
|
* If no value could be found, the default value will be returned.
|
||||||
|
* @param key
|
||||||
|
* @param defaultValue
|
||||||
|
*/
|
||||||
public env(key: string, defaultValue?: any): any {
|
public env(key: string, defaultValue?: any): any {
|
||||||
return infer(process.env[key] ?? '') ?? defaultValue
|
return infer(process.env[key] ?? '') ?? defaultValue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the application by starting all units in order, then stopping them in reverse order.
|
||||||
|
*/
|
||||||
async run() {
|
async run() {
|
||||||
try {
|
try {
|
||||||
await this.up()
|
await this.up()
|
||||||
@ -153,6 +238,9 @@ export class Application extends Container {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start all units in the application, one at a time, in order.
|
||||||
|
*/
|
||||||
async up() {
|
async up() {
|
||||||
const logging: Logging = this.make<Logging>(Logging)
|
const logging: Logging = this.make<Logging>(Logging)
|
||||||
|
|
||||||
@ -164,6 +252,9 @@ export class Application extends Container {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop all units in the application, one at a time, in reverse order.
|
||||||
|
*/
|
||||||
async down() {
|
async down() {
|
||||||
const logging: Logging = this.make<Logging>(Logging)
|
const logging: Logging = this.make<Logging>(Logging)
|
||||||
|
|
||||||
@ -174,6 +265,10 @@ export class Application extends Container {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a single unit, setting its status.
|
||||||
|
* @param unit
|
||||||
|
*/
|
||||||
public async startUnit(unit: Unit) {
|
public async startUnit(unit: Unit) {
|
||||||
const logging: Logging = this.make<Logging>(Logging)
|
const logging: Logging = this.make<Logging>(Logging)
|
||||||
|
|
||||||
@ -190,6 +285,10 @@ export class Application extends Container {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop a single unit, setting its status.
|
||||||
|
* @param unit
|
||||||
|
*/
|
||||||
public async stopUnit(unit: Unit) {
|
public async stopUnit(unit: Unit) {
|
||||||
const logging: Logging = this.make<Logging>(Logging)
|
const logging: Logging = this.make<Logging>(Logging)
|
||||||
|
|
||||||
|
@ -3,6 +3,11 @@ import {Logging} from "../service/Logging";
|
|||||||
import {Inject} from "@extollo/di";
|
import {Inject} from "@extollo/di";
|
||||||
import {ErrorWithContext} from "@extollo/util";
|
import {ErrorWithContext} from "@extollo/util";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class with logic for handling errors that are thrown at the run-level of the application.
|
||||||
|
*
|
||||||
|
* Colloquially, these are errors thrown ourside the request-lifecycle that are not caught by a unit.
|
||||||
|
*/
|
||||||
export class RunLevelErrorHandler {
|
export class RunLevelErrorHandler {
|
||||||
@Inject()
|
@Inject()
|
||||||
protected logging!: Logging
|
protected logging!: Logging
|
||||||
@ -18,6 +23,11 @@ export class RunLevelErrorHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap the given base Error instance into an ErrorWithContext.
|
||||||
|
* @param e
|
||||||
|
* @param context
|
||||||
|
*/
|
||||||
wrapContext(e: Error, context: {[key: string]: any}): ErrorWithContext {
|
wrapContext(e: Error, context: {[key: string]: any}): ErrorWithContext {
|
||||||
if ( e instanceof ErrorWithContext ) {
|
if ( e instanceof ErrorWithContext ) {
|
||||||
e.context = {...e.context, ...context}
|
e.context = {...e.context, ...context}
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import {AppClass} from './AppClass';
|
import {AppClass} from './AppClass';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The various statuses of a Unit.
|
||||||
|
*/
|
||||||
export enum UnitStatus {
|
export enum UnitStatus {
|
||||||
Starting,
|
Starting,
|
||||||
Started,
|
Started,
|
||||||
@ -8,8 +11,26 @@ export enum UnitStatus {
|
|||||||
Error,
|
Error,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for a service that can be registered with the application
|
||||||
|
* that is started and stopped during the application lifecycle.
|
||||||
|
*/
|
||||||
export abstract class Unit extends AppClass {
|
export abstract class Unit extends AppClass {
|
||||||
|
/** The current status of the unit. */
|
||||||
public status: UnitStatus = UnitStatus.Stopped
|
public status: UnitStatus = UnitStatus.Stopped
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called to start the unit when the application is booting.
|
||||||
|
* Here, you should do any setup required to get the package up and running.
|
||||||
|
*/
|
||||||
public up(): Promise<void> | void {}
|
public up(): Promise<void> | void {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called to stop the unit when the application is shutting down.
|
||||||
|
* Here, you should do any teardown required to stop the package cleanly.
|
||||||
|
*
|
||||||
|
* IN PARTICULAR take care to free blocking resources that could prevent the
|
||||||
|
* process from exiting without a kill.
|
||||||
|
*/
|
||||||
public down(): Promise<void> | void {}
|
public down(): Promise<void> | void {}
|
||||||
}
|
}
|
||||||
|
@ -8,12 +8,18 @@ import {Inject} from "@extollo/di";
|
|||||||
import * as nodePath from 'path'
|
import * as nodePath from 'path'
|
||||||
import {Unit} from "../lifecycle/Unit";
|
import {Unit} from "../lifecycle/Unit";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface describing a definition of a single canonical item loaded from the app.
|
||||||
|
*/
|
||||||
export interface CanonicalDefinition {
|
export interface CanonicalDefinition {
|
||||||
canonicalName: string,
|
canonicalName: string,
|
||||||
originalName: string,
|
originalName: string,
|
||||||
imported: any,
|
imported: any,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type alias for a function that resolves a canonical name to a canonical item, if one exists.
|
||||||
|
*/
|
||||||
export type CanonicalResolver<T> = (key: string) => T | undefined
|
export type CanonicalResolver<T> = (key: string) => T | undefined
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -25,6 +31,19 @@ export interface CanonicalReference {
|
|||||||
particular?: string,
|
particular?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract unit type that loads items recursively from a directory structure, assigning
|
||||||
|
* them normalized names ("canonical names"), and providing a way to fetch the resources
|
||||||
|
* by name.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* The Config service is a Canonical derivative that loads files ending with `.config.js`
|
||||||
|
* from the `app/config` directory.
|
||||||
|
*
|
||||||
|
* If, for example, there is a config file `app/config/auth/Forms.config.js` (in the
|
||||||
|
* generated code), it can be loaded by the canonical name `auth:Forms`.
|
||||||
|
*
|
||||||
|
*/
|
||||||
export abstract class Canonical<T> extends Unit {
|
export abstract class Canonical<T> extends Unit {
|
||||||
@Inject()
|
@Inject()
|
||||||
protected readonly logging!: Logging
|
protected readonly logging!: Logging
|
||||||
@ -81,18 +100,26 @@ export abstract class Canonical<T> extends Unit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an array of all loaded canonical names.
|
||||||
|
*/
|
||||||
public all(): string[] {
|
public all(): string[] {
|
||||||
return Object.keys(this.loadedItems)
|
return Object.keys(this.loadedItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a Universal path to the base directory where this unit loads its canonical files from.
|
||||||
|
*/
|
||||||
public get path(): UniversalPath {
|
public get path(): UniversalPath {
|
||||||
return this.app().appPath(...this.appPath)
|
return this.app().appPath(...this.appPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get the plural name of the canonical items provided by this unit. */
|
||||||
public get canonicalItems() {
|
public get canonicalItems() {
|
||||||
return `${this.canonicalItem}s`
|
return `${this.canonicalItem}s`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get a canonical item by key. */
|
||||||
public get(key: string): T | undefined {
|
public get(key: string): T | undefined {
|
||||||
if ( key.startsWith('@') ) {
|
if ( key.startsWith('@') ) {
|
||||||
const [namespace, ...rest] = key.split(':')
|
const [namespace, ...rest] = key.split(':')
|
||||||
@ -112,6 +139,34 @@ export abstract class Canonical<T> extends Unit {
|
|||||||
return this.loadedItems[key]
|
return this.loadedItems[key]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a namespace resolver with the canonical unit.
|
||||||
|
*
|
||||||
|
* Namespaces are canonical names that start with a particular key, beginning with the `@` character,
|
||||||
|
* which resolve their resources using a resolver function.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const items = {
|
||||||
|
* 'foo:bar': 123,
|
||||||
|
* 'bob': 456,
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* const resolver = (key: string) => items[key]
|
||||||
|
*
|
||||||
|
* canonical.registerNamespace('@mynamespace', resolver)
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Now, the items in the `@mynamespace` namespace can be accessed like so:
|
||||||
|
*
|
||||||
|
* ```typescript
|
||||||
|
* canonical.get('@mynamespace:foo:bar') // => 123
|
||||||
|
* canonical.get('@mynamespace:bob') // => 456
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param name
|
||||||
|
* @param resolver
|
||||||
|
*/
|
||||||
public registerNamespace(name: string, resolver: CanonicalResolver<T>) {
|
public registerNamespace(name: string, resolver: CanonicalResolver<T>) {
|
||||||
if ( !name.startsWith('@') ) {
|
if ( !name.startsWith('@') ) {
|
||||||
throw new ErrorWithContext(`Canonical namespaces must start with @.`, { name })
|
throw new ErrorWithContext(`Canonical namespaces must start with @.`, { name })
|
||||||
@ -139,10 +194,20 @@ export abstract class Canonical<T> extends Unit {
|
|||||||
this.canon.registerCanonical(this)
|
this.canon.registerCanonical(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called for each canonical item loaded from a file. This function should do any setup necessary and return the item
|
||||||
|
* that should be associated with the canonical name.
|
||||||
|
* @param definition
|
||||||
|
*/
|
||||||
public async initCanonicalItem(definition: CanonicalDefinition): Promise<T> {
|
public async initCanonicalItem(definition: CanonicalDefinition): Promise<T> {
|
||||||
return definition.imported.default ?? definition.imported[definition.canonicalName.split(':').reverse()[0]]
|
return definition.imported.default ?? definition.imported[definition.canonicalName.split(':').reverse()[0]]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given the path to a file in the canonical items directory, create a CanonicalDefinition record from that file.
|
||||||
|
* @param filePath
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
protected async buildCanonicalDefinition(filePath: string): Promise<CanonicalDefinition> {
|
protected async buildCanonicalDefinition(filePath: string): Promise<CanonicalDefinition> {
|
||||||
const originalName = filePath.replace(this.path.toLocal, '').substr(1)
|
const originalName = filePath.replace(this.path.toLocal, '').substr(1)
|
||||||
const pathRegex = new RegExp(nodePath.sep, 'g')
|
const pathRegex = new RegExp(nodePath.sep, 'g')
|
||||||
|
@ -5,12 +5,18 @@
|
|||||||
import {Canonical, CanonicalDefinition} from "./Canonical";
|
import {Canonical, CanonicalDefinition} from "./Canonical";
|
||||||
import {Instantiable, isInstantiable} from "@extollo/di";
|
import {Instantiable, isInstantiable} from "@extollo/di";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error thrown when the export of a canonical file is determined to be invalid.
|
||||||
|
*/
|
||||||
export class InvalidCanonicalExportError extends Error {
|
export class InvalidCanonicalExportError extends Error {
|
||||||
constructor(name: string) {
|
constructor(name: string) {
|
||||||
super(`Unable to import canonical item from "${name}". The default export of this file is invalid.`)
|
super(`Unable to import canonical item from "${name}". The default export of this file is invalid.`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Variant of the Canonical unit whose files export classes which are instantiated using the global container.
|
||||||
|
*/
|
||||||
export class CanonicalInstantiable<T> extends Canonical<Instantiable<T>> {
|
export class CanonicalInstantiable<T> extends Canonical<Instantiable<T>> {
|
||||||
public async initCanonicalItem(definition: CanonicalDefinition): Promise<Instantiable<T>> {
|
public async initCanonicalItem(definition: CanonicalDefinition): Promise<Instantiable<T>> {
|
||||||
if ( isInstantiable(definition.imported.default) ) {
|
if ( isInstantiable(definition.imported.default) ) {
|
||||||
@ -23,4 +29,4 @@ export class CanonicalInstantiable<T> extends Canonical<Instantiable<T>> {
|
|||||||
|
|
||||||
throw new InvalidCanonicalExportError(definition.originalName)
|
throw new InvalidCanonicalExportError(definition.originalName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,27 @@
|
|||||||
import {Canonical} from "./Canonical";
|
import {Canonical} from "./Canonical";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Variant of the Canonical unit whose accessor allows accessing nested
|
||||||
|
* properties on the resolved objects.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* The Config unit is a CanonicalRecursive unit. So, once a config file is
|
||||||
|
* resolved, a particular value in the config file can be retrieved as well:
|
||||||
|
*
|
||||||
|
* ```typescript
|
||||||
|
* // app/config/my/config.config.ts
|
||||||
|
* {
|
||||||
|
* foo: {
|
||||||
|
* bar: 123
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* This can be accessed as:
|
||||||
|
* ```typescript
|
||||||
|
* config.get('my:config.foo.bar') // => 123
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
export class CanonicalRecursive extends Canonical<any> {
|
export class CanonicalRecursive extends Canonical<any> {
|
||||||
public get(key: string, fallback?: any): any | undefined {
|
public get(key: string, fallback?: any): any | undefined {
|
||||||
const parts = key.split('.')
|
const parts = key.split('.')
|
||||||
|
@ -2,6 +2,14 @@ import {Canonical, CanonicalDefinition} from "./Canonical";
|
|||||||
import {isStaticClass, StaticClass} from "@extollo/di";
|
import {isStaticClass, StaticClass} from "@extollo/di";
|
||||||
import {InvalidCanonicalExportError} from "./CanonicalInstantiable";
|
import {InvalidCanonicalExportError} from "./CanonicalInstantiable";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Variant of the Canonical unit whose files export static classes, and these static classes
|
||||||
|
* are the exports of the class.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* The Controllers class is CanonicalStatic. The various `.controller.ts` files export static
|
||||||
|
* Controller classes, so the canonical items managed by the Controllers service are `Instantiable<Controller>`.
|
||||||
|
*/
|
||||||
export class CanonicalStatic<T, T2> extends Canonical<StaticClass<T, T2>> {
|
export class CanonicalStatic<T, T2> extends Canonical<StaticClass<T, T2>> {
|
||||||
public async initCanonicalItem(definition: CanonicalDefinition): Promise<StaticClass<T, T2>> {
|
public async initCanonicalItem(definition: CanonicalDefinition): Promise<StaticClass<T, T2>> {
|
||||||
if ( isStaticClass(definition.imported.default) ) {
|
if ( isStaticClass(definition.imported.default) ) {
|
||||||
|
@ -2,6 +2,9 @@ import {Singleton, Inject} from "@extollo/di";
|
|||||||
import {CanonicalRecursive} from "./CanonicalRecursive";
|
import {CanonicalRecursive} from "./CanonicalRecursive";
|
||||||
import {Logging} from "./Logging";
|
import {Logging} from "./Logging";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Canonical unit that loads configuration files from `app/configs`.
|
||||||
|
*/
|
||||||
@Singleton()
|
@Singleton()
|
||||||
export class Config extends CanonicalRecursive {
|
export class Config extends CanonicalRecursive {
|
||||||
@Inject()
|
@Inject()
|
||||||
@ -10,7 +13,11 @@ export class Config extends CanonicalRecursive {
|
|||||||
protected appPath: string[] = ['configs']
|
protected appPath: string[] = ['configs']
|
||||||
protected suffix: string = '.config.js'
|
protected suffix: string = '.config.js'
|
||||||
protected canonicalItem: string = 'config'
|
protected canonicalItem: string = 'config'
|
||||||
|
|
||||||
|
/** If true, all the unique configuration keys will be stored for debugging. */
|
||||||
protected recordConfigAccesses: boolean = false
|
protected recordConfigAccesses: boolean = false
|
||||||
|
|
||||||
|
/** Array of all unique accessed config keys, if `recordConfigAccesses` is true. */
|
||||||
protected accessedKeys: string[] = []
|
protected accessedKeys: string[] = []
|
||||||
|
|
||||||
public async up() {
|
public async up() {
|
||||||
|
@ -3,6 +3,9 @@ import {Singleton, Instantiable} from "@extollo/di";
|
|||||||
import {Controller} from "../http/Controller";
|
import {Controller} from "../http/Controller";
|
||||||
import {CanonicalDefinition} from "./Canonical";
|
import {CanonicalDefinition} from "./Canonical";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A canonical unit that loads the controller classes from `app/http/controllers`.
|
||||||
|
*/
|
||||||
@Singleton()
|
@Singleton()
|
||||||
export class Controllers extends CanonicalStatic<Instantiable<Controller>, Controller> {
|
export class Controllers extends CanonicalStatic<Instantiable<Controller>, Controller> {
|
||||||
protected appPath = ['http', 'controllers']
|
protected appPath = ['http', 'controllers']
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
import {Canonical} from "./Canonical";
|
import {Canonical} from "./Canonical";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Canonical class used for faking canonical units. Here, the canonical resolver
|
||||||
|
* is registered with the global service, but no files are loaded from the filesystem.
|
||||||
|
*/
|
||||||
export class FakeCanonical<T> extends Canonical<T> {
|
export class FakeCanonical<T> extends Canonical<T> {
|
||||||
public async up() {
|
public async up() {
|
||||||
this.canon.registerCanonical(this)
|
this.canon.registerCanonical(this)
|
||||||
|
@ -15,6 +15,10 @@ import {error} from "../http/response/ErrorResponseFactory";
|
|||||||
import {ExecuteResolvedRoutePreflightHTTPModule} from "../http/kernel/module/ExecuteResolvedRoutePreflightHTTPModule";
|
import {ExecuteResolvedRoutePreflightHTTPModule} from "../http/kernel/module/ExecuteResolvedRoutePreflightHTTPModule";
|
||||||
import {ExecuteResolvedRoutePostflightHTTPModule} from "../http/kernel/module/ExecuteResolvedRoutePostflightHTTPModule";
|
import {ExecuteResolvedRoutePostflightHTTPModule} from "../http/kernel/module/ExecuteResolvedRoutePostflightHTTPModule";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Application unit that starts the HTTP/S server, creates Request and Response objects
|
||||||
|
* for it, and handles those requests using the HTTPKernel.
|
||||||
|
*/
|
||||||
@Singleton()
|
@Singleton()
|
||||||
export class HTTPServer extends Unit {
|
export class HTTPServer extends Unit {
|
||||||
@Inject()
|
@Inject()
|
||||||
@ -23,6 +27,7 @@ export class HTTPServer extends Unit {
|
|||||||
@Inject()
|
@Inject()
|
||||||
protected readonly kernel!: HTTPKernel
|
protected readonly kernel!: HTTPKernel
|
||||||
|
|
||||||
|
/** The underlying native Node.js server. */
|
||||||
protected server?: Server
|
protected server?: Server
|
||||||
|
|
||||||
public async up() {
|
public async up() {
|
||||||
|
@ -1,53 +1,120 @@
|
|||||||
import {Logger, LoggingLevel, LogMessage} from "@extollo/util";
|
import {Logger, LoggingLevel, LogMessage} from "@extollo/util";
|
||||||
import {Singleton} from "@extollo/di";
|
import {Singleton} from "@extollo/di";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A singleton service that manages loggers registered in the application, and
|
||||||
|
* can be used to log output to all of them based on the configured logging level.
|
||||||
|
*
|
||||||
|
* This should be used in place of `console.log` as it also supports logging to
|
||||||
|
* external locations.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* logging.info('Info level!')
|
||||||
|
* logging.debug('Some debugging information...')
|
||||||
|
* logging.warn('A warning!', true) // true, to force it to show, regardless of logging level.
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
@Singleton()
|
@Singleton()
|
||||||
export class Logging {
|
export class Logging {
|
||||||
|
/** Array of Logger implementations that should be logged to. */
|
||||||
protected registeredLoggers: Logger[] = []
|
protected registeredLoggers: Logger[] = []
|
||||||
|
|
||||||
|
/** The currently configured logging level. */
|
||||||
protected currentLevel: LoggingLevel = LoggingLevel.Warning
|
protected currentLevel: LoggingLevel = LoggingLevel.Warning
|
||||||
|
|
||||||
|
/** Register a Logger implementation with this service. */
|
||||||
public registerLogger(logger: Logger) {
|
public registerLogger(logger: Logger) {
|
||||||
if ( !this.registeredLoggers.includes(logger) ) {
|
if ( !this.registeredLoggers.includes(logger) ) {
|
||||||
this.registeredLoggers.push(logger)
|
this.registeredLoggers.push(logger)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a Logger implementation from this service, if it is registered.
|
||||||
|
* @param logger
|
||||||
|
*/
|
||||||
public unregisterLogger(logger: Logger) {
|
public unregisterLogger(logger: Logger) {
|
||||||
this.registeredLoggers = this.registeredLoggers.filter(x => x !== logger)
|
this.registeredLoggers = this.registeredLoggers.filter(x => x !== logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current logging level.
|
||||||
|
*/
|
||||||
public get level(): LoggingLevel {
|
public get level(): LoggingLevel {
|
||||||
return this.currentLevel
|
return this.currentLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the current logging level.
|
||||||
|
* @param level
|
||||||
|
*/
|
||||||
public set level(level: LoggingLevel) {
|
public set level(level: LoggingLevel) {
|
||||||
this.currentLevel = level
|
this.currentLevel = level
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write a success-level output to the logs.
|
||||||
|
* @param output
|
||||||
|
* @param force - if true, output even if outside the current logging level
|
||||||
|
*/
|
||||||
public success(output: any, force = false) {
|
public success(output: any, force = false) {
|
||||||
this.writeLog(LoggingLevel.Success, output, force)
|
this.writeLog(LoggingLevel.Success, output, force)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write an error-level output to the logs.
|
||||||
|
* @param output
|
||||||
|
* @param force - if true, output even if outside the current logging level
|
||||||
|
*/
|
||||||
public error(output: any, force = false) {
|
public error(output: any, force = false) {
|
||||||
this.writeLog(LoggingLevel.Error, output, force)
|
this.writeLog(LoggingLevel.Error, output, force)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write a warning-level output to the logs.
|
||||||
|
* @param output
|
||||||
|
* @param force - if true, output even if outside the current logging level
|
||||||
|
*/
|
||||||
public warn(output: any, force = false) {
|
public warn(output: any, force = false) {
|
||||||
this.writeLog(LoggingLevel.Warning, output, force)
|
this.writeLog(LoggingLevel.Warning, output, force)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write an info-level output to the logs.
|
||||||
|
* @param output
|
||||||
|
* @param force - if true, output even if outside the current logging level
|
||||||
|
*/
|
||||||
public info(output: any, force = false) {
|
public info(output: any, force = false) {
|
||||||
this.writeLog(LoggingLevel.Info, output, force)
|
this.writeLog(LoggingLevel.Info, output, force)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write a debugging-level output to the logs.
|
||||||
|
* @param output
|
||||||
|
* @param force - if true, output even if outside the current logging level
|
||||||
|
*/
|
||||||
public debug(output: any, force = false) {
|
public debug(output: any, force = false) {
|
||||||
this.writeLog(LoggingLevel.Debug, output, force)
|
this.writeLog(LoggingLevel.Debug, output, force)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write a verbose-level output to the logs.
|
||||||
|
* @param output
|
||||||
|
* @param force - if true, output even if outside the current logging level
|
||||||
|
*/
|
||||||
public verbose(output: any, force = false) {
|
public verbose(output: any, force = false) {
|
||||||
this.writeLog(LoggingLevel.Verbose, output, force)
|
this.writeLog(LoggingLevel.Verbose, output, force)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to write the given output, at the given logging level, to
|
||||||
|
* all of the registered loggers.
|
||||||
|
* @param level
|
||||||
|
* @param output
|
||||||
|
* @param force - if true, output even if outside the current logging level
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
protected writeLog(level: LoggingLevel, output: any, force = false) {
|
protected writeLog(level: LoggingLevel, output: any, force = false) {
|
||||||
const message = this.buildMessage(level, output)
|
const message = this.buildMessage(level, output)
|
||||||
if ( this.currentLevel >= level || force ) {
|
if ( this.currentLevel >= level || force ) {
|
||||||
@ -61,6 +128,12 @@ export class Logging {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a level and output item, build a formatted LogMessage with date and caller.
|
||||||
|
* @param level
|
||||||
|
* @param output
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
protected buildMessage(level: LoggingLevel, output: any): LogMessage {
|
protected buildMessage(level: LoggingLevel, output: any): LogMessage {
|
||||||
return {
|
return {
|
||||||
level,
|
level,
|
||||||
@ -70,6 +143,11 @@ export class Logging {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the name of the object that called the log method using error traces.
|
||||||
|
* @param level
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
protected getCallerInfo(level = 5): string {
|
protected getCallerInfo(level = 5): string {
|
||||||
const e = new Error()
|
const e = new Error()
|
||||||
if ( !e.stack ) return 'Unknown'
|
if ( !e.stack ) return 'Unknown'
|
||||||
|
@ -3,6 +3,9 @@ import {Singleton, Instantiable} from "@extollo/di";
|
|||||||
import {CanonicalDefinition} from "./Canonical";
|
import {CanonicalDefinition} from "./Canonical";
|
||||||
import {Middleware} from "../http/routing/Middleware";
|
import {Middleware} from "../http/routing/Middleware";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A canonical unit that loads the middleware classes from `app/http/middlewares`.
|
||||||
|
*/
|
||||||
@Singleton()
|
@Singleton()
|
||||||
export class Middlewares extends CanonicalStatic<Instantiable<Middleware>, Middleware> {
|
export class Middlewares extends CanonicalStatic<Instantiable<Middleware>, Middleware> {
|
||||||
protected appPath = ['http', 'middlewares']
|
protected appPath = ['http', 'middlewares']
|
||||||
|
@ -6,6 +6,9 @@ import {Route} from "../http/routing/Route";
|
|||||||
import {HTTPMethod} from "../http/lifecycle/Request";
|
import {HTTPMethod} from "../http/lifecycle/Request";
|
||||||
import {ViewEngineFactory} from "../views/ViewEngineFactory";
|
import {ViewEngineFactory} from "../views/ViewEngineFactory";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Application unit that loads the various route files from `app/http/routes` and pre-compiles the route handlers.
|
||||||
|
*/
|
||||||
@Singleton()
|
@Singleton()
|
||||||
export class Routing extends Unit {
|
export class Routing extends Unit {
|
||||||
@Inject()
|
@Inject()
|
||||||
@ -35,12 +38,21 @@ export class Routing extends Unit {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an HTTPMethod and route path, return the Route instance that matches them,
|
||||||
|
* if one exists.
|
||||||
|
* @param method
|
||||||
|
* @param path
|
||||||
|
*/
|
||||||
public match(method: HTTPMethod, path: string): Route | undefined {
|
public match(method: HTTPMethod, path: string): Route | undefined {
|
||||||
return this.compiledRoutes.firstWhere(route => {
|
return this.compiledRoutes.firstWhere(route => {
|
||||||
return route.match(method, path)
|
return route.match(method, path)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the universal path to the root directory of the route definitions.
|
||||||
|
*/
|
||||||
public get path(): UniversalPath {
|
public get path(): UniversalPath {
|
||||||
return this.app().appPath('http', 'routes')
|
return this.app().appPath('http', 'routes')
|
||||||
}
|
}
|
||||||
|
9
src/support/cache/CacheFactory.ts
vendored
9
src/support/cache/CacheFactory.ts
vendored
@ -13,10 +13,15 @@ import {Config} from "../../service/Config";
|
|||||||
import {Cache} from "./Cache"
|
import {Cache} from "./Cache"
|
||||||
import {MemoryCache} from "./MemoryCache";
|
import {MemoryCache} from "./MemoryCache";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dependency container factory that matches the abstract Cache token, but
|
||||||
|
* produces an instance of whatever Cache driver is configured in the `server.cache.driver` config.
|
||||||
|
*/
|
||||||
export class CacheFactory extends AbstractFactory {
|
export class CacheFactory extends AbstractFactory {
|
||||||
protected readonly logging: Logging
|
protected readonly logging: Logging
|
||||||
protected readonly config: Config
|
protected readonly config: Config
|
||||||
|
|
||||||
|
/** true if we have printed the memory-based cache driver warning once. */
|
||||||
private static loggedMemoryCacheWarningOnce = false
|
private static loggedMemoryCacheWarningOnce = false
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -52,6 +57,10 @@ export class CacheFactory extends AbstractFactory {
|
|||||||
return meta
|
return meta
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the configured cache driver and return some Instantiable<Cache>.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
protected getCacheClass() {
|
protected getCacheClass() {
|
||||||
const CacheClass = this.config.get('server.cache.driver', MemoryCache)
|
const CacheClass = this.config.get('server.cache.driver', MemoryCache)
|
||||||
if ( CacheClass === MemoryCache && !CacheFactory.loggedMemoryCacheWarningOnce ) {
|
if ( CacheClass === MemoryCache && !CacheFactory.loggedMemoryCacheWarningOnce ) {
|
||||||
|
5
src/support/cache/MemoryCache.ts
vendored
5
src/support/cache/MemoryCache.ts
vendored
@ -1,7 +1,12 @@
|
|||||||
import {Collection} from "@extollo/util"
|
import {Collection} from "@extollo/util"
|
||||||
import {Cache} from "./Cache"
|
import {Cache} from "./Cache"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An in-memory implementation of the Cache.
|
||||||
|
* This is the default implementation for compatibility, but applications should switch to a persistent-backed cache driver.
|
||||||
|
*/
|
||||||
export class MemoryCache extends Cache {
|
export class MemoryCache extends Cache {
|
||||||
|
/** Static collection of in-memory cache items. */
|
||||||
private static cacheItems: Collection<{key: string, value: string, expires?: Date}> = new Collection<{key: string; value: string, expires?: Date}>()
|
private static cacheItems: Collection<{key: string, value: string, expires?: Date}> = new Collection<{key: string; value: string, expires?: Date}>()
|
||||||
|
|
||||||
public fetch(key: string): string | Promise<string | undefined> | undefined {
|
public fetch(key: string): string | Promise<string | undefined> | undefined {
|
||||||
|
@ -2,8 +2,12 @@ import {ViewEngine} from "./ViewEngine"
|
|||||||
import {Injectable} from "@extollo/di"
|
import {Injectable} from "@extollo/di"
|
||||||
import * as pug from "pug"
|
import * as pug from "pug"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of the ViewEngine class that renders Pug/Jade templates.
|
||||||
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PugViewEngine extends ViewEngine {
|
export class PugViewEngine extends ViewEngine {
|
||||||
|
/** A cache of compiled templates. */
|
||||||
protected compileCache: {[key: string]: ((locals?: pug.LocalsObject) => string)} = {}
|
protected compileCache: {[key: string]: ((locals?: pug.LocalsObject) => string)} = {}
|
||||||
|
|
||||||
public renderString(templateString: string, locals: { [p: string]: any }): string | Promise<string> {
|
public renderString(templateString: string, locals: { [p: string]: any }): string | Promise<string> {
|
||||||
@ -22,6 +26,10 @@ export class PugViewEngine extends ViewEngine {
|
|||||||
return compiled(locals)
|
return compiled(locals)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the object of options passed to Pug's compile methods.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
protected getOptions() {
|
protected getOptions() {
|
||||||
return {
|
return {
|
||||||
basedir: this.path.toLocal,
|
basedir: this.path.toLocal,
|
||||||
|
@ -3,6 +3,9 @@ import {Config} from "../service/Config"
|
|||||||
import {Container} from "@extollo/di"
|
import {Container} from "@extollo/di"
|
||||||
import {UniversalPath} from "@extollo/util"
|
import {UniversalPath} from "@extollo/util"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract base class for rendering views via different view engines.
|
||||||
|
*/
|
||||||
export abstract class ViewEngine extends AppClass {
|
export abstract class ViewEngine extends AppClass {
|
||||||
protected readonly config: Config
|
protected readonly config: Config
|
||||||
protected readonly debug: boolean
|
protected readonly debug: boolean
|
||||||
@ -14,10 +17,24 @@ export abstract class ViewEngine extends AppClass {
|
|||||||
|| this.config.get('server.debug', false))
|
|| this.config.get('server.debug', false))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the UniversalPath to the base directory where views are loaded from.
|
||||||
|
*/
|
||||||
public get path(): UniversalPath {
|
public get path(): UniversalPath {
|
||||||
return this.app().appPath(...['resources', 'views']) // FIXME allow configuring
|
return this.app().appPath(...['resources', 'views']) // FIXME allow configuring
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a template string and a set of variables for the view, render the string to HTML and return it.
|
||||||
|
* @param templateString
|
||||||
|
* @param locals
|
||||||
|
*/
|
||||||
public abstract renderString(templateString: string, locals: {[key: string]: any}): string | Promise<string>
|
public abstract renderString(templateString: string, locals: {[key: string]: any}): string | Promise<string>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given the canonical name of a template file, render the file using the provided variables.
|
||||||
|
* @param templateName
|
||||||
|
* @param locals
|
||||||
|
*/
|
||||||
public abstract renderByName(templateName: string, locals: {[key: string]: any}): string | Promise<string>
|
public abstract renderByName(templateName: string, locals: {[key: string]: any}): string | Promise<string>
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,10 @@ import {Config} from "../service/Config";
|
|||||||
import {ViewEngine} from "./ViewEngine";
|
import {ViewEngine} from "./ViewEngine";
|
||||||
import {PugViewEngine} from "./PugViewEngine";
|
import {PugViewEngine} from "./PugViewEngine";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dependency factory whose token matches the abstract ViewEngine class, but produces
|
||||||
|
* a particular ViewEngine implementation based on the configuration.
|
||||||
|
*/
|
||||||
export class ViewEngineFactory extends AbstractFactory {
|
export class ViewEngineFactory extends AbstractFactory {
|
||||||
protected readonly logging: Logging
|
protected readonly logging: Logging
|
||||||
protected readonly config: Config
|
protected readonly config: Config
|
||||||
@ -50,6 +54,10 @@ export class ViewEngineFactory extends AbstractFactory {
|
|||||||
return meta
|
return meta
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Using the config, get the implementation of the ViewEngine that should be used in the application.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
protected getViewEngineClass() {
|
protected getViewEngineClass() {
|
||||||
const ViewEngineClass = this.config.get('server.view_engine.driver', PugViewEngine)
|
const ViewEngineClass = this.config.get('server.view_engine.driver', PugViewEngine)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user