TypeDoc all the thngs

This commit is contained in:
2021-03-25 08:50:13 -05:00
parent 7cb0546b01
commit fad1184afe
52 changed files with 976 additions and 3 deletions

View File

@@ -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('*/*')

View File

@@ -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