parent
3acc1bc83e
commit
a9ffa771dc
@ -0,0 +1,11 @@
|
||||
import {ErrorWithContext, HTTPStatus, HTTPMessage} from "@extollo/util"
|
||||
|
||||
export class HTTPError extends ErrorWithContext {
|
||||
constructor(
|
||||
public readonly status: HTTPStatus = 500,
|
||||
public readonly message: string = ''
|
||||
) {
|
||||
super(message || HTTPMessage[status])
|
||||
this.message = message || HTTPMessage[status]
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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) : [],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in new issue