import { Router, type Request, type Response } from 'express'; import type { AnyAPIExecuter, APINamespace, ClassType } from './defs.js'; import { plainToInstance } from 'class-transformer'; import { validate } from 'class-validator'; import type { ExtractQuery, RawArgType } from '../apiStructure/defs.js'; import type { Empty, InvalidProc, ProcDecorator, ProcDecoratorChain, ResolveChain } from './ProcDecorator/base.js'; import clamp from 'lodash-es/clamp.js'; async function parseParam>(req: Request, argValidator?: ClassType): Promise { if (!argValidator) { return req.query as Q; } const query = req.query; const classObject = plainToInstance(argValidator, query); const errors = await validate(classObject); if (errors.length) { throw errors; } return classObject as Q; } async function parseBody>(req: Request, argValidator?: ClassType): Promise { if (!argValidator) { return req.body as Q; } const query = req.body; const classObject = plainToInstance(argValidator, query); const errors = await validate(classObject); if (errors.length) { throw errors; } return classObject as Q; } // eslint-disable-next-line @typescript-eslint/no-explicit-any type PlainDecorator = ProcDecorator; async function preCtx>(preDecorator: PlainDecorator[], req: Request, res: Response): Promise | [InvalidProc, ResolveChain, number]> { if (!preDecorator) { return {} as ResolveChain; } let ctx = {} as ResolveChain; for (const [idx, proc] of preDecorator.entries()) { try { const result = await proc(ctx, req, res, true); if ('invalidProcSymbol' in result) { return [result, ctx, idx]; } ctx = result; } catch (e) { return [{ result: false, reason: `internal error[${idx}]: ${e}`, invalidProcSymbol: ['preCtx', idx], }, ctx, idx]; } } return ctx; } async function postCtx(ctx: ResolveChain, postDecorator: (undefined | PlainDecorator)[], req: Request, res: Response, index = -1): Promise<[InvalidProc, number] | undefined> { if (!postDecorator) { return; } let isValidRoute = true; if (index !== -1 && index !== postDecorator.length) { isValidRoute = false; index = clamp(index, 0, postDecorator.length); } for (let i = index - 1; i >= 0; i--) { const proc = postDecorator[i]; if (!proc) { continue; } try { const result = await proc(ctx, req, res, isValidRoute); if ('invalidProcSymbol' in result) { result.invalidProcSymbol = ['postCtx', i, result.invalidProcSymbol]; return [result, i]; } ctx = result; } catch (e) { return [{ result: false, reason: `internal error[${i}]: ${e}`, invalidProcSymbol: ['postCtx', i], }, i]; } } return; } async function apiRun(query: object, req: Request, res: Response, api: AnyAPIExecuter): Promise { const ctx = await preCtx(api.preDecorator, req, res); if (Array.isArray(ctx)) { const [invalidProc, errCtx, idx] = ctx; if (idx == 0) { res.json(invalidProc); return; } const result = await postCtx(errCtx, api.postDecorator, req, res, idx); if (result === undefined) { res.json(invalidProc); return; } const [postInvalidProc,] = result; res.json(postInvalidProc); return; } const result = await api(query, ctx, req, res); const postResult = await postCtx(ctx, api.postDecorator, req, res); if (postResult !== undefined) { if (result === true) { //NOTE: 이미 api에서 response를 보낸 특이 케이스. return; } const [invalidProc,] = postResult; invalidProc.alreadyProcessed = true; res.json(invalidProc); return; } if (result !== true) { res.json(result); } } export function buildAPISystem(api: N | Q): Router { const router = Router(); if(typeof api === 'function') { throw 'root api cannot be function'; } for (const [key, value] of Object.entries(api)) { if (typeof value !== 'function') { router.use(key, buildAPISystem(value)); continue; } const executer = value; if (!executer.httpMethod) { throw new Error('APIExecuter.reqType is not defined'); } if (executer.httpMethod === 'get') { router[executer.httpMethod](key, async (req, res) => { let query: ExtractQuery; try { query = await parseParam(req, executer.argValidator); } catch (e) { res.json({ result: false, reason: `invalid parameter: ${e}`, }); return; } await apiRun(query, req, res, executer); }); } else { router[executer.httpMethod](key, async (req, res) => { let query: ExtractQuery; try { query = await parseBody(req, executer.argValidator); } catch (e) { res.json({ result: false, reason: `invalid parameter: ${e}`, }); return; } await apiRun(query, req, res, executer); }); } } return router; }