Add basic response factories and helpers

This commit is contained in:
2021-03-08 10:07:10 -06:00
parent 3acc1bc83e
commit a9ffa771dc
15 changed files with 365 additions and 10 deletions

View File

@@ -0,0 +1,20 @@
import {ResponseFactory} from "./ResponseFactory"
import {Rehydratable} from "@extollo/util"
import {Request} from "../lifecycle/Request";
export function dehydrate(value: Rehydratable): DehydratedStateResponseFactory {
return new DehydratedStateResponseFactory(value)
}
export class DehydratedStateResponseFactory extends ResponseFactory {
constructor(
public readonly rehydratable: Rehydratable
) { super() }
public async write(request: Request) {
request = await super.write(request)
request.response.body = JSON.stringify(this.rehydratable.dehydrate())
request.response.setHeader('Content-Type', 'application/json')
return request
}
}

View File

@@ -0,0 +1,90 @@
import {ResponseFactory} from "./ResponseFactory"
import {ErrorWithContext, HTTPStatus} from "@extollo/util"
import {Request} from "../lifecycle/Request";
import * as api from "./api"
export function error(
error: Error | string,
status: HTTPStatus = HTTPStatus.INTERNAL_SERVER_ERROR,
output: 'json' | 'html' | 'auto' = 'auto'
): ErrorResponseFactory {
if ( typeof error === 'string' ) error = new Error(error)
return new ErrorResponseFactory(error, status, output)
}
export class ErrorResponseFactory extends ResponseFactory {
protected targetMode: 'json' | 'html' | 'auto' = 'auto'
constructor(
public readonly error: Error,
status: HTTPStatus,
output: 'json' | 'html' | 'auto' = 'auto'
) {
super()
this.status(status)
this.mode(output)
}
public mode(output: 'json' | 'html' | 'auto'): ErrorResponseFactory {
this.targetMode = output
return this
}
public async write(request: Request) {
request = await super.write(request)
const wants = request.wants()
if ( this.targetMode === 'json' || (this.targetMode === 'auto' && wants === 'json') ) {
request.response.setHeader('Content-Type', 'application/json')
request.response.body = this.buildJSON(this.error)
} else if ( this.targetMode === 'html' || (this.targetMode === 'auto' && (wants === 'html' || wants === 'unknown')) ) {
request.response.setHeader('Content-Type', 'text/html')
request.response.body = this.buildHTML(this.error)
}
// FIXME XML support
return request
}
/**
* Build the HTML display for the given error.
* @param {Error} error
* @return string
*/
protected buildHTML(error: Error) {
let context: any
if ( error instanceof ErrorWithContext ) {
context = error.context
if ( error.originalError ) {
error = error.originalError
}
}
let str = `
<b>Sorry, an unexpected error occurred while processing your request.</b>
<br>
<pre><code>
Name: ${error.name}
Message: ${error.message}
Stack trace:
- ${error.stack ? error.stack.split(/\s+at\s+/).slice(1).join('<br> - ') : 'none'}
</code></pre>
`
if ( context && typeof context === 'object' ) {
str += `
<pre><code>
Context:
${Object.keys(context).map(key => ` - ${key} : ${context[key]}`)}
</code></pre>
`
}
return str
}
protected buildJSON(error: Error) {
return JSON.stringify(api.error(error))
}
}

View File

@@ -0,0 +1,19 @@
import {ResponseFactory} from "./ResponseFactory";
import {Request} from "../lifecycle/Request";
export function html(value: string): HTMLResponseFactory {
return new HTMLResponseFactory(value)
}
export class HTMLResponseFactory extends ResponseFactory {
constructor(
public readonly value: string,
) { super() }
public async write(request: Request) {
request = await super.write(request)
request.response.setHeader('Content-Type', 'text/html; charset=utf-8')
request.response.body = this.value
return request
}
}

View File

@@ -0,0 +1,16 @@
import {ErrorResponseFactory} from "./ErrorResponseFactory";
import {HTTPError} from "../HTTPError";
import {HTTPStatus} from "@extollo/util"
export function http(status: HTTPStatus, message?: string, output: 'json' | 'html' | 'auto' = 'auto'): HTTPErrorResponseFactory {
return new HTTPErrorResponseFactory(new HTTPError(status, message), output)
}
export class HTTPErrorResponseFactory extends ErrorResponseFactory {
constructor(
public readonly error: HTTPError,
output: 'json' | 'html' | 'auto' = 'auto', // FIXME xml support
) {
super(error, error.status, output)
}
}

View File

@@ -0,0 +1,19 @@
import {ResponseFactory} from "./ResponseFactory";
import {Request} from "../lifecycle/Request";
export function json(value: any): JSONResponseFactory {
return new JSONResponseFactory(value)
}
export class JSONResponseFactory extends ResponseFactory {
constructor(
public readonly value: any
) { super() }
public async write(request: Request) {
request = await super.write(request)
request.response.setHeader('Content-Type', 'application/json')
request.response.body = JSON.stringify(this.value)
return request
}
}

View File

@@ -0,0 +1,17 @@
import {HTTPStatus} from "@extollo/util"
import {Instantiable} from "@extollo/di"
import {Request} from "../lifecycle/Request"
export abstract class ResponseFactory {
protected targetStatus: HTTPStatus = HTTPStatus.OK
public async write(request: Request): Promise<Request> {
request.response.setStatus(this.targetStatus)
return request
}
public status(status: HTTPStatus) {
this.targetStatus = status
return this
}
}

View File

@@ -0,0 +1,19 @@
import {ResponseFactory} from "./ResponseFactory";
import {Request} from "../lifecycle/Request";
export function plaintext(value: string): StringResponseFactory {
return new StringResponseFactory(value)
}
export class StringResponseFactory extends ResponseFactory {
constructor(
public readonly value: string,
) { super() }
public async write(request: Request) {
request = await super.write(request)
request.response.setHeader('Content-Type', 'text/plain')
request.response.body = this.value
return request
}
}

View File

@@ -0,0 +1,21 @@
import {ResponseFactory} from "./ResponseFactory";
import {HTTPStatus} from "@extollo/util";
import {Request} from "../lifecycle/Request";
export function redirect(destination: string): TemporaryRedirectResponseFactory {
return new TemporaryRedirectResponseFactory(destination)
}
export class TemporaryRedirectResponseFactory extends ResponseFactory {
protected targetStatus: HTTPStatus = HTTPStatus.TEMPORARY_REDIRECT
constructor(
public readonly destination: string
) { super() }
public async write(request: Request) {
request = await super.write(request)
request.response.setHeader('Location', this.destination)
return request
}
}

76
src/http/response/api.ts Normal file
View File

@@ -0,0 +1,76 @@
/**
* Base type for an API response format.
*/
export interface APIResponse {
success: boolean,
message?: string,
data?: any,
error?: {
name: string,
message: string,
stack?: string[],
}
}
/**
* Formats a mesage as a successful API response.
* @param {string} message
* @return APIResponse
*/
export function message(message: string): APIResponse {
return {
success: true,
message,
}
}
/**
* Formats a single record as a successful API response.
* @param record
* @return APIResponse
*/
export function one(record: any): APIResponse {
return {
success: true,
data: record,
}
}
/**
* Formats an array of records as a successful API response.
* @param {array} records
* @return APIResponse
*/
export function many(records: any[]): APIResponse {
return {
success: true,
data: {
records,
total: records.length,
},
}
}
/**
* Formats an error message or Error instance as an API response.
* @param {string|Error} error
* @return APIResponse
*/
export function error(error: string | Error): APIResponse {
if ( typeof error === 'string' ) {
return {
success: false,
message: error,
}
} else {
return {
success: false,
message: error.message,
error: {
name: error.name,
message: error.message,
stack: error.stack ? error.stack.split(/\s+at\s+/).slice(1) : [],
},
}
}
}