diff --git a/server/api/Login/LoginByID.ts b/server/api/Login/LoginByID.ts index 0f4e35e..bbf1575 100644 --- a/server/api/Login/LoginByID.ts +++ b/server/api/Login/LoginByID.ts @@ -11,10 +11,14 @@ class Q implements QType { username!: string; password!: string; } -export class LoginByID extends POST{ + +const procDecorator = [] as const; + +export class LoginByID extends POST{ readonly argValidator = Q; + readonly procDecorator = procDecorator; protected LoginByID = Symbol("LoginByID");//TODO: remove this - protected override async api(query: QType): Promise { + protected override async api(query: QType, ctx,): Promise { throw new Error("Method not implemented."); } } \ No newline at end of file diff --git a/server/api/Login/ReqNonce.ts b/server/api/Login/ReqNonce.ts index ed5c8bd..71417c2 100644 --- a/server/api/Login/ReqNonce.ts +++ b/server/api/Login/ReqNonce.ts @@ -9,6 +9,7 @@ type QType = ExtractQuery; export class ReqNonce extends GET{ readonly argValidator = undefined; + readonly procDecorator = [] as const; protected ReqNonce = Symbol("ReqNonce");//TODO: remove this protected override async api(query: QType): Promise { throw new Error("Method not implemented."); diff --git a/server/api/ProcDecorator/base.ts b/server/api/ProcDecorator/base.ts index 72444bb..ecb00a7 100644 --- a/server/api/ProcDecorator/base.ts +++ b/server/api/ProcDecorator/base.ts @@ -1,67 +1,28 @@ import type { Request, Response } from "express"; +import type { InvalidResponse } from "../../apiStructure/defs"; type MayBePromise = T | Promise; -export type InvalidProc = { +export type Empty = Record; +export type InvalidProc = InvalidResponse & { invalidProcSymbol: unknown; - reason?: string; alreadyProcessed?: boolean; -} +}; export interface ProcDecorator> { - (inCtx: In, req: Request, res: Response): MayBePromise; -} - -export type Empty = Record; - -type Login = { - userID: number; -} -type GameLogin = { - userID: number; - generalID: number; -} -type UserLevel = { - userID: number; - userLevel: number; -} - -export function ParseUserLevel(): ProcDecorator { - return (inCtx) => { - return { - userLevel: 123, - ...inCtx, - }; - } -} - -export function ReqLogin(): [ProcDecorator, ProcDecorator] { - return [ - (inCtx) => { - return { - userID: 999, - ...inCtx, - }; - }, - (inCtx) => { - //무언가 - return inCtx; - } - ] -} - -export function ReqGameLogin(): ProcDecorator { - return (inCtx) => { - return { - generalID: inCtx.userID * 4, - ...inCtx, - }; - } + (inCtx: In, req: Request, res: Response, isValidRoute: boolean): MayBePromise; } export type ProcDecoratorGenerator = ProcDecorator; export type ProcDecoratorPrePostGenerator = [ProcDecorator, ProcDecorator]; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type ProcDecoratorChain = undefined | readonly ((() => ProcDecorator) | (() => [ProcDecorator, ProcDecorator]))[]; + +export type ResolveChain = T extends undefined ? Empty : T extends ProcDecoratorChain ? Resolve> : never; + + + type Compose2 = D & B extends A & C ? B extends C ? ProcDecorator : never : never; type PD1 = () => ProcDecorator; @@ -79,74 +40,3 @@ type PackChain = never; type Resolve = T extends ProcDecorator ? Empty extends A ? B : never : never; - - -export type ResolveChain = Resolve>; - -const q1 = [ - ReqLogin, - ParseUserLevel, - ReqGameLogin, -] as const; - -const q2 = [ - ReqLogin, - ParseUserLevel, -] as const; - - -type R1 = Resolve>; -type R2 = Resolve>; -const r1: R1 = undefined as unknown as R1; -const r2: R2 = undefined as unknown as R2; -r1.userLevel; -r2.userID; - - -async function test(req: Request, res: Response) { - const a0 = await ReqLogin()[0]({}, req, res); - if ('invalidProcSymbol' in a0) { - return false; - } - const a1 = await ParseUserLevel()(a0, req, res); - if ('invalidProcSymbol' in a1) { - return false; - } - const a2 = await ReqGameLogin()(a1, req, res); - if ('invalidProcSymbol' in a2) { - return false; - } - const b0 = await ReqLogin()[1](a2, req, res); - return true; -} - - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type ProcDecoratorChain = readonly ((() => ProcDecorator) | (() => [ProcDecorator, ProcDecorator]))[]; - -function unpack2, B>(v: A | [A, B]): A { - if (Array.isArray(v)) { - return v[0]; - } - return v; -} - -function test2(req: Request, res: Response, chain: T): Resolve> | false { - const tmp = chain[0](); - - const actors = chain.map((v) => unpack2(v())); - - let result = {}; - for (const actor of actors) { - result = actor(result, req, res); - if ('isInvalid' in result) { - return false; - } - } - return result as Resolve>; -} - -const finalResult1 = test2({} as Request, {} as Response, q1); -if (finalResult1 !== false) { - finalResult1.userLevel; -} \ No newline at end of file diff --git a/server/api/defs.ts b/server/api/defs.ts index 37595be..25288fc 100644 --- a/server/api/defs.ts +++ b/server/api/defs.ts @@ -6,46 +6,164 @@ import { type ValidatorOptions, } from "class-validator"; import { type ClassTransformOptions, plainToInstance } from "class-transformer"; +import { InvalidProc, ProcDecorator, ProcDecoratorChain, ResolveChain } from './ProcDecorator/base'; +import { clamp } from 'lodash-es'; export type APINamespace = { [key: string]: APINamespace // eslint-disable-next-line @typescript-eslint/no-explicit-any - | ClassType> + | ClassType> // eslint-disable-next-line @typescript-eslint/no-explicit-any - | ClassType> + | ClassType> // eslint-disable-next-line @typescript-eslint/no-explicit-any - | ClassType> + | ClassType> // eslint-disable-next-line @typescript-eslint/no-explicit-any - | ClassType> + | ClassType> // eslint-disable-next-line @typescript-eslint/no-explicit-any - | ClassType> + | ClassType> // eslint-disable-next-line @typescript-eslint/no-explicit-any - | ClassType> + | ClassType> ; } type ValidatorType = T extends undefined ? undefined : ClassType; // eslint-disable-next-line @typescript-eslint/no-explicit-any -export type AnyAPIExecuter = APIExecuter; +export type AnyAPIExecuter = APIExecuter; -export abstract class APIExecuter{ +export abstract class APIExecuter{ readonly abstract reqType: HttpMethod; readonly abstract argValidator?: ValidatorType; - protected abstract parseQuery(expressReq: Request): Promise; - protected abstract api(query: Q, expressReq: Request, expressRes: Response): Promise; - public async run(expressReq: Request, expressRes: Response): Promise { - const query = await this.parseQuery(expressReq); - //TODO: middleware (인증, 요구사항, 아마도 decorator) - const result = await this.api(query, expressReq, expressRes); - if (result === true) { + 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; } - expressRes.json(result); + 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 abstract class APIBodyParseExecuter extends APIExecuter{ +export abstract class APIBodyParseExecuter extends APIExecuter{ protected async parseQuery(expressReq: Request): Promise { if (!this.argValidator) { @@ -63,7 +181,7 @@ export abstract class APIBodyParseExecuter extends APIExecuter{ +export abstract class GET extends APIExecuter{ override readonly reqType = 'get'; protected async parseQuery(expressReq: Request): Promise { if (!this.argValidator) { @@ -81,23 +199,23 @@ export abstract class GET extends APIBodyParseExecuter{ +export abstract class POST extends APIBodyParseExecuter{ readonly reqType = 'post'; } -export abstract class PUT extends APIBodyParseExecuter{ +export abstract class PUT extends APIBodyParseExecuter{ readonly reqType = 'put'; } -export abstract class DELETE extends APIBodyParseExecuter{ +export abstract class DELETE extends APIBodyParseExecuter{ readonly reqType = 'delete'; } -export abstract class PATCH extends APIBodyParseExecuter{ +export abstract class PATCH extends APIBodyParseExecuter{ readonly reqType = 'patch'; } -export abstract class HEAD extends APIBodyParseExecuter{ +export abstract class HEAD extends APIBodyParseExecuter{ readonly reqType = 'head'; } @@ -120,12 +238,18 @@ export function raiseError(reason: string, recovery?: recoveryMethod): InvalidRe export type ClassType = new (...args: any[]) => T; export type APIServerType = - T extends GetAPICallT ? ClassType> : - T extends PostAPICallT ? ClassType> : - T extends PutAPICallT ? ClassType> : - T extends DeleteAPICallT ? ClassType> : - T extends PatchAPICallT ? ClassType> : - T extends HeadAPICallT ? ClassType> : + // eslint-disable-next-line @typescript-eslint/no-explicit-any + T extends GetAPICallT ? ClassType> : + // eslint-disable-next-line @typescript-eslint/no-explicit-any + T extends PostAPICallT ? ClassType> : + // eslint-disable-next-line @typescript-eslint/no-explicit-any + T extends PutAPICallT ? ClassType> : + // eslint-disable-next-line @typescript-eslint/no-explicit-any + T extends DeleteAPICallT ? ClassType> : + // eslint-disable-next-line @typescript-eslint/no-explicit-any + T extends PatchAPICallT ? ClassType> : + // eslint-disable-next-line @typescript-eslint/no-explicit-any + T extends HeadAPICallT ? ClassType> : never; export type APINamespaceType = { [K in keyof T]: