api 설계 변경

This commit is contained in:
2023-08-18 13:02:40 +00:00
parent 7716c6936b
commit bfe0b99d44
7 changed files with 168 additions and 179 deletions
@@ -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<Q extends LoginCtx & SessionCtx>(): ProcDecoratorGenerator<UserLevelCtx, Q> {
return (ctx) => {
const userLevel = 123;
ctx.setValue('userLevel', userLevel);
return [{
result: true,
}, {
userLevel,
...ctx,
}];
}
}
+21 -25
View File
@@ -1,39 +1,35 @@
import type { LoginCtx } from "./ReqLogin.js"; import type { LoginCtx } from "./ReqLogin.js";
import type { SessionCtx } from "./StartSession.js"; import type { SessionCtx } from "./StartSession.js";
import type { ProcDecoratorGenerator } from "./base.js"; import type { DecoratorResult, ProcDecoratorGenerator } from "./base.js";
import type { Request, Response } from "express";
import { delay } from "../../util/delay.js";
export type GameLoginCtx = { export type GameLoginCtx = {
generalID: number; generalID: number;
generalName: string;
gameLoginDate: Date;
} }
export function ReqGameLogin<Q extends LoginCtx & SessionCtx>(): ProcDecoratorGenerator<GameLoginCtx, Q> { export function ReqGameLogin<Q extends LoginCtx & SessionCtx>(): ProcDecoratorGenerator<GameLoginCtx, Q> {
return async (inCtx) => { return async (ctx) => {
const ctx: Q & Partial<GameLoginCtx> = inCtx; //NOTE: 게임 서버별로 gameLoginCtx를 따로 가져야 하는가?
if (ctx.generalID !== undefined) { let gameLoginCtx = ctx.session.getItem<GameLoginCtx>(`gameLoginCtx`);
return [{ if(gameLoginCtx){
result: true, if(gameLoginCtx.gameLoginDate >= ctx.loginDate){
type: 'GameLogin', return [{
info: 'UseSession', result: true,
}, { },{
generalID: ctx.generalID, ...gameLoginCtx,
...inCtx, ...ctx,
}, ]; }];
}
gameLoginCtx = undefined;
} }
console.log('Something GameLogin'); //TODO: DB에서 generalID를 가져오는 로직
await delay(0);
inCtx.setValue('generalID', ctx.userID * 4);
return [{ return [{
result: true, result: false,
type: 'GameLogin', type: 'Required GameLogin',
info: 'UseDB', info: 'NotYetImplemented'
}, { }, ctx];
generalID: ctx.userID * 4,
...inCtx,
}, ]
} }
} }
+13 -6
View File
@@ -1,15 +1,18 @@
import type { SessionCtx } from "./StartSession.js"; import type { SessionCtx } from "./StartSession.js";
import type { DecoratorResultFalse, DecoratorResultTrue, ProcDecorator, ProcDecoratorGenerator } from "./base.js"; import type { ProcDecorator } from "./base.js";
export type LoginCtx = { export type LoginCtx = {
userID: number; userID: number;
userName: string;
userLevel: number;
allowServerAction: Record<string, number>;
loginDate: Date;
} }
export function ReqLogin<Q extends SessionCtx>(): ProcDecorator<LoginCtx & Q, Q> { export function ReqLogin<Q extends SessionCtx>(): ProcDecorator<LoginCtx & Q, Q> {
return (ctx) => { return (ctx) => {
const userID = ctx.userID; const loginCtx = ctx.session.getItem<LoginCtx>('loginCtx');
if(!loginCtx){
if (userID === undefined) {
return [{ return [{
result: false, result: false,
type: 'Required Login', type: 'Required Login',
@@ -17,7 +20,11 @@ export function ReqLogin<Q extends SessionCtx>(): ProcDecorator<LoginCtx & Q, Q>
}, ctx]; }, ctx];
} }
return [{ result: true }, ctx as Q & LoginCtx]; return [{
result: true,
},{
...loginCtx,
...ctx,
}];
} }
} }
+49 -30
View File
@@ -1,39 +1,58 @@
import { type Session, type SessionData } from "express-session";
import type { Empty, ProcDecoratorGenerator } from "./base.js"; import type { Empty, ProcDecoratorGenerator } from "./base.js";
export type SessionCtx = { export type SessionCtx = {
clearSession: () => Promise<void>; session: {
deleteValue: (key: string) => Promise<void>; clear: () => Promise<void>;
getValue: <T>(key: string) => Promise<T | undefined>; removeItem: (key: string) => boolean;
setValue: (key: string, value: unknown) => Promise<void>; getItem: <T>(key: string) => T | undefined;
sessionRaw: { setItem: (key: string, value: unknown) => void;
raw: object; raw: Session & Partial<SessionData> & Record<string, unknown>;
changed: boolean; }
};
} }
export function StartSession<Q extends object = Empty>(): ProcDecoratorGenerator<SessionCtx, Q> { export function StartSession<Q extends object = Empty>(): ProcDecoratorGenerator<SessionCtx, Q> {
return (inCtx) => { return (inCtx, req) => {
const raw: Record<string, unknown> = {}; if (!req.session) {
return [{ result: true }, { throw 'Express-session required';
clearSession: () => Promise.resolve(), }
deleteValue: async (key: string) => { const sessionObj = {
console.log('deleteValue', key); raw: req.session
if (raw[key] !== undefined) { } as SessionCtx['session'];
delete raw[key]; 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 = <T>(key: string): T | undefined => {
if (!(key in sessionObj.raw)) {
return undefined;
}
return sessionObj.raw[key] as T;
}
sessionObj.setItem = <T>(key: string, value: T | undefined): void => {
if (value === undefined) {
sessionObj.removeItem(key);
return;
}
sessionObj.raw[key] = value;
}
return [
{
result: true,
}, },
getValue: async <T>(key: string) => { {
return raw[key] as T | undefined; session: sessionObj,
}, ...inCtx,
setValue: async (key: string, value: unknown) => { }
console.log('setValue', key, value); ]
raw[key] = value;
},
sessionRaw: {
raw,
changed: false,
},
...inCtx,
}];
} }
} }
+35 -21
View File
@@ -16,10 +16,9 @@ export type DecoratorResultFalse = {
export type DecoratorResult = DecoratorResultTrue | DecoratorResultFalse; export type DecoratorResult = DecoratorResultTrue | DecoratorResultFalse;
export type DecoratorStack = DecoratorResult[]; export type DecoratorStack = DecoratorResult[];
export interface ProcDecorator<Out extends object, In extends object = Record<string, never>> { export interface ProcDecorator<Out extends object, In extends object = Empty> {
(inCtx: In & Partial<Out>, req: Request, res: Response) (inCtx: In & Partial<Out>, req: Request, res: Response)
: MayBePromise<[DecoratorResultTrue, Out]> : MayBePromise<[DecoratorResultTrue, Out] | [DecoratorResultFalse, In & Partial<Out>]>;
| MayBePromise<[DecoratorResultFalse, In & Partial<Out>]>;
} }
export interface PostProcDecorator<T extends object> { export interface PostProcDecorator<T extends object> {
@@ -50,12 +49,15 @@ export type ResolveChain<T> = T extends undefined ? Empty : T extends ProcDecora
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ParseInType<T> = T extends ProcDecorator<any, infer A> ? A : never; export type ParseInType<T> = T extends ProcDecorator<any, infer A> ? object extends A ? A : never : never;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ParseOutType<T> = T extends ProcDecorator<infer B, any> ? B : never; export type ParseOutType<T> = T extends ProcDecorator<infer B, object> ? B : never;
export function declProcDecorators<T extends ProcDecoratorChain>(decorators: T) { export const EmptyProcDecorator: readonly [ProcDecoratorRunner<Empty, Empty>, PostProcDecoratorRunner<Empty>] = [
type InType = ParseInType<PackChain<T>>; async (ctx) => [[], ctx], async (ctx, stack) => [stack, ctx]
];
export function declProcDecorators<T extends ProcDecoratorChain>(...decorators: T) {
type OutType = ParseOutType<PackChain<T>>; type OutType = ParseOutType<PackChain<T>>;
const preDecorator: PlainDecorator[] = []; const preDecorator: PlainDecorator[] = [];
@@ -74,7 +76,7 @@ export function declProcDecorators<T extends ProcDecoratorChain>(decorators: T)
} }
} }
const packedDecorators: readonly [ProcDecoratorRunner<OutType, InType>, PostProcDecoratorRunner<OutType>] = [ const packedDecorators: readonly [ProcDecoratorRunner<OutType, Empty>, PostProcDecoratorRunner<OutType>] = [
async (ctx, req, res) => { async (ctx, req, res) => {
let rctx = ctx as unknown as OutType; let rctx = ctx as unknown as OutType;
const decoratorStack: DecoratorStack = []; const decoratorStack: DecoratorStack = [];
@@ -111,35 +113,47 @@ export function declProcDecorators<T extends ProcDecoratorChain>(decorators: T)
return [decoratorStack, rctx]; return [decoratorStack, rctx];
}, },
async (ctx, stack, req, res) => { async (ctx, stack, req, res) => {
if (!postDecorator) { if (!postDecorator.length) {
return [stack, ctx]; return [stack, ctx];
} }
const isValidRoute = stack.length === postDecorator.length && stack.every(v => v.result); let isValidRoute = stack.length === postDecorator.length && stack.every(v => v.result);
while (stack.length > 0) { for (let idx = stack.length - 1; idx >= 0; idx--) {
const preStackResult = stack.pop() as DecoratorResult; const preStackResult = stack[idx] as DecoratorResult;
const idx = stack.length; if (!preStackResult.result) {
continue;
}
const proc = postDecorator[idx]; const proc = postDecorator[idx];
if (!proc) { if (!proc) {
if (isValidRoute) {
stack.pop();
}
continue; continue;
} }
try { try {
const [postStackResult, nextCtx] = await proc(ctx, preStackResult, req, res, isValidRoute); const [postStackResult, nextCtx] = await proc(ctx, preStackResult, req, res, isValidRoute);
if (!postStackResult.result) { if (postStackResult.result) {
stack.push(postStackResult); if (isValidRoute) {
return [stack, nextCtx]; stack.pop();
}
ctx = nextCtx;
continue;
} }
isValidRoute = false;
stack[idx] = postStackResult;
ctx = nextCtx; ctx = nextCtx;
} }
catch (e) { catch (e) {
stack.push({ isValidRoute = false;
stack[idx] = {
result: false, result: false,
type: 'PostThrow', type: 'PostThrow',
info: `internal error: ${e}`, info: `internal error: ${e}`,
}); };
return [stack, ctx];
} }
} }
@@ -150,7 +164,7 @@ export function declProcDecorators<T extends ProcDecoratorChain>(decorators: T)
return packedDecorators; return packedDecorators;
} }
type Compose2<B extends object, A extends object, D, C> = D & B extends A & C ? B extends C ? ProcDecorator<D & B, A> : never : never; type Compose2<B extends object, A extends object, D extends object, C> = B extends C ? ProcDecorator<D & B, A> : never;
type PD1<B extends object, A extends object> = () => ProcDecorator<B, A>; type PD1<B extends object, A extends object> = () => ProcDecorator<B, A>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -166,4 +180,4 @@ export type PackChain<T> =
T extends readonly [PD2<infer B, infer A>, PD2<infer D, infer C>, ... infer R] ? PackChain<[() => Compose2<B, A, D, C>, ...R]> : T extends readonly [PD2<infer B, infer A>, PD2<infer D, infer C>, ... infer R] ? PackChain<[() => Compose2<B, A, D, C>, ...R]> :
never; never;
type Resolve<T> = T extends ProcDecorator<infer B, infer A> ? Empty extends A ? B : never : never; type Resolve<T> = T extends ProcDecorator<infer B, infer A> ? Empty extends A ? B : never : never;
+3 -8
View File
@@ -1,13 +1,8 @@
import type { Request, Response } from 'express'; 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 type { Callable, DefAPINamespace, DeleteAPICallT, GetAPICallT, HeadAPICallT, HttpMethod, InvalidResponse, PatchAPICallT, PostAPICallT, PutAPICallT, RawArgType, ValidResponse, recoveryMethod } from '../apiStructure/defs.js';
import { import type { Empty, PostProcDecoratorRunner, ProcDecoratorRunner } from './ProcDecorator/base.js';
validate, import type { ZodType } from 'zod';
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';
export type APINamespace = { export type APINamespace = {
[key: string]: APINamespace [key: string]: APINamespace
@@ -26,7 +21,7 @@ export type APINamespace = {
; ;
} }
type ValidatorType<T> = T extends undefined ? undefined : ClassType<T>; type ValidatorType<T> = T extends undefined ? undefined : ZodType<T>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AnyAPIExecuter = APIExecuter<any, any, any, any>; export type AnyAPIExecuter = APIExecuter<any, any, any, any>;
+47 -67
View File
@@ -1,53 +1,48 @@
import { Router, type Request, type Response } from 'express'; import { Router, type Request, type Response } from 'express';
import type { AnyAPIExecuter, APINamespace, ClassType } from './defs.js'; import type { AnyAPIExecuter, APINamespace } from './defs.js';
import { plainToInstance } from 'class-transformer'; import type { RawArgType } from '../apiStructure/defs.js';
import { validate } from 'class-validator'; import { z } from 'zod';
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';
async function parseParam<Q extends Exclude<RawArgType, undefined>>(req: Request, argValidator?: ClassType<Q>): Promise<Q> { async function parseParam<Q extends Exclude<RawArgType, undefined>>(req: Request, argValidator?: z.ZodType<Q>): Promise<z.SafeParseReturnType<Q, Q>> {
if (!argValidator) { if (!argValidator) {
return req.query as Q; return {
success: true,
data: req.query as Q
}
} }
const query = req.query; const query = req.query;
const classObject = plainToInstance(argValidator, query); return await argValidator.safeParseAsync(query);
const errors = await validate(classObject);
if (errors.length) {
throw errors;
}
return classObject as Q;
} }
async function parseBody<Q extends Exclude<RawArgType, undefined>>(req: Request, argValidator?: ClassType<Q>): Promise<Q> { async function parseBody<Q extends Exclude<RawArgType, undefined>>(req: Request, argValidator?: z.ZodType<Q>): Promise<z.SafeParseReturnType<Q, Q>> {
if (!argValidator) { if (!argValidator) {
return req.body as Q; return {
success: true,
data: req.body as Q
}
} }
const query = req.body; const query = req.body;
const classObject = plainToInstance(argValidator, query); return await argValidator.safeParseAsync(query);
const errors = await validate(classObject);
if (errors.length) {
throw errors;
}
return classObject as Q;
} }
async function apiRun(query: object, req: Request, res: Response, api: AnyAPIExecuter): Promise<void> { async function apiRun(query: object, req: Request, res: Response, api: AnyAPIExecuter): Promise<void> {
const [preResult, ctx] = await api.preDecorator({}, req, res); const [preResult, ctx] = await api.preDecorator({}, req, res);
if (!preResult.every((v) => v.result)) { 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); 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({ res.json({
result: false, result: false,
path: req.path, path: req.path,
reason: `preDecorator: ${lastErr.type} ${lastErr.info}, postDecorator: ${lastPostErr.type} ${lastPostErr.info}`, reason: `preDecorator: (${lastErr.type})${lastErr.info}, postDecorator: ${postErrorsStr}`,
}); });
return; return;
} }
@@ -55,22 +50,25 @@ async function apiRun(query: object, req: Request, res: Response, api: AnyAPIExe
res.json({ res.json({
result: false, result: false,
path: req.path, path: req.path,
reason: `preDecorator: ${lastErr.type} ${lastErr.info}`, reason: `preDecorator: (${lastErr.type})${lastErr.info}`,
}) })
return; return;
} }
const result = await api(query, ctx, req, res); const result = await api(query, ctx, req, res);
const [postResult,] = await api.postDecorator(ctx, preResult, req, res, true); 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) { if(result === true) {
//NOTE: 이미 api에서 response를 보낸 특이 케이스. //NOTE: 이미 api에서 response를 보낸 특이 케이스.
return; return;
} }
const lastPostErr = postResult[postResult.length - 1]; const postErrorsStr = postErrors.map((obj)=>{
return `(${obj.type})${obj.info}`
}).join(', ');
res.json({ res.json({
result: false, result: false,
path: req.path, path: req.path,
reason: `postDecorator: ${lastPostErr.type} ${lastPostErr.info}`, reason: `postDecorator: ${postErrorsStr}`,
originalResult: result, originalResult: result,
}); });
return; return;
@@ -89,8 +87,10 @@ export function buildAPISystem<N extends APINamespace, Q extends AnyAPIExecuter>
} }
for (const [key, value] of Object.entries(api)) { for (const [key, value] of Object.entries(api)) {
const rkey = `/${key}`;
if (typeof value !== 'function') { if (typeof value !== 'function') {
router.use(key, buildAPISystem(value)); router.use(rkey, buildAPISystem(value));
continue; continue;
} }
const executer = value; const executer = value;
@@ -99,40 +99,20 @@ export function buildAPISystem<N extends APINamespace, Q extends AnyAPIExecuter>
throw new Error('APIExecuter.reqType is not defined'); throw new Error('APIExecuter.reqType is not defined');
} }
if (executer.httpMethod === 'get') { const parser = executer.httpMethod === 'get' ? parseParam : parseBody;
router[executer.httpMethod](key, async (req, res) => { router[executer.httpMethod](rkey, async (req, res) => {
let query: ExtractQuery<Q>; const queryResult = await parser(req, executer.argValidator);
try { if(!queryResult.success){
query = await parseParam(req, executer.argValidator); res.json({
} result: false,
catch (e) { reason: `invalid parameter: ${queryResult.error.message}`,
res.json({ error: queryResult.error
result: false, });
reason: `invalid parameter: ${e}`, return;
}); }
return;
}
await apiRun(query, req, res, executer); await apiRun(queryResult.data, req, res, executer);
}); });
}
else {
router[executer.httpMethod](key, async (req, res) => {
let query: ExtractQuery<Q>;
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; return router;
} }