/// import {createLogger, debugType, debugTypeInObj} from "@gongt/ts-stl-library/debug/create-logger"; import {LOG_LEVEL} from "@gongt/ts-stl-library/debug/levels"; import DI, {DDNames} from "@gongt/ts-stl-library/DI"; import {STATUS_CODE_BASE} from "@gongt/ts-stl-library/request/protocol"; import {EBodyType, ERequestType} from "@gongt/ts-stl-library/request/request"; import {RequestError} from "@gongt/ts-stl-library/request/request-error"; import {Request, RequestHandler} from "express-serve-static-core"; import {CreateFileHandler} from "../inject/multer.types"; import {initBodyParser} from "./body-parser"; import {RequestContext} from "./context"; import {BodyConfig} from "./handler"; import {ArgumentProcessorContext, EParamType, ParamType, REQUEST_CONTEXT_VAR_NAME} from "./types"; const debug = createLogger(LOG_LEVEL.SILLY, 'handler:argument'); const data = createLogger(LOG_LEVEL.DATA, 'handler:argument'); export const onlyUserParams = e => e.from === EParamType.TYPE_LOGIN; export const onlyPathParams = e => e.from === EParamType.TYPE_REWRITE_PATH; export const notQueryParams = e => e.from !== EParamType.TYPE_REWRITE_PATH && e.from !== EParamType.TYPE_GET; export const onlyFileParams = e => e.from === EParamType.TYPE_UPLOAD; export const onlyRawParams = e => e.from === EParamType.TYPE_BODY; export const onlyPostParams = e => e.from === EParamType.TYPE_POST; export interface IHandlerUsing { usingUser?: boolean; useParamBody: boolean; usingCookie: boolean; usingSession: boolean; } export interface IHandlerConfig { url: string; method: ERequestType; methodName: string; bodyConfig: BodyConfig; using: IHandlerUsing; } export function handleArgumentFail(e: Error, handlerConfig: IHandlerConfig, list: ParamType[]) { const haveBody = handlerConfig.method === ERequestType.TYPE_POST || handlerConfig.method === ERequestType.TYPE_PUT; const haveSomeUpload = list.filter(onlyFileParams).map(e => e.name); const useRawBody = list.filter(onlyRawParams).map(e => e.name); const useParamBody = list.filter(onlyPostParams).map(e => e.name); console.error('\x1B[38;5;9m ------- server start failed -------\x1B[0m'); console.error('Message: %s', e.message); console.error('\x1B[2m%s\x1B[0m', e.stack.split(/\n/g).slice(1, 5).join('\n')); console.error('handler: %s %s', handlerConfig.methodName, handlerConfig.url); console.error(' bodyConfig: %j', handlerConfig.bodyConfig); console.error('config:'); console.error(' haveBody = %s', haveBody); console.error(' haveSomeUpload = %s', haveSomeUpload); console.error(' useRawBody = %s', useRawBody); console.error(' useParamBody = %s', useParamBody); console.error('argument list: '); list.forEach((obj, i) => { obj = Object.assign({}, obj); console.error(' [arg%s]: %s from %s(%s)', i, obj.name, EParamType[obj.from], obj.from); delete obj.name; delete obj.from; Object.keys(obj).forEach((item) => { console.error(' %s: ', item, debugTypeInObj(obj[item])); }); }); console.error('\x1B[38;5;9m ------- server start failed -------\x1B[0m'); console.error('\x1B[5mwaitting changes, server will restart.\x1B[0m'); process.exit(1); } export function checkConfigValidate(handlerConfig: IHandlerConfig, list: ParamType[]) { try { realCheckConfigValidate.apply(undefined, arguments); } catch (e) { handleArgumentFail(e, handlerConfig, list); } } export function realCheckConfigValidate(handlerConfig: IHandlerConfig, list: ParamType[]) { const userParam = list.findIndex(onlyUserParams); if (userParam !== -1) { if (list.slice(userParam + 1).find(onlyUserParams)) { throw new TypeError('only one user params allowed'); } } const haveBody = handlerConfig.method === ERequestType.TYPE_POST || handlerConfig.method === ERequestType.TYPE_PUT; const haveSomeUpload = list.findIndex(onlyFileParams) !== -1; const useRawBody = list.findIndex(onlyRawParams) !== -1; const useParamBody = list.findIndex(onlyPostParams) !== -1; const mustHaveBody = useParamBody || haveSomeUpload; if (useRawBody && haveSomeUpload) { throw new TypeError(`can't upload file with raw body type.`); } if (useRawBody && list.filter(notQueryParams).length > 1) { throw new TypeError(`BODY argument must use standalone. (no other POST params)`); } if (mustHaveBody && !haveBody) { throw new TypeError(`try to get body from request with no body`); } if (haveSomeUpload) { if (handlerConfig.bodyConfig.bodyType.length) { throw new TypeError(`uploading file require multipart body type, no input.accept() can be called.`); } if (handlerConfig.bodyConfig.bodySize) { throw new TypeError(`uploading file require multipart body type, no input.limitBodySize() can be called.`); } } else if (handlerConfig.bodyConfig.uploadLimit) { throw new TypeError(`no files upload, can't use input.uploadFileLimit().`); } const bodyParam = list.find(onlyRawParams); if (bodyParam && !bodyParam.middleware) { throw new TypeError(`raw body param ${bodyParam.name} not called input.type().`); } list.forEach((self) => { if (!self.name) { throw new TypeError(`some argument incomplete defined, missing name(${self.name}).`); } if (!EParamType[self.from]) { throw new TypeError(`argument "${self.name}" incomplete defined, missing from(${self.from}).`); } if (list.find(e => e !== self && e.name === self.name)) { throw new TypeError(`duplicate argument: ${self.name}`); } }); } export function inputHelperEmitMiddlewares(handlerConfig: IHandlerConfig, plist: ParamType[]) { debug('generate arguments middleware: '); const list: RequestHandler[] = []; const bodyConfig = handlerConfig.bodyConfig; const uploadArray = []; let {useParamBody, usingCookie, usingSession} = handlerConfig.using; let usingUser: any = null; plist.forEach((e) => { switch (e.from) { case EParamType.TYPE_GET: case EParamType.TYPE_REWRITE_PATH: return; case EParamType.TYPE_LOGIN: { usingCookie = true; usingUser = e.middleware || DI.get(DDNames.defaultUserHandler); if (e.middlewares && e.middlewares.length) { e.middlewares.forEach(m => list.push(m)); debug(` ${e.name}:\t user, %s middlewares first: %s`, e.middlewares.length + 1, e.middleware.name); usingUser = [usingUser, ...e.middlewares]; } else { debug(` ${e.name}:\t user. no other middleware.`); usingUser = [usingUser]; } return; } case EParamType.TYPE_BODY: list.push(e.middleware); debug(` ${e.name}:\t raw body getter: %s`, e.middleware.name); return; case EParamType.TYPE_POST: useParamBody = true; debug(` ${e.name}:\t normal post pararm`); return; case EParamType.TYPE_UPLOAD: uploadArray.push({ name: e.name, maxCount: e.upload? e.upload.fileCount : bodyConfig.uploadLimit.count, }); debug(` ${e.name}:\t file upload (%s)`, uploadArray[uploadArray.length - 1].maxCount); return; case EParamType.TYPE_COOKIE: case EParamType.TYPE_SAFE_COOKIE: usingCookie = true; debug(` ${e.name}:\t cookie`); return; case EParamType.TYPE_SESSION: usingCookie = true; usingSession = true; debug(` ${e.name}:\t session`); return; default: throw new TypeError(`unknown argument type: [${e.name}] = ${e.from}`) } }); if (usingCookie) { debug(` add cookie parser`); list.push(DI.get(DDNames.cookieHandler)); } if (usingSession) { debug(` add session parser`); list.push(DI.get(DDNames.sessionHandler)); } if (usingUser) { debug(` add user handler`); usingUser.forEach(e => list.push(e)); } if (uploadArray.length) { debug(` add multipart parser`); const createFileHandler: CreateFileHandler = DI.get(DDNames.multerCreator); const fileupload = createFileHandler({ fileSize: bodyConfig.uploadLimit.size, }, bodyConfig.uploadLimit.fileFilter).fields(uploadArray); list.push(fileupload); } if (useParamBody) { let body = bodyConfig.bodyType; const size = bodyConfig.bodySize; debug(` body parser: %j`, body); if (!body.length) { body = [EBodyType.TYPE_ENCODE, EBodyType.TYPE_JSON]; } body.forEach((t) => { debug(` add body parser: %s`, EBodyType[t]); list.push(initBodyParser(t, size)); }); } handlerConfig.using.useParamBody = useParamBody; handlerConfig.using.usingCookie = usingCookie; handlerConfig.using.usingSession = usingSession; handlerConfig.using.usingUser = !!usingUser; debug(`ok. ${list.length} middlewares`); return list; } function noop() { return Promise.resolve(); } export function inputHelperEmitArgumentCollector(plist: ParamType[]): RequestHandler { if (!plist.length) { return null; } const paramsFilters = plist.filter(e => !!e.filter); const paramsHandler = plist.filter(e => !!e.handler); return function argumentHandleMiddleware(req: Request&{user: any, files: any}, res, next) { const context: RequestContext = req[REQUEST_CONTEXT_VAR_NAME]; const params = context.params; const arg_context: ArgumentProcessorContext = { request: req, response: context.response, }; collectArguments.call(arg_context, params, plist, req); if (paramsFilters.length) { filterParamsItem.call(arg_context, params, paramsFilters); } if (paramsHandler.length) { handleParamsItem.call(arg_context, params, paramsHandler); } delete params['__missing']; if (debug.enabled) { debug('collected params: ', JSON.stringify(params)); // Object.freeze(params); } next(); } } function hasOwn(obj: any, name: string) { return Object.prototype.hasOwnProperty.call(obj, name); } function collectArguments(this: ArgumentProcessorContext, ret: ReqType, plist: ParamType[], req: Request&{user: any, files: any}): ReqType { const missing: {[id: string]: boolean} = ret['__missing'] = {}; const handleNotExists = (from: any, item: ParamType) => { if (hasOwn(from, item.name)) { ret[item.name] = from[item.name]; } else if (item.optionalDefault === null) { missing[item.name] = true; ret[item.name] = null; } else if (item.optionalDefault === undefined) { data('missing from: %j', from); throw new Error(`missing required param: ${item.name}`); } else { missing[item.name] = true; if (typeof item.optionalDefault === 'function') { ret[item.name] = item.optionalDefault.call(this, req); } else { ret[item.name] = item.optionalDefault; } } }; plist.forEach((item) => { debug('try to get param "%s" from "%s"', item.name, EParamType[item.from]); switch (item.from) { case EParamType.TYPE_GET: handleNotExists(req.query, item); break; case EParamType.TYPE_POST: handleNotExists(req.body, item); break; case EParamType.TYPE_BODY: ret[item.name] = req.body; break; case EParamType.TYPE_REWRITE_PATH: handleNotExists(req.params, item); break; case EParamType.TYPE_LOGIN: ret[item.name] = req.user; break; case EParamType.TYPE_COOKIE: handleNotExists(req.cookies, item); break; case EParamType.TYPE_SAFE_COOKIE: ret[item.name] = req.signedCookies[item.name]; break; case EParamType.TYPE_SESSION: ret[item.name] = req.session[item.name]; break; case EParamType.TYPE_UPLOAD: ret[item.name] = req.files[item.name]; break; default: throw new TypeError(`unknown argument type: [${item.name}] = ${item.from}`) } if (debug.enabled) { const res = ret[item.name]; if (res && typeof res === 'object') { debug(' result: %j', debugType(res)); } else { debug(' result: %s', res); } } }); return ret; } function filterParamsItem(this: ArgumentProcessorContext, collect: ReqType, params: ParamType[]) { const missing: string[] = collect['__missing']; params.forEach((n) => { if (missing[n.name]) { return true; } let ret; try { ret = n.filter.call(this, collect[n.name], n.name); } catch (e) { data('filter fail value: %j', collect[n.name]); const fnName = n.filter['displayName'] || n.filter.name; throw new RequestError(STATUS_CODE_BASE.INVALID_INPUT, `param value parse failed: ${n.name}[${n.from}] by ${fnName}: ${e.message}`); } if (!ret) { const fnName = n.filter['displayName'] || n.filter.name; data('filter fail value: %j', collect[n.name]); throw new RequestError(STATUS_CODE_BASE.INVALID_INPUT, `param value parse failed: ${n.name}[${n.from}] by ${fnName}: no information`); } }); } function handleParamsItem(this: ArgumentProcessorContext, collect: ReqType, params: ParamType[]) { params.forEach((n) => { const ret = n.handler.call(this, collect[n.name]); if (ret) { console.log(n.name, n.handler.toString()); collect[n.name] = ret; } }); }