From 13d96624395505614ef58d1b914bcbb551b52266 Mon Sep 17 00:00:00 2001 From: Hide_D Date: Tue, 8 Aug 2023 17:46:39 +0000 Subject: [PATCH] =?UTF-8?q?=EB=B9=8C=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/api/Login/LoginByID.ts | 2 +- server/api/Login/ReqNonce.ts | 2 +- server/api/Login/test.ts | 2 +- server/api/ProcDecorator/ParseUserLevel.ts | 4 +- server/api/ProcDecorator/ReqGameLogin.ts | 4 +- server/api/ProcDecorator/ReqLogin.ts | 2 +- server/api/ProcDecorator/StartSession.ts | 2 +- server/api/ProcDecorator/base.ts | 2 +- server/api/defs.ts | 206 +-------------------- server/api/generator.ts | 181 +++++++++++++++++- 10 files changed, 193 insertions(+), 214 deletions(-) diff --git a/server/api/Login/LoginByID.ts b/server/api/Login/LoginByID.ts index 256137a..073a229 100644 --- a/server/api/Login/LoginByID.ts +++ b/server/api/Login/LoginByID.ts @@ -1,4 +1,4 @@ -import { POST, ngPOST } from "../defs"; +import { ngPOST } from "../defs"; import type { structure } from "../../apiStructure/sammoRootAPI"; import type { ExtractError, ExtractQuery, ExtractResponse } from "../../apiStructure/defs"; import { StartSession } from "../ProcDecorator/StartSession"; diff --git a/server/api/Login/ReqNonce.ts b/server/api/Login/ReqNonce.ts index d8fcbe8..1e59aff 100644 --- a/server/api/Login/ReqNonce.ts +++ b/server/api/Login/ReqNonce.ts @@ -1,4 +1,4 @@ -import { APINamespace, APINamespaceType, ngGET } from "../defs"; +import { type APINamespaceType, ngGET } from "../defs"; import type { structure } from "../../apiStructure/sammoRootAPI"; import type { DefAPINamespace, ExtractError, ExtractQuery, ExtractResponse } from "../../apiStructure/defs"; import { StartSession } from "../ProcDecorator/StartSession"; diff --git a/server/api/Login/test.ts b/server/api/Login/test.ts index 4984da4..c5c5469 100644 --- a/server/api/Login/test.ts +++ b/server/api/Login/test.ts @@ -1,4 +1,4 @@ -import { GET, ngGET } from "../defs"; +import { ngGET } from "../defs"; import type { structure } from "../../apiStructure/sammoRootAPI"; import type { ExtractError, ExtractQuery, ExtractResponse } from "../../apiStructure/defs"; diff --git a/server/api/ProcDecorator/ParseUserLevel.ts b/server/api/ProcDecorator/ParseUserLevel.ts index 9099731..fcd7006 100644 --- a/server/api/ProcDecorator/ParseUserLevel.ts +++ b/server/api/ProcDecorator/ParseUserLevel.ts @@ -1,5 +1,5 @@ -import { LoginCtx } from "./ReqLogin"; -import { SessionCtx } from "./StartSession"; +import type { LoginCtx } from "./ReqLogin"; +import type { SessionCtx } from "./StartSession"; import type { ProcDecoratorGenerator } from "./base"; import type { Request, Response } from "express"; diff --git a/server/api/ProcDecorator/ReqGameLogin.ts b/server/api/ProcDecorator/ReqGameLogin.ts index 90e4294..2d594f6 100644 --- a/server/api/ProcDecorator/ReqGameLogin.ts +++ b/server/api/ProcDecorator/ReqGameLogin.ts @@ -1,5 +1,5 @@ -import { LoginCtx } from "./ReqLogin"; -import { SessionCtx } from "./StartSession"; +import type { LoginCtx } from "./ReqLogin"; +import type { SessionCtx } from "./StartSession"; import type { ProcDecoratorGenerator } from "./base"; import type { Request, Response } from "express"; import { delay } from "../../util/delay.js"; diff --git a/server/api/ProcDecorator/ReqLogin.ts b/server/api/ProcDecorator/ReqLogin.ts index df62193..95aa66c 100644 --- a/server/api/ProcDecorator/ReqLogin.ts +++ b/server/api/ProcDecorator/ReqLogin.ts @@ -1,4 +1,4 @@ -import { SessionCtx } from "./StartSession"; +import type { SessionCtx } from "./StartSession"; import type { InvalidProc, ProcDecoratorGenerator } from "./base"; export type LoginCtx = { diff --git a/server/api/ProcDecorator/StartSession.ts b/server/api/ProcDecorator/StartSession.ts index 6400307..8466b00 100644 --- a/server/api/ProcDecorator/StartSession.ts +++ b/server/api/ProcDecorator/StartSession.ts @@ -13,7 +13,7 @@ export type SessionCtx = { export function StartSession(): ProcDecoratorGenerator { return (inCtx) => { - const raw: object = {}; + const raw: Record = {}; return { clearSession: () => Promise.resolve(), deleteValue: async (key: string) => { diff --git a/server/api/ProcDecorator/base.ts b/server/api/ProcDecorator/base.ts index ecb00a7..3541eaf 100644 --- a/server/api/ProcDecorator/base.ts +++ b/server/api/ProcDecorator/base.ts @@ -9,7 +9,7 @@ export type InvalidProc = InvalidResponse & { alreadyProcessed?: boolean; }; -export interface ProcDecorator> { +export interface ProcDecorator> { (inCtx: In, req: Request, res: Response, isValidRoute: boolean): MayBePromise; } diff --git a/server/api/defs.ts b/server/api/defs.ts index 94a3b5c..f71c254 100644 --- a/server/api/defs.ts +++ b/server/api/defs.ts @@ -6,162 +6,30 @@ import { type ValidatorOptions, } from "class-validator"; import { type ClassTransformOptions, plainToInstance } from "class-transformer"; -import { InvalidProc, ProcDecorator, ProcDecoratorChain, ResolveChain } from './ProcDecorator/base'; +import type { InvalidProc, ProcDecorator, ProcDecoratorChain, ResolveChain } from './ProcDecorator/base.js'; import { clamp } from 'lodash-es'; export type APINamespace = { [key: string]: APINamespace // eslint-disable-next-line @typescript-eslint/no-explicit-any - | ClassType> + | iAPI_GET // eslint-disable-next-line @typescript-eslint/no-explicit-any - | ClassType> + | iAPI_POST // eslint-disable-next-line @typescript-eslint/no-explicit-any - | ClassType> + | iAPI_PUT // eslint-disable-next-line @typescript-eslint/no-explicit-any - | ClassType> + | iAPI_DELETE // eslint-disable-next-line @typescript-eslint/no-explicit-any - | ClassType> + | iAPI_PATCH // eslint-disable-next-line @typescript-eslint/no-explicit-any - | ClassType> + | iAPI_HEAD ; } type ValidatorType = T extends undefined ? undefined : ClassType; // eslint-disable-next-line @typescript-eslint/no-explicit-any -export type AnyAPIExecuter = APIExecuter; - -export abstract class APIExecuter{ - readonly abstract reqType: HttpMethod; - readonly abstract argValidator?: ValidatorType; - readonly abstract procDecorator: PD; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private preDecorator: ProcDecorator[] = []; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private postDecorator: (ProcDecorator | undefined)[] = []; - private decoratorParsed = false; - - public parseDecorator(): void { - if (this.decoratorParsed) { - return; - } - this.decoratorParsed = true; - if (!this.procDecorator) { - return; - } - for (const procGen of this.procDecorator) { - const proc = procGen(); - if (Array.isArray(proc)) { - this.preDecorator.push(proc[0]); - this.postDecorator.push(undefined); - } - else { - this.preDecorator.push(proc); - this.postDecorator.push(proc); - } - } - } - - private async preCtx(expressReq: Request, expressRes: Response): Promise | [InvalidProc, ResolveChain, number]> { - if (!this.procDecorator) { - return {} as ResolveChain; - } - - let ctx = {} as ResolveChain; - for (const [idx, proc] of this.preDecorator.entries()) { - try { - const result = await proc(ctx, expressReq, expressRes, 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; - } - - private async postCtx(ctx: ResolveChain, expressReq: Request, expressRes: Response, index = -1): Promise<[InvalidProc, number] | undefined> { - if (!this.procDecorator) { - return; - } - - let isValidRoute = true; - if (index !== -1 && index !== this.postDecorator.length) { - isValidRoute = false; - index = clamp(index, 0, this.postDecorator.length); - } - - for (let i = index - 1; i >= 0; i--) { - const proc = this.postDecorator[i]; - if (!proc) { - continue; - } - - try { - const result = await proc(ctx, expressReq, expressRes, 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; - } - - protected abstract parseQuery(expressReq: Request): Promise; - protected abstract api(query: Q, ctx: ResolveChain, expressReq: Request, expressRes: Response): Promise; - public async run(expressReq: Request, expressRes: Response): Promise { - const query = await this.parseQuery(expressReq); - const ctx = await this.preCtx(expressReq, expressRes); - if (Array.isArray(ctx)) { - const [invalidProc, errCtx, idx] = ctx; - if (idx == 0) { - expressRes.json(invalidProc); - return; - } - const result = await this.postCtx(errCtx, expressReq, expressRes, idx); - if (result === undefined) { - expressRes.json(invalidProc); - return; - } - const [postInvalidProc,] = result; - expressRes.json(postInvalidProc); - return; - } - const result = await this.api(query, ctx, expressReq, expressRes); - const postResult = await this.postCtx(ctx, expressReq, expressRes); - if (postResult !== undefined) { - if (result === true) { - //NOTE: 이미 api에서 response를 보낸 특이 케이스. - return; - } - const [invalidProc,] = postResult; - invalidProc.alreadyProcessed = true; - expressRes.json(invalidProc); - return; - } - - if (result !== true) { - expressRes.json(result); - } - } -} +export type AnyAPIExecuter = ngAPIExecuter; interface ngAPIExecuter { (query: Q, ctx: ResolveChain, expressReq: Request, expressRes: Response): Promise; @@ -305,64 +173,6 @@ export function ngHEAD extends APIExecuter{ - protected async parseQuery(expressReq: Request): Promise { - - if (!this.argValidator) { - return expressReq.body as Q; - } - - const query = expressReq.body; - const classObject = plainToInstance(this.argValidator, query); - const errors = await validate(classObject); - if (errors.length) { - throw errors; - } - - return classObject as Q; - } -} - -export abstract class GET extends APIExecuter{ - override readonly reqType = 'get'; - protected async parseQuery(expressReq: Request): Promise { - if (!this.argValidator) { - return expressReq.query as Q; - } - - const query = expressReq.query; - const classObject = plainToInstance(this.argValidator, query); - const errors = await validate(classObject); - if (errors.length) { - throw errors; - } - - return classObject as Q; - } -} - -export abstract class POST extends APIBodyParseExecuter{ - readonly reqType = 'post'; -} - -export abstract class PUT extends APIBodyParseExecuter{ - readonly reqType = 'put'; -} - -export abstract class DELETE extends APIBodyParseExecuter{ - readonly reqType = 'delete'; -} - -export abstract class PATCH extends APIBodyParseExecuter{ - readonly reqType = 'patch'; -} - -export abstract class HEAD extends APIBodyParseExecuter{ - readonly reqType = 'head'; -} - export function raiseError(reason: string, recovery?: recoveryMethod): InvalidResponse { if (recovery) { return { diff --git a/server/api/generator.ts b/server/api/generator.ts index 69d0053..758a941 100644 --- a/server/api/generator.ts +++ b/server/api/generator.ts @@ -1,17 +1,186 @@ -import { Router } from 'express'; -import type { AnyAPIExecuter, APINamespace } from './defs.js'; +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(); for (const [key, value] of Object.entries(api)) { if (typeof value === 'function') { const executer = new value() as Q; - if (!executer.reqType) { + + if (!executer.httpMethod) { throw new Error('APIExecuter.reqType is not defined'); } - router[executer.reqType](key, async (req, res) => { - await executer.run(req, res); - }); + + if (executer.httpMethod === 'head') { + 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); + }); + } + + continue; } else { router.use(key, buildAPISystem(value));