TypeDoc all the thngs
This commit is contained in:
@@ -7,44 +7,88 @@ import * as url from "url";
|
||||
import {Response} from "./Response";
|
||||
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';
|
||||
|
||||
/**
|
||||
* Returns true if the given item is a valid HTTP verb.
|
||||
* @param what
|
||||
*/
|
||||
export function isHTTPMethod(what: any): what is HTTPMethod {
|
||||
return ['post', 'get', 'patch', 'put', 'delete'].includes(what)
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface that describes the HTTP protocol version.
|
||||
*/
|
||||
export interface HTTPProtocol {
|
||||
string: string,
|
||||
major: number,
|
||||
minor: number,
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface that describes the origin IP address of a request.
|
||||
*/
|
||||
export interface HTTPSourceAddress {
|
||||
address: string;
|
||||
family: 'IPv4' | 'IPv6';
|
||||
port: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A class that represents an HTTP request from a client.
|
||||
*/
|
||||
@Injectable()
|
||||
export class Request extends ScopedContainer {
|
||||
|
||||
/** The cookie manager for the request. */
|
||||
public readonly cookies: HTTPCookieJar;
|
||||
|
||||
/** The URL suffix of the request. */
|
||||
public readonly url: string;
|
||||
|
||||
/** The fully-qualified URL of the request. */
|
||||
public readonly fullUrl: string;
|
||||
|
||||
/** The HTTP verb of the request. */
|
||||
public readonly method: HTTPMethod;
|
||||
|
||||
/** True if the request was made via TLS. */
|
||||
public readonly secure: boolean;
|
||||
|
||||
/** The request HTTP protocol version. */
|
||||
public readonly protocol: HTTPProtocol;
|
||||
|
||||
/** The URL path, stripped of query params. */
|
||||
public readonly path: string;
|
||||
|
||||
/** The raw parsed query data from the request. */
|
||||
public readonly rawQueryData: {[key: string]: string | string[] | undefined};
|
||||
|
||||
/** The inferred query data. */
|
||||
public readonly query: {[key: string]: any};
|
||||
|
||||
/** True if the request was made via XMLHttpRequest. */
|
||||
public readonly isXHR: boolean;
|
||||
|
||||
/** The origin IP address of the request. */
|
||||
public readonly address: HTTPSourceAddress;
|
||||
|
||||
/** The associated response. */
|
||||
public readonly response: Response;
|
||||
|
||||
/** The media types accepted by the client. */
|
||||
public readonly mediaTypes: string[];
|
||||
|
||||
constructor(
|
||||
/** The native Node.js request. */
|
||||
protected clientRequest: IncomingMessage,
|
||||
|
||||
/** The native Node.js response. */
|
||||
protected serverResponse: ServerResponse,
|
||||
) {
|
||||
super(Container.getContainer())
|
||||
@@ -103,20 +147,30 @@ export class Request extends ScopedContainer {
|
||||
this.response = new Response(this, serverResponse)
|
||||
}
|
||||
|
||||
/** Get the value of a header, if it exists. */
|
||||
public getHeader(name: string) {
|
||||
return this.clientRequest.headers[name.toLowerCase()]
|
||||
}
|
||||
|
||||
/** Get the native Node.js IncomingMessage object. */
|
||||
public toNative() {
|
||||
return this.clientRequest
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of an input field on the request. Spans multiple input sources.
|
||||
* @param key
|
||||
*/
|
||||
public input(key: string) {
|
||||
if ( key in this.query ) {
|
||||
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) {
|
||||
if ( type === 'json' ) type = 'application/json'
|
||||
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()))
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the short form of the content type the client has requested.
|
||||
*/
|
||||
wants(): 'html' | 'json' | 'xml' | 'unknown' {
|
||||
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('*/*')
|
||||
|
||||
@@ -2,6 +2,9 @@ import {Request} from "./Request";
|
||||
import {ErrorWithContext, HTTPStatus, BehaviorSubject} from "@extollo/util"
|
||||
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 {
|
||||
constructor(response: Response, headerName?: string) {
|
||||
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 {
|
||||
constructor(response: Response) {
|
||||
super(`Cannot modify or re-send response as it has already ended.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A class representing an HTTP response to a client.
|
||||
*/
|
||||
export class Response {
|
||||
/** Mapping of headers that should be sent back to the client. */
|
||||
private headers: {[key: string]: string | string[]} = {}
|
||||
|
||||
/** True if the headers have been sent. */
|
||||
private _sentHeaders: boolean = false
|
||||
|
||||
/** True if the response has been sent and closed. */
|
||||
private _responseEnded: boolean = false
|
||||
|
||||
/** The HTTP status code that should be sent to the client. */
|
||||
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
|
||||
|
||||
/** The body contents that should be written to the response. */
|
||||
public body: string = ''
|
||||
|
||||
/**
|
||||
* Behavior subject fired right before the response content is written.
|
||||
*/
|
||||
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>()
|
||||
|
||||
constructor(
|
||||
/** The associated request object. */
|
||||
public readonly request: Request,
|
||||
|
||||
/** The native Node.js ServerResponse. */
|
||||
protected readonly serverResponse: ServerResponse,
|
||||
) { }
|
||||
|
||||
/** Get the currently set response status. */
|
||||
public getStatus() {
|
||||
return this._status
|
||||
}
|
||||
|
||||
/** Set a new response status. */
|
||||
public setStatus(status: HTTPStatus) {
|
||||
if ( this._sentHeaders ) throw new HeadersAlreadySentError(this, 'status')
|
||||
this._status = status
|
||||
}
|
||||
|
||||
/** Get the HTTPCookieJar for the client. */
|
||||
public get cookies() {
|
||||
return this.request.cookies
|
||||
}
|
||||
|
||||
/** Get the value of the response header, if it exists. */
|
||||
public getHeader(name: string): string | string[] | undefined {
|
||||
return this.headers[name]
|
||||
}
|
||||
|
||||
/** Set the value of the response header. */
|
||||
public setHeader(name: string, value: string | string[]) {
|
||||
if ( this._sentHeaders ) throw new HeadersAlreadySentError(this, name)
|
||||
this.headers[name] = value
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk set the specified headers in the response.
|
||||
* @param data
|
||||
*/
|
||||
public setHeaders(data: {[name: string]: string | string[]}) {
|
||||
if ( this._sentHeaders ) throw new HeadersAlreadySentError(this)
|
||||
this.headers = {...this.headers, ...data}
|
||||
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[]) {
|
||||
if ( this._sentHeaders ) throw new HeadersAlreadySentError(this, name)
|
||||
if ( !Array.isArray(value) ) value = [value]
|
||||
@@ -70,6 +120,9 @@ export class Response {
|
||||
this.headers[name] = existing
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the headers to the client.
|
||||
*/
|
||||
public sendHeaders() {
|
||||
const headers = {} as any
|
||||
|
||||
@@ -85,14 +138,20 @@ export class Response {
|
||||
this._sentHeaders = true
|
||||
}
|
||||
|
||||
/** Returns true if the headers have been sent. */
|
||||
public hasSentHeaders() {
|
||||
return this._sentHeaders
|
||||
}
|
||||
|
||||
/** Returns true if a body has been set in the response. */
|
||||
public hasBody() {
|
||||
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) {
|
||||
if ( typeof set !== 'undefined' ) {
|
||||
this._blockingWriteback = set
|
||||
@@ -101,6 +160,10 @@ export class Response {
|
||||
return this._blockingWriteback
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the headers and specified data to the client.
|
||||
* @param data
|
||||
*/
|
||||
public async write(data: any) {
|
||||
return new Promise<void>((res, rej) => {
|
||||
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() {
|
||||
await this.sending$.next(this)
|
||||
this.setHeader('Content-Length', String(this.body?.length ?? 0))
|
||||
@@ -119,6 +185,9 @@ export class Response {
|
||||
await this.sent$.next(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the response as ended and close the socket.
|
||||
*/
|
||||
public end() {
|
||||
if ( this._responseEnded ) throw new ResponseAlreadySentError(this)
|
||||
this._sentHeaders = true
|
||||
|
||||
Reference in New Issue
Block a user