import {createLogger, debugType, IDebugger} from "@gongt/ts-stl-library/debug/create-logger"; import {LOG_LEVEL} from "@gongt/ts-stl-library/debug/levels"; import {ApiResponse, getErrorMessage, STATUS_CODE_BASE, STATUS_CODE_TYPE} from "@gongt/ts-stl-library/request/protocol"; import {HTTP} from "@gongt/ts-stl-library/request/request"; import {ErrorResponse, RequestError} from "@gongt/ts-stl-library/request/request-error"; import {CookieOptions, Response} from "express-serve-static-core"; export type KV = {[name: string]: any}; export interface AssignFunction { (name: string, value: any): void; } export interface AssignObjectFunction { (object: KV): void; } const debug = createLogger(LOG_LEVEL.INFO, 'response'); const debugSill = createLogger(LOG_LEVEL.SILLY, 'response'); export abstract class ResponseInterface { protected readonly response: Response; private httpStatus: HTTP = HTTP.OK; protected logDebug: IDebugger; protected logNotice: IDebugger; protected logError: IDebugger; protected logSill: IDebugger; constructor(response: Response) { this.response = response; } protected completed: boolean = false; get complete() { return this.completed; } set __force_complete(v: boolean) { this.completed = v; } redirect(target: string, permanent: boolean = false, withGet: boolean = true) { if (this.completed) { throw new Error(`duplicate response in one request`); } this.completed = true; this.logSill('redirect to %s, permanent=%s, with-get=%s', target, permanent, withGet); if (withGet) { this.response.redirect(permanent? HTTP.MOVED_PERMANENTLY : HTTP.FOUND, target); } else { this.response.redirect(permanent? HTTP.PERMANENT_REDIRECT : HTTP.TEMPORARY_REDIRECT, target); } } public sendRawData(data: any) { debugSill('call sendRawData()'); if (this.completed) { throw new Error(`duplicate response in one request`); } this.response.send(data); this.completed = true; } protected send(): any { debugSill('\x1B[38;5;5m + call send();\x1B[0m'); if (this.completed) { throw new Error(`duplicate response in one request`); } this.response.status(this.httpStatus); const r: any = this._send(); if (r && r instanceof Promise) { return r.then(() => { this.completed = true; }, (e) => { if (!this.completed) { this.fatalError(e); } }); } this.completed = true; } public httpCode(status: HTTP) { this.httpStatus = status; } public setHeader(name: string, value: string) { this.response.header(name, value); } protected fatalError(e: any) { debug('call fatalError()'); debugSill(' with: %o', e); if (this.completed) { throw new Error(`duplicate response in one request (this one is fataError())`); } this.completed = true; this.response.status(HTTP.INTERNAL_SERVER_ERROR).send(` Internal Server Error

Internal Server Error

${e? e.stack || e.message : 'unknown why'}
`); this.logError('error while process raw request: %s', e? e.stack || e.message : 'NO ERROR OBJECT'); } protected abstract _send(): void; protected abstract asyncResolve(res: ResType): void; protected asyncReject(e: any): void { this.fatalError(e); } resolve(p: Promise): Promise { if (this.complete) { this.logError('return promise but already responsed.'); return p; } return p.then((res) => { if (debugSill.enabled) { if (res) { debugSill('async handler promise resolved'); if (res instanceof Buffer) { debugSill(' with: Buffer(%s)', res.length); } else { debugSill(' with: %j', debugType(res)); } } else { debugSill('async handler promise resolved (empty: %s)', res); } } if (this.complete) { if (res) { this.logError('extra response!\n==============\n%j\n==============', res); } return; } else if (res) { this.asyncResolve(res); } return this.send(); }).catch((err) => { if (debugSill.enabled) { debugSill('async handler promise rejected'); debugSill(' with: %j', err); } if (this.complete) { this.logError('error after response: %s', err.stack || err.message); this.response.status(500).send(`

Internal Server Error

return promise but already responsed.

${err? err.stack || err.message : 'No Error Info'}
`); return; } this.logNotice('response complete, with error returned.'); if (!( err instanceof RequestError)) { this.logNotice(err.stack); } this.asyncReject(err); if (!this.complete) { this.logError('error handler no response'); } }); } setCookie(options: CookieOptions&{name: string; value: any;}) { const def: CookieOptions = { signed: false, httpOnly: true, path: '/', secure: false, }; if (!options.expires && !options.maxAge) { def.maxAge = 3600000 * 24 } if (options.value === undefined || options.value === null) { this.logSill('setCookie: %s=*{delete}', options.name); this.response.clearCookie(options.name, options); } else { this.logSill('setCookie: %s= %s, sign=%s', options.name, options.value, options.signed); this.response.cookie(options.name, options.value, Object.assign(def, options)); } } } export abstract class MultiValueResponseWrapper extends ResponseInterface { protected abstract result: ResType&ApiResponse|ErrorResponse; protected mergeTemplateLocals(): ResType&ApiResponse|ErrorResponse { const result: any = this.response.locals || {}; // NOTE: response.req.app is internal API of express@4 Object.assign(result, this.response['req']['app'].locals, { status: STATUS_CODE_BASE.NO_RESPONSE, message: 'success', }); return result; } get localValue() { return this.result; } assignObject(object: KV): void { Object.assign(this.result, object); } assign(name: string, value: any): void { this.result[name] = value; } success() { this.result.status = STATUS_CODE_BASE.SUCCESS; return this.send(); } internalError(error: Error) { this.result = RequestError.internal(error).response(); return this.send(); } error(error: STATUS_CODE_TYPE, extraMessage?: string) { this.result.status = error; this.result.message = getErrorMessage(error); if (extraMessage) { this.result.message += extraMessage; } return this.send(); } }