import {createLogger, IDebugger} from "@gongt/ts-stl-library/debug/create-logger"; import {LOG_LEVEL} from "@gongt/ts-stl-library/debug/levels"; import {EBodyType, ERequestType, HTTP, lowercase_method, uppercase_method} from "@gongt/ts-stl-library/request/request"; import {NextFunction, Request, RequestHandler, Response, Router} from "express-serve-static-core"; import {resolve as resolvePath} from "path"; import {MulterFileFilter} from "../inject/multer.types"; import {RequestContext} from "./context"; import {ArgumentHandlerCallbackType, InputHelperBuilder} from "./input-helper"; import { checkConfigValidate, IHandlerUsing, inputHelperEmitArgumentCollector, inputHelperEmitMiddlewares, onlyPathParams, } from "./input-helper.middleware"; import {ResponseInterface} from "./response-wrapper"; import {AsyncEHCF, EHCF, REQUEST_CONTEXT_VAR_NAME} from "./types"; class InputHelperBuilderInternal extends InputHelperBuilder { constructor(name: string, parentCall: ArgumentHandlerCallbackType) { super({ state: {name, from: null}, handlerCallback: parentCall, }); } } export interface BodyConfig { bodySize: number|string; bodyType: EBodyType[]; uploadLimit: { size: number; count: number; fileFilter?: MulterFileFilter; }; } export class CompleteEvent extends Error { } const fatal = createLogger(LOG_LEVEL.ERROR, 'handler'); const debug_sill = createLogger(LOG_LEVEL.SILLY, 'handler'); const debug = createLogger(LOG_LEVEL.DEBUG, 'handler'); const notice = createLogger(LOG_LEVEL.NOTICE, 'handler'); export abstract class ExpressHandler> { protected _method: ERequestType; public debugBaseUrl: string = ''; protected _url: string; protected handler: EHCF; protected middlewares: (RequestHandler)[] = []; public readonly debug: IDebugger; public readonly error: IDebugger; public readonly notice: IDebugger; public readonly sill: IDebugger; protected inputHelpers: InputHelperBuilder[] = []; private extraContextLayers: AsyncEHCF[] = []; public readonly fileName: string; public readonly fileNameStackOffset: number = 0; public readonly using: IHandlerUsing = { useParamBody: false, usingCookie: false, usingSession: false, usingUser: false, }; protected abstract instanceContext(req: Request, res: Response): ContextType; constructor(method: ERequestType, url: string) { this._method = method; this._url = url; this.debug = this.wrapDebug(debug); this.sill = this.wrapDebug(debug_sill); this.error = this.wrapDebug(fatal); this.notice = this.wrapDebug(notice); this.fileName = ((s: string) => { const fn = /\((.+)\:\d+\)/.exec(s.split(/\n/g)[3 + this.fileNameStackOffset] || ''); if (!fn) { return ''; } return fn[1]; })((new Error).stack); } setMethod(method: ERequestType) { this._method = method; } get method() { return this._method; } get methodName() { return uppercase_method(this._method); } setUrl(url: string) { this._url = url; } get url() { return this._url; } private uploadLimit: { size: number; count: number; fileFilter?: MulterFileFilter; }; private bodySize?: number|string; private bodyType: EBodyType[] = []; get bodyConfig(): BodyConfig { return { uploadLimit: this.uploadLimit, bodyType: this.bodyType, bodySize: this.bodySize, }; } uploadFileLimit(count: number, sizeKb: number = 1024, fileFilter?: MulterFileFilter) { this.uploadLimit = {count, size: sizeKb * 1024, fileFilter}; return this; } limitBodySize(size: number|string) { this.bodySize = size; return this; } accept(...types: EBodyType[]) { this.bodyType = types; return this; } parseCookie() { this.using.usingCookie = true; } startSession() { this.using.usingSession = true; this.using.usingCookie = true; } handleArgument(name: string): InputHelperBuilder { const item = new InputHelperBuilderInternal(name, this.handleArgument.bind(this)); this.inputHelpers.push(item); return item; } protected getRouteUrl() { const extraPaths = this.inputHelpers.filter(onlyPathParams).map(item => `:${item._toJSON().name}`); const args = ['/', this._url].concat(extraPaths); return resolvePath.apply(undefined, args); } prependHandler(handler: AsyncEHCF) { if (!handler.name) { handler['_name'] = 'anonymous_' + detectStack() } this.extraContextLayers.unshift(handler); } prependMiddleware(middleware: RequestHandler) { this.middlewares.push(middleware); } setHandler(handler: EHCF) { this.handler = handler.bind(undefined); } /** * @deprecated */ setAsyncHandler(handler: AsyncEHCF) { this.handler = handler.bind(undefined); } registerRouter(route: Router) { Object.freeze(this); Object.freeze(this.middlewares); this.inputHelpers.forEach((e) => { e._finish(); }); Object.freeze(this.inputHelpers); const url = this.getRouteUrl(); const LC_method = this._method === ERequestType.TYPE_ANY? 'use' : lowercase_method(this._method); const UC_method = uppercase_method(this._method); const handler = this.handler; debug_sill('register route: [%s] \x1B[38;5;9m%s\x1B[0m', LC_method, url); if (!handler) { throw new TypeError(`no handler of route: ${UC_method} ${url}`); } const plist = this.inputHelpers.map(e => e._toJSON()); checkConfigValidate(this, plist); const contextCreator = (req, res, next) => { this.sill('create request context:'); const context: ContextType = req[REQUEST_CONTEXT_VAR_NAME] = this.instanceContext(req, res); Object.assign(context.response, { logDebug: this.debug, logError: this.error, logNotice: this.notice, logSill: this.sill, }); if (!this.using.usingSession) { context.setSession = alertNoSession; } if (!this.using.usingCookie) { context.response.setCookie = alertNoCookies; } this.sill('context created!'); next(); }; const collectParamsFunction = inputHelperEmitArgumentCollector(plist); const middlewares = [].concat( inputHelperEmitMiddlewares(this, plist), contextCreator, collectParamsFunction || [], this.middlewares, ); middlewares['handlerInfo'] = `[${UC_method}] ${url}`; debug_sill(' add context layers (length=%s)', this.extraContextLayers.length); this.extraContextLayers.forEach((cb, i) => { const wrapFunc = (req, res, next) => { const context: ContextType = req[REQUEST_CONTEXT_VAR_NAME]; this.sill('call layer %s: %s', i, cb.name); let ret; try { ret = cb(context); } catch (e) { this.sill('call layer %s: %s failed: %s', i, cb.name, e.message); return next(e); } if (context.response.complete) { this.sill('response killed by extraContextLayers - %s.', cb.name); return next(new CompleteEvent()); } if (ret && typeof ret.then === 'function') { return ret.then(() => { if (context.response.complete) { this.sill('response killed by extraContextLayers - %s.', cb.name); return next(new CompleteEvent()); } else { return next(); } }, next); } else { return next(); } }; wrapFunc['displayName'] = wrapFunc['_name'] = cb.name || cb['displayName'] || cb['_name']; middlewares.push(wrapFunc); }); debug_sill(' add request callback'); const requestMainCall = (req: Request, res: Response, next: NextFunction) => { this.sill('run api async'); const context: ContextType = req[REQUEST_CONTEXT_VAR_NAME]; this.sill('call handler'); const ret = handler.call(this, context); this.sill('handler returned [%s]', ret); if (ret && typeof ret.then === 'function') { if (context.response.complete) { throw new Error('async handler function returned a promise, but response is already finished.'); } context.response.resolve(ret).catch(next); } else if (!context.response.complete) { context.response.resolve(Promise.resolve(ret)).catch(next); } }; middlewares.push(requestMainCall); if (this.sill.enabled) { middlewares_debug_logging(this.sill, middlewares); } debug_sill(' add error callback'); const errorCallback = (e: Error, req: Request, res: Response, next: NextFunction) => { if (e instanceof CompleteEvent) { return; } const msg = e? e.message || e : 'no error message'; if (typeof msg === 'string') { this.sill(`can't handle request, because: %s`, msg.replace(/^Error: /, '')); } else { this.sill(`can't handle request (not a error object):`); this.sill(msg); } if (req[REQUEST_CONTEXT_VAR_NAME]) { const context_response: ResponseInterface = req[REQUEST_CONTEXT_VAR_NAME].response; if (!context_response.complete) { context_response.resolve(Promise.reject(e)).catch(next); } } else { if (!res.headersSent) { res.status(HTTP.INTERNAL_SERVER_ERROR).send(`

Unhandled Internal Server Error

${msg}
`); } } }; if (this.sill.enabled) { let fn; eval(`const errorCallbackOf_${url.replace(/[^0-9a-z_]/ig, '_')} = ${errorCallback.toString()}; fn = errorCallbackOf_${url.replace(/[^0-9a-z_]/ig, '_')}`); middlewares.push(fn); middlewares.unshift((req, res, next) => { this.sill('got request: %s', req.originalUrl); this.sill(' handler: %s', this.fileName); next(); }); } else { middlewares.push(errorCallback); } debug_sill(` register express handler functions:\n route.${LC_method}("${url}", [${middlewares.map((e) => e._name || e.displayName || e.name).join(', ')}])`); route[LC_method](url, middlewares); debug_sill(' - complete. (%s middlewares and 1 error cb)', middlewares.length); } private wrapDebug(original): IDebugger { const method = uppercase_method(this.method); const retFn: any = (str: string, ...args: any[]) => { str = '[%s %s%s]: ' + str; args.unshift(this.url); args.unshift(this.debugBaseUrl); args.unshift(method); args.unshift(str); return original.apply(undefined, args); }; Object.defineProperty(retFn, 'enabled', { get() { return original.enabled; }, }); return retFn; } } function middlewares_debug_logging(logger: IDebugger, middlewares: RequestHandler[]) { middlewares.forEach((func, i) => { if (!func.name && !func['_name']) { console.error('--------------------\n%s\n--------------------', func.toString()); } const ref = func; middlewares[i] = function (this: any) { logger('middleware (%s/%s): %s', 1 + i, middlewares.length, func['displayName'] || func.name); return ref.apply(this, arguments); }; middlewares[i]['_name'] = func['_name'] || func.name || ''; }); } function detectStack() { const err = new Error; const file = err.stack.split(/\n/g)[3] || ''; const m = /\/[^\/]+:\d+:\d+/.exec(file); return m? m[0].replace(/^\//, '').replace(/-|:|\./g, '_') : 'unknown'; } function alertNoSession() { throw new Error('using session without start it.'); } function alertNoCookies() { throw new Error('using cookies without parse it.'); }