diff --git a/server/api/ProcDecorator/ParseUserLevel.ts b/server/api/ProcDecorator/ParseUserLevel.ts deleted file mode 100644 index 8ab8fb3..0000000 --- a/server/api/ProcDecorator/ParseUserLevel.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { LoginCtx } from "./ReqLogin.js"; -import type { SessionCtx } from "./StartSession.js"; -import type { ProcDecoratorGenerator } from "./base.js"; -import type { Request, Response } from "express"; - - -export interface UserLevelCtx { - userLevel: number; -} - -export function ParseUserLevel(): ProcDecoratorGenerator { - return (ctx) => { - const userLevel = 123; - ctx.setValue('userLevel', userLevel); - return [{ - result: true, - }, { - userLevel, - ...ctx, - }]; - } -} \ No newline at end of file diff --git a/server/api/ProcDecorator/ReqGameLogin.ts b/server/api/ProcDecorator/ReqGameLogin.ts index af22a6c..e32df85 100644 --- a/server/api/ProcDecorator/ReqGameLogin.ts +++ b/server/api/ProcDecorator/ReqGameLogin.ts @@ -1,39 +1,35 @@ import type { LoginCtx } from "./ReqLogin.js"; import type { SessionCtx } from "./StartSession.js"; -import type { ProcDecoratorGenerator } from "./base.js"; -import type { Request, Response } from "express"; -import { delay } from "../../util/delay.js"; +import type { DecoratorResult, ProcDecoratorGenerator } from "./base.js"; export type GameLoginCtx = { generalID: number; + generalName: string; + gameLoginDate: Date; } export function ReqGameLogin(): ProcDecoratorGenerator { - return async (inCtx) => { - const ctx: Q & Partial = inCtx; - if (ctx.generalID !== undefined) { - return [{ - result: true, - type: 'GameLogin', - info: 'UseSession', - }, { - generalID: ctx.generalID, - ...inCtx, - }, ]; + return async (ctx) => { + //NOTE: 게임 서버별로 gameLoginCtx를 따로 가져야 하는가? + let gameLoginCtx = ctx.session.getItem(`gameLoginCtx`); + if(gameLoginCtx){ + if(gameLoginCtx.gameLoginDate >= ctx.loginDate){ + return [{ + result: true, + },{ + ...gameLoginCtx, + ...ctx, + }]; + } + gameLoginCtx = undefined; } - console.log('Something GameLogin'); - await delay(0); - - inCtx.setValue('generalID', ctx.userID * 4); + //TODO: DB에서 generalID를 가져오는 로직 return [{ - result: true, - type: 'GameLogin', - info: 'UseDB', - }, { - generalID: ctx.userID * 4, - ...inCtx, - }, ] + result: false, + type: 'Required GameLogin', + info: 'NotYetImplemented' + }, ctx]; } } \ No newline at end of file diff --git a/server/api/ProcDecorator/ReqLogin.ts b/server/api/ProcDecorator/ReqLogin.ts index 8ca25ed..f8bbf71 100644 --- a/server/api/ProcDecorator/ReqLogin.ts +++ b/server/api/ProcDecorator/ReqLogin.ts @@ -1,15 +1,18 @@ import type { SessionCtx } from "./StartSession.js"; -import type { DecoratorResultFalse, DecoratorResultTrue, ProcDecorator, ProcDecoratorGenerator } from "./base.js"; +import type { ProcDecorator } from "./base.js"; export type LoginCtx = { userID: number; + userName: string; + userLevel: number; + allowServerAction: Record; + loginDate: Date; } export function ReqLogin(): ProcDecorator { return (ctx) => { - const userID = ctx.userID; - - if (userID === undefined) { + const loginCtx = ctx.session.getItem('loginCtx'); + if(!loginCtx){ return [{ result: false, type: 'Required Login', @@ -17,7 +20,11 @@ export function ReqLogin(): ProcDecorator }, ctx]; } - return [{ result: true }, ctx as Q & LoginCtx]; - + return [{ + result: true, + },{ + ...loginCtx, + ...ctx, + }]; } } \ No newline at end of file diff --git a/server/api/ProcDecorator/StartSession.ts b/server/api/ProcDecorator/StartSession.ts index 081e30d..486fa13 100644 --- a/server/api/ProcDecorator/StartSession.ts +++ b/server/api/ProcDecorator/StartSession.ts @@ -1,39 +1,58 @@ +import { type Session, type SessionData } from "express-session"; import type { Empty, ProcDecoratorGenerator } from "./base.js"; export type SessionCtx = { - clearSession: () => Promise; - deleteValue: (key: string) => Promise; - getValue: (key: string) => Promise; - setValue: (key: string, value: unknown) => Promise; - sessionRaw: { - raw: object; - changed: boolean; - }; + session: { + clear: () => Promise; + removeItem: (key: string) => boolean; + getItem: (key: string) => T | undefined; + setItem: (key: string, value: unknown) => void; + raw: Session & Partial & Record; + } } export function StartSession(): ProcDecoratorGenerator { - return (inCtx) => { - const raw: Record = {}; - return [{ result: true }, { - clearSession: () => Promise.resolve(), - deleteValue: async (key: string) => { - console.log('deleteValue', key); - if (raw[key] !== undefined) { - delete raw[key]; - } + return (inCtx, req) => { + if (!req.session) { + throw 'Express-session required'; + } + const sessionObj = { + raw: req.session + } as SessionCtx['session']; + sessionObj.clear = () => { + return new Promise((resolve) => { + sessionObj.raw = req.session.regenerate(resolve) as SessionCtx['session']['raw']; + }) + } + sessionObj.removeItem = (key: string): boolean => { + if (key in sessionObj.raw) { + delete sessionObj.raw[key]; + return true; + } + return false; + } + sessionObj.getItem = (key: string): T | undefined => { + if (!(key in sessionObj.raw)) { + return undefined; + } + return sessionObj.raw[key] as T; + } + sessionObj.setItem = (key: string, value: T | undefined): void => { + if (value === undefined) { + sessionObj.removeItem(key); + return; + } + sessionObj.raw[key] = value; + } + + return [ + { + result: true, }, - getValue: async (key: string) => { - return raw[key] as T | undefined; - }, - setValue: async (key: string, value: unknown) => { - console.log('setValue', key, value); - raw[key] = value; - }, - sessionRaw: { - raw, - changed: false, - }, - ...inCtx, - }]; + { + session: sessionObj, + ...inCtx, + } + ] } } \ No newline at end of file diff --git a/server/api/ProcDecorator/base.ts b/server/api/ProcDecorator/base.ts index eb8bb7e..658cd63 100644 --- a/server/api/ProcDecorator/base.ts +++ b/server/api/ProcDecorator/base.ts @@ -16,10 +16,9 @@ export type DecoratorResultFalse = { export type DecoratorResult = DecoratorResultTrue | DecoratorResultFalse; export type DecoratorStack = DecoratorResult[]; -export interface ProcDecorator> { +export interface ProcDecorator { (inCtx: In & Partial, req: Request, res: Response) - : MayBePromise<[DecoratorResultTrue, Out]> - | MayBePromise<[DecoratorResultFalse, In & Partial]>; + : MayBePromise<[DecoratorResultTrue, Out] | [DecoratorResultFalse, In & Partial]>; } export interface PostProcDecorator { @@ -50,12 +49,15 @@ export type ResolveChain = T extends undefined ? Empty : T extends ProcDecora // eslint-disable-next-line @typescript-eslint/no-explicit-any -export type ParseInType = T extends ProcDecorator ? A : never; +export type ParseInType = T extends ProcDecorator ? object extends A ? A : never : never; // eslint-disable-next-line @typescript-eslint/no-explicit-any -export type ParseOutType = T extends ProcDecorator ? B : never; +export type ParseOutType = T extends ProcDecorator ? B : never; -export function declProcDecorators(decorators: T) { - type InType = ParseInType>; +export const EmptyProcDecorator: readonly [ProcDecoratorRunner, PostProcDecoratorRunner] = [ + async (ctx) => [[], ctx], async (ctx, stack) => [stack, ctx] +]; + +export function declProcDecorators(...decorators: T) { type OutType = ParseOutType>; const preDecorator: PlainDecorator[] = []; @@ -74,7 +76,7 @@ export function declProcDecorators(decorators: T) } } - const packedDecorators: readonly [ProcDecoratorRunner, PostProcDecoratorRunner] = [ + const packedDecorators: readonly [ProcDecoratorRunner, PostProcDecoratorRunner] = [ async (ctx, req, res) => { let rctx = ctx as unknown as OutType; const decoratorStack: DecoratorStack = []; @@ -111,35 +113,47 @@ export function declProcDecorators(decorators: T) return [decoratorStack, rctx]; }, async (ctx, stack, req, res) => { - if (!postDecorator) { + if (!postDecorator.length) { return [stack, ctx]; } - const isValidRoute = stack.length === postDecorator.length && stack.every(v => v.result); - while (stack.length > 0) { - const preStackResult = stack.pop() as DecoratorResult; - const idx = stack.length; + let isValidRoute = stack.length === postDecorator.length && stack.every(v => v.result); + for (let idx = stack.length - 1; idx >= 0; idx--) { + const preStackResult = stack[idx] as DecoratorResult; + if (!preStackResult.result) { + continue; + } + const proc = postDecorator[idx]; if (!proc) { + if (isValidRoute) { + stack.pop(); + } continue; } try { const [postStackResult, nextCtx] = await proc(ctx, preStackResult, req, res, isValidRoute); - if (!postStackResult.result) { - stack.push(postStackResult); - return [stack, nextCtx]; + if (postStackResult.result) { + if (isValidRoute) { + stack.pop(); + } + ctx = nextCtx; + continue; } + isValidRoute = false; + stack[idx] = postStackResult; + ctx = nextCtx; } catch (e) { - stack.push({ + isValidRoute = false; + stack[idx] = { result: false, type: 'PostThrow', info: `internal error: ${e}`, - }); - return [stack, ctx]; + }; } } @@ -150,7 +164,7 @@ export function declProcDecorators(decorators: T) return packedDecorators; } -type Compose2 = D & B extends A & C ? B extends C ? ProcDecorator : never : never; +type Compose2 = B extends C ? ProcDecorator : never; type PD1 = () => ProcDecorator; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -166,4 +180,4 @@ export type PackChain = T extends readonly [PD2, PD2, ... infer R] ? PackChain<[() => Compose2, ...R]> : never; -type Resolve = T extends ProcDecorator ? Empty extends A ? B : never : never; +type Resolve = T extends ProcDecorator ? Empty extends A ? B : never : never; \ No newline at end of file diff --git a/server/api/defs.ts b/server/api/defs.ts index 600dfe9..9d6bb01 100644 --- a/server/api/defs.ts +++ b/server/api/defs.ts @@ -1,13 +1,8 @@ import type { Request, Response } from 'express'; import type { Callable, DefAPINamespace, DeleteAPICallT, GetAPICallT, HeadAPICallT, HttpMethod, InvalidResponse, PatchAPICallT, PostAPICallT, PutAPICallT, RawArgType, ValidResponse, recoveryMethod } from '../apiStructure/defs.js'; -import { - validate, - type ValidatorOptions, -} from "class-validator"; -import { type ClassTransformOptions, plainToInstance } from "class-transformer"; -import type { Empty, ParseOutType, PostProcDecoratorRunner, ProcDecorator, ProcDecoratorChain, ProcDecoratorRunner, ResolveChain } from './ProcDecorator/base.js'; -import { clamp } from 'lodash-es'; +import type { Empty, PostProcDecoratorRunner, ProcDecoratorRunner } from './ProcDecorator/base.js'; +import type { ZodType } from 'zod'; export type APINamespace = { [key: string]: APINamespace @@ -26,7 +21,7 @@ export type APINamespace = { ; } -type ValidatorType = T extends undefined ? undefined : ClassType; +type ValidatorType = T extends undefined ? undefined : ZodType; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type AnyAPIExecuter = APIExecuter; diff --git a/server/api/generator.ts b/server/api/generator.ts index 9bcca75..4dab34c 100644 --- a/server/api/generator.ts +++ b/server/api/generator.ts @@ -1,53 +1,48 @@ 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 { DecoratorResult, Empty, ProcDecorator, ProcDecoratorChain, ResolveChain } from './ProcDecorator/base.js'; -import clamp from 'lodash-es/clamp.js'; +import type { AnyAPIExecuter, APINamespace } from './defs.js'; +import type { RawArgType } from '../apiStructure/defs.js'; +import { z } from 'zod'; -async function parseParam>(req: Request, argValidator?: ClassType): Promise { +async function parseParam>(req: Request, argValidator?: z.ZodType): Promise> { if (!argValidator) { - return req.query as Q; + return { + success: true, + data: 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; + return await argValidator.safeParseAsync(query); } -async function parseBody>(req: Request, argValidator?: ClassType): Promise { +async function parseBody>(req: Request, argValidator?: z.ZodType): Promise> { if (!argValidator) { - return req.body as Q; + return { + success: true, + data: 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; + return await argValidator.safeParseAsync(query); } async function apiRun(query: object, req: Request, res: Response, api: AnyAPIExecuter): Promise { const [preResult, ctx] = await api.preDecorator({}, req, res); if (!preResult.every((v) => v.result)) { - const lastErr = preResult[preResult.length - 1]; + const lastErr = preResult.pop() as typeof preResult[0]; + const [postResult,] = await api.postDecorator(ctx, preResult, req, res, false); - if (!postResult.every((v) => v.result)) { + const postErrors = postResult.filter((obj)=>!obj.result); + if (postErrors.length) { //회수조차 불가능? - const lastPostErr = postResult[postResult.length - 1]; + const postErrorsStr = postErrors.map((obj)=>{ + return `(${obj.type})${obj.info}` + }).join(', '); res.json({ result: false, path: req.path, - reason: `preDecorator: ${lastErr.type} ${lastErr.info}, postDecorator: ${lastPostErr.type} ${lastPostErr.info}`, + reason: `preDecorator: (${lastErr.type})${lastErr.info}, postDecorator: ${postErrorsStr}`, }); return; } @@ -55,22 +50,25 @@ async function apiRun(query: object, req: Request, res: Response, api: AnyAPIExe res.json({ result: false, path: req.path, - reason: `preDecorator: ${lastErr.type} ${lastErr.info}`, + reason: `preDecorator: (${lastErr.type})${lastErr.info}`, }) return; } const result = await api(query, ctx, req, res); const [postResult,] = await api.postDecorator(ctx, preResult, req, res, true); - if(!postResult.every((v) => v.result)) { + const postErrors = postResult.filter((obj)=>!obj.result); + if(postErrors.length) { if(result === true) { //NOTE: 이미 api에서 response를 보낸 특이 케이스. return; } - const lastPostErr = postResult[postResult.length - 1]; + const postErrorsStr = postErrors.map((obj)=>{ + return `(${obj.type})${obj.info}` + }).join(', '); res.json({ result: false, path: req.path, - reason: `postDecorator: ${lastPostErr.type} ${lastPostErr.info}`, + reason: `postDecorator: ${postErrorsStr}`, originalResult: result, }); return; @@ -89,8 +87,10 @@ export function buildAPISystem } for (const [key, value] of Object.entries(api)) { + const rkey = `/${key}`; if (typeof value !== 'function') { - router.use(key, buildAPISystem(value)); + router.use(rkey, buildAPISystem(value)); + continue; } const executer = value; @@ -99,40 +99,20 @@ export function buildAPISystem 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; - } + const parser = executer.httpMethod === 'get' ? parseParam : parseBody; + router[executer.httpMethod](rkey, async (req, res) => { + const queryResult = await parser(req, executer.argValidator); + if(!queryResult.success){ + res.json({ + result: false, + reason: `invalid parameter: ${queryResult.error.message}`, + error: queryResult.error + }); + 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); - }); - } + await apiRun(queryResult.data, req, res, executer); + }); } return router; -} +} \ No newline at end of file