///
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;
}
});
}