refac: api 시스템 교체

- procDecorator의 타입 복잡도를 낮춤
- 타입 에러가 살짝 더 깔끔함
This commit is contained in:
2023-08-09 16:10:53 +00:00
parent 0c511d27c5
commit 81d16aaba2
12 changed files with 238 additions and 266 deletions
+6 -5
View File
@@ -1,16 +1,17 @@
import { GET } from "../defs"; import { GET } from "../defs";
import type { structure } from "../../apiStructure/sammoRootAPI"; import type { structure } from "../../apiStructure/sammoRootAPI";
import type { ExtractError, ExtractQuery, ExtractResponse } from "../../apiStructure/defs"; import type { ExtractError, ExtractQuery, ExtractResponse } from "../../apiStructure/defs";
import { declProcDecorators } from "../ProcDecorator/base";
type BaseAPI = typeof structure.Login.ReqNonce; type BaseAPI = typeof structure.Login.ReqNonce;
type RType = ExtractResponse<BaseAPI>; type RType = ExtractResponse<BaseAPI>;
type EType = ExtractError<BaseAPI>; type EType = ExtractError<BaseAPI>;
type QType = ExtractQuery<BaseAPI>; type QType = ExtractQuery<BaseAPI>;
const argValidator = undefined; const argValidator = undefined;
const procDecorator = [] as const;
export const ReqNonce = GET<RType, EType, QType>(argValidator)(procDecorator)( export const ReqNonce = GET<RType, EType, QType>
(query, ctx, req, res) => { (argValidator)
(declProcDecorators([] as const))
(() => {
throw new Error("Method not implemented."); throw new Error("Method not implemented.");
} });
);
+10 -9
View File
@@ -4,6 +4,7 @@ import type { ExtractError, ExtractQuery, ExtractResponse } from "../../apiStruc
import { StartSession } from "../ProcDecorator/StartSession"; import { StartSession } from "../ProcDecorator/StartSession";
import { IsString } from "class-validator"; import { IsString } from "class-validator";
import { delay } from "../../util/delay"; import { delay } from "../../util/delay";
import { declProcDecorators } from "../ProcDecorator/base";
type BaseAPI = typeof structure.Login.LoginByID; type BaseAPI = typeof structure.Login.LoginByID;
type RType = ExtractResponse<BaseAPI>; type RType = ExtractResponse<BaseAPI>;
@@ -15,19 +16,20 @@ class ArgValidator implements QType {
@IsString() @IsString()
password!: string; password!: string;
} }
const procDecorator = [
StartSession,
] as const;
export const LoginByID = POST<RType, EType, QType>(ArgValidator)(procDecorator)( export const LoginByID = POST<RType, EType, QType>
async (query, ctx, req, res) => { (ArgValidator)
(declProcDecorators([
StartSession,
] as const))
(async (query, ctx, req, res) => {
const username = query.username; const username = query.username;
const password = query.password; const password = query.password;
//TODO: DB에서 뭔가 가져와야 함 //TODO: DB에서 뭔가 가져와야 함
await delay(1); await delay(1);
if(Math.random() < 0.3){ if (Math.random() < 0.3) {
return { return {
result: false, result: false,
reason: "로그인 실패", reason: "로그인 실패",
@@ -35,7 +37,7 @@ export const LoginByID = POST<RType, EType, QType>(ArgValidator)(procDecorator)(
} }
} }
if(Math.random() < 0.5){ if (Math.random() < 0.5) {
return { return {
result: false, result: false,
reason: "OTP 인증 필요", reason: "OTP 인증 필요",
@@ -51,5 +53,4 @@ export const LoginByID = POST<RType, EType, QType>(ArgValidator)(procDecorator)(
result: true, result: true,
nextToken, nextToken,
} }
} });
);
+9 -7
View File
@@ -4,22 +4,25 @@ import type { ExtractError, ExtractQuery, ExtractResponse } from "../../apiStruc
import { StartSession } from "../ProcDecorator/StartSession"; import { StartSession } from "../ProcDecorator/StartSession";
import { IsNumber, IsString } from "class-validator"; import { IsNumber, IsString } from "class-validator";
import { delay } from "../../util/delay"; import { delay } from "../../util/delay";
import { PackChain, ParseInType, ParseOutType, declProcDecorators } from "../ProcDecorator/base";
type BaseAPI = typeof structure.Login.LoginByToken; type BaseAPI = typeof structure.Login.LoginByToken;
type RType = ExtractResponse<BaseAPI>; type RType = ExtractResponse<BaseAPI>;
type EType = ExtractError<BaseAPI>; type EType = ExtractError<BaseAPI>;
type QType = ExtractQuery<BaseAPI>; type QType = ExtractQuery<BaseAPI>;
class ArgValidator implements QType { class ArgValidator implements QType {
@IsNumber() @IsNumber()
token_id!: number; token_id!: number;
@IsString() @IsString()
hashedToken!: string; hashedToken!: string;
} }
const procDecorator = [
StartSession,
] as const;
export const LoginByToken = POST<RType, EType, QType>(ArgValidator)(procDecorator)( export const LoginByToken = POST<RType, EType, QType>
async (query, ctx) => { (ArgValidator)
(declProcDecorators([
StartSession,
] as const))
(async (query, ctx) => {
query.hashedToken; query.hashedToken;
ctx.clearSession(); ctx.clearSession();
@@ -38,5 +41,4 @@ export const LoginByToken = POST<RType, EType, QType>(ArgValidator)(procDecorato
nextToken, nextToken,
} }
} });
);
+11 -14
View File
@@ -1,21 +1,22 @@
import { type APINamespaceType, GET as GET } from "../defs"; import { GET } from "../defs";
import type { structure } from "../../apiStructure/sammoRootAPI"; import type { structure } from "../../apiStructure/sammoRootAPI";
import type { DefAPINamespace, ExtractError, ExtractQuery, ExtractResponse } from "../../apiStructure/defs"; import type { ExtractError, ExtractQuery, ExtractResponse } from "../../apiStructure/defs";
import { StartSession } from "../ProcDecorator/StartSession"; import { StartSession } from "../ProcDecorator/StartSession";
import { declProcDecorators } from "../ProcDecorator/base";
type BaseAPI = typeof structure.Login.ReqNonce; type BaseAPI = typeof structure.Login.ReqNonce;
type RType = ExtractResponse<BaseAPI>; type RType = ExtractResponse<BaseAPI>;
type EType = ExtractError<BaseAPI>; type EType = ExtractError<BaseAPI>;
type QType = ExtractQuery<BaseAPI>; type QType = ExtractQuery<BaseAPI>;
const argValidator = undefined;
const procDecorator = [
StartSession,
] as const;
export const ReqNonce = GET<RType, EType, QType>(argValidator)(procDecorator)( export const ReqNonce = GET<RType, EType, QType>
async (query, ctx) => { (undefined)
(declProcDecorators([
StartSession,
] as const))
(async (query, ctx) => {
const nonce = await ctx.getValue<string>("nonce"); const nonce = await ctx.getValue<string>("nonce");
if(nonce !== undefined){ if (nonce !== undefined) {
return { return {
loginNonce: nonce, loginNonce: nonce,
result: true, result: true,
@@ -28,8 +29,4 @@ export const ReqNonce = GET<RType, EType, QType>(argValidator)(procDecorator)(
loginNonce: newNonce, loginNonce: newNonce,
result: true, result: true,
} }
} });
);
type BaseAPI2 = APINamespaceType<typeof structure.Login>['ReqNonce'];
type A = typeof ReqNonce;
+6 -4
View File
@@ -1,14 +1,16 @@
import { GET } from "../defs"; import { GET } from "../defs";
import type { structure } from "../../apiStructure/sammoRootAPI"; import type { structure } from "../../apiStructure/sammoRootAPI";
import type { ExtractError, ExtractQuery, ExtractResponse } from "../../apiStructure/defs"; import type { ExtractError, ExtractQuery, ExtractResponse } from "../../apiStructure/defs";
import { declProcDecorators } from "../ProcDecorator/base";
type BaseAPI = typeof structure.Login.test; type BaseAPI = typeof structure.Login.test;
type RType = ExtractResponse<BaseAPI>; type RType = ExtractResponse<BaseAPI>;
type EType = ExtractError<BaseAPI>; type EType = ExtractError<BaseAPI>;
type QType = ExtractQuery<BaseAPI>; type QType = ExtractQuery<BaseAPI>;
export const test = GET<RType, EType, QType>(undefined)(undefined)( export const test = GET<RType, EType, QType>
(query, ctx, req, res) => { (undefined)
(declProcDecorators([] as const))
(() => {
throw new Error("Method not implemented."); throw new Error("Method not implemented.");
} });
);
+7 -5
View File
@@ -9,12 +9,14 @@ export interface UserLevelCtx {
} }
export function ParseUserLevel<Q extends LoginCtx & SessionCtx>(): ProcDecoratorGenerator<UserLevelCtx, Q> { export function ParseUserLevel<Q extends LoginCtx & SessionCtx>(): ProcDecoratorGenerator<UserLevelCtx, Q> {
return (inCtx) => { return (ctx) => {
const userLevel = 123; const userLevel = 123;
inCtx.setValue('userLevel', userLevel); ctx.setValue('userLevel', userLevel);
return { return [{
result: true,
}, {
userLevel, userLevel,
...inCtx, ...ctx,
} }];
} }
} }
+12 -4
View File
@@ -12,10 +12,14 @@ export function ReqGameLogin<Q extends LoginCtx & SessionCtx>(): ProcDecoratorGe
return async (inCtx) => { return async (inCtx) => {
const ctx: Q & Partial<GameLoginCtx> = inCtx; const ctx: Q & Partial<GameLoginCtx> = inCtx;
if (ctx.generalID !== undefined) { if (ctx.generalID !== undefined) {
return { return [{
result: true,
type: 'GameLogin',
info: 'UseSession',
}, {
generalID: ctx.generalID, generalID: ctx.generalID,
...inCtx, ...inCtx,
}; }, ];
} }
console.log('Something GameLogin'); console.log('Something GameLogin');
@@ -23,9 +27,13 @@ export function ReqGameLogin<Q extends LoginCtx & SessionCtx>(): ProcDecoratorGe
inCtx.setValue('generalID', ctx.userID * 4); inCtx.setValue('generalID', ctx.userID * 4);
return { return [{
result: true,
type: 'GameLogin',
info: 'UseDB',
}, {
generalID: ctx.userID * 4, generalID: ctx.userID * 4,
...inCtx, ...inCtx,
} }, ]
} }
} }
+9 -9
View File
@@ -1,23 +1,23 @@
import type { SessionCtx } from "./StartSession"; import type { SessionCtx } from "./StartSession";
import type { InvalidProc, ProcDecoratorGenerator } from "./base"; import type { DecoratorResultFalse, DecoratorResultTrue, ProcDecorator, ProcDecoratorGenerator } from "./base";
export type LoginCtx = { export type LoginCtx = {
userID: number; userID: number;
} }
export function ReqLogin<Q extends SessionCtx>(): ProcDecoratorGenerator<LoginCtx, Q> { export function ReqLogin<Q extends SessionCtx>(): ProcDecorator<LoginCtx & Q, Q> {
return (inCtx) => { return (ctx) => {
const ctx: Q & Partial<LoginCtx> = inCtx;
const userID = ctx.userID; const userID = ctx.userID;
if (userID === undefined) { if (userID === undefined) {
return { return [{
result: false, result: false,
reason: 'Required Login', type: 'Required Login',
invalidProcSymbol: 'ReqLogin' info: 'ReqLogin'
} satisfies InvalidProc; }, ctx];
} }
return ctx as LoginCtx & Q; return [{ result: true }, ctx as Q & LoginCtx];
} }
} }
+2 -2
View File
@@ -14,7 +14,7 @@ export type SessionCtx = {
export function StartSession<Q extends object = Empty>(): ProcDecoratorGenerator<SessionCtx, Q> { export function StartSession<Q extends object = Empty>(): ProcDecoratorGenerator<SessionCtx, Q> {
return (inCtx) => { return (inCtx) => {
const raw: Record<string, unknown> = {}; const raw: Record<string, unknown> = {};
return { return [{ result: true }, {
clearSession: () => Promise.resolve(), clearSession: () => Promise.resolve(),
deleteValue: async (key: string) => { deleteValue: async (key: string) => {
console.log('deleteValue', key); console.log('deleteValue', key);
@@ -34,6 +34,6 @@ export function StartSession<Q extends object = Empty>(): ProcDecoratorGenerator
changed: false, changed: false,
}, },
...inCtx, ...inCtx,
}; }];
} }
} }
+85 -63
View File
@@ -1,131 +1,153 @@
import type { Request, Response } from "express"; import type { Request, Response } from "express";
import type { InvalidResponse } from "../../apiStructure/defs";
type MayBePromise<T> = T | Promise<T>; type MayBePromise<T> = T | Promise<T>;
export type Empty = Record<string, never>; export type Empty = Record<string, never>;
export type DecoratorStack = { export type DecoratorResultTrue = {
result: boolean, result: true;
type: string, type?: string;
info: string, info?: string;
}[]; };
export type DecoratorResultFalse = {
result: false;
type: string;
info: string;
}
export type DecoratorResult = DecoratorResultTrue | DecoratorResultFalse;
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 = Record<string, never>> {
(inCtx: In & Partial<Out>, req: Request, res: Response): MayBePromise<[Out, DecoratorStack]>; (inCtx: In & Partial<Out>, req: Request, res: Response)
: MayBePromise<[DecoratorResultTrue, Out]>
| MayBePromise<[DecoratorResultFalse, In & Partial<Out>]>;
} }
export interface PostProcDecorator<T extends object>{ export interface PostProcDecorator<T extends object> {
(ctx: T, stack: DecoratorStack, req: Request, res: Response, isValidRoute: boolean): MayBePromise<[T, DecoratorStack]>; (ctx: T, preResult: DecoratorResult, req: Request, res: Response, isValidRoute: boolean): MayBePromise<[DecoratorResult, T]>;
} }
export interface ProcDecoratorRunner<Out extends object, In extends object> {
(inCtx: In & Partial<Out>, req: Request, res: Response): MayBePromise<[DecoratorStack, Out]>;
}
export interface PostProcDecoratorRunner<T extends object> {
(ctx: T, preResult: DecoratorStack, req: Request, res: Response, isValidRoute: boolean): MayBePromise<[DecoratorStack, T]>;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type PlainDecorator = ProcDecorator<any, any>; type PlainDecorator = ProcDecorator<any, any>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type PlainPostDecorator = PostProcDecorator<any>; type PlainPostDecorator = PostProcDecorator<any>;
export type ProcDecoratorGenerator<B extends object, A extends object> = ProcDecorator<B & A, A>; export type ProcDecoratorGenerator<B extends object, A extends object> = ProcDecorator<B & A, A>;
export type ProcDecoratorPrePostGenerator<B extends object, A extends object> = [ProcDecorator<B & A, A>, PostProcDecorator<B & A>]; export type ProcDecoratorPrePostGenerator<B extends object, A extends object> = [ProcDecorator<B & A, A>, PostProcDecorator<B & A>];
// eslint-disable-next-line @typescript-eslint/no-explicit-any export type ProcDecoratorChain = readonly ((() => PlainDecorator) | (() => [PlainDecorator, PlainPostDecorator]))[];
export type ProcDecoratorChain = undefined | readonly ((() => PlainDecorator) | (() => [PlainDecorator, PlainPostDecorator]))[];
export type ResolveChain<T> = T extends undefined ? Empty : T extends ProcDecoratorChain ? Resolve<PackChain<T>> : never; export type ResolveChain<T> = T extends undefined ? Empty : T extends ProcDecoratorChain ? Resolve<PackChain<T>> : never;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ParseInType<T> = T extends ProcDecorator<infer B, infer A> ? A : never; // eslint-disable-next-line @typescript-eslint/no-explicit-any
type ParseOutType<T> = T extends ProcDecorator<infer B, infer A> ? B : never; export type ParseInType<T> = T extends ProcDecorator<any, infer A> ? A : never;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ParseOutType<T> = T extends ProcDecorator<infer B, any> ? B : never;
export function declProcDecorators<T extends ProcDecoratorChain>(decorators: T) { export function declProcDecorators<T extends ProcDecoratorChain>(decorators: T) {
type InType = ParseInType<PackChain<T>>; type InType = ParseInType<PackChain<T>>;
type OutType = ParseOutType<PackChain<T>>; type OutType = ParseOutType<PackChain<T>>;
const preDecorator: PlainDecorator[] = []; const preDecorator: PlainDecorator[] = [];
const postDecorator: (PlainDecorator | undefined)[] = []; const postDecorator: (PlainPostDecorator | undefined)[] = [];
if (decorators) { if (decorators) {
for (const procGen of decorators) { for (const procGen of decorators) {
const proc = procGen(); const proc = procGen();
if (Array.isArray(proc)) { if (Array.isArray(proc)) {
preDecorator.push(proc[0]); preDecorator.push(proc[0]);
postDecorator.push(undefined); postDecorator.push(proc[1]);
} }
else { else {
preDecorator.push(proc); preDecorator.push(proc);
postDecorator.push(proc); postDecorator.push(undefined);
} }
} }
} }
const packedDecorators: readonly [ProcDecorator<OutType, InType>, PostProcDecorator<OutType>] = [ const packedDecorators: readonly [ProcDecoratorRunner<OutType, InType>, 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 = [];
if (!preDecorator) { if (!preDecorator) {
return [rctx, decoratorStack]; return [decoratorStack, rctx];
} }
for (const [idx, proc] of preDecorator.entries()) { for (const [idx, proc] of preDecorator.entries()) {
try { try {
const [newCtx, [stackResult]] = await proc(rctx, req, res); const [stackResult, newCtx] = await proc(rctx, req, res);
decoratorStack.push(stackResult); decoratorStack.push(stackResult);
if(stackResult.result){ if (stackResult.result) {
rctx = newCtx; rctx = newCtx;
continue; continue;
} }
return [newCtx, decoratorStack]; return [decoratorStack, newCtx];
} }
catch (e) { catch (e) {
while (decoratorStack.length > idx) {
return [{ decoratorStack.pop();
}
decoratorStack.push({
result: false, result: false,
reason: `internal error[${idx}]: ${e}`, type: 'PreThrow',
invalidProcInfo: [['PreThrow', idx]], info: `internal error: ${e}`,
}, rctx]; });
return [decoratorStack, rctx];
} }
} }
return [rctx, decoratorStack]; return [decoratorStack, rctx];
}, },
async (ctx, stack, req, res) => { async (ctx, stack, req, res) => {
if (!postDecorator) {
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;
const proc = postDecorator[idx];
return [ctx, stack]; if (!proc) {
continue;
}
try {
const [postStackResult, nextCtx] = await proc(ctx, preStackResult, req, res, isValidRoute);
if (!postStackResult.result) {
stack.push(postStackResult);
return [stack, nextCtx];
}
ctx = nextCtx;
}
catch (e) {
stack.push({
result: false,
type: 'PostThrow',
info: `internal error: ${e}`,
});
return [stack, ctx];
}
}
return [stack, ctx];
}, },
] as const; ] as const;
return packedDecorators; return packedDecorators;
return [
async (ctx: InType & Partial<OutType>, req: Request, res: Response) => {
return ctx as unknown as OutType;
/*if (!preDecorator) {
return ctx as Result;
}
for (const [idx, proc] of preDecorator.entries()) {
try {
const result = await proc(ctx, req, res, true) as Result | [InvalidProc, object];
if (Array.isArray(result)) {
const [invalidProc, erroredCtx] = result;
invalidProc.invalidProcInfo.push(['Pre', idx]);
throw [invalidProc, erroredCtx as Partial<Result>]
return ;
}
ctx = result;
}
catch (e) {
return [{
result: false,
reason: `internal error[${idx}]: ${e}`,
invalidProcSymbol: ['preCtx', idx],
}, ctx, idx];
}
}
return ctx as ResolveChain<T>;*/
},
async (ctx, req, res) => ctx
]
} }
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, C> = D & B extends A & C ? B extends C ? ProcDecorator<D & B, A> : never : never;
@@ -134,7 +156,7 @@ 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
type PD2<B extends object, A extends object> = () => [ProcDecorator<B, A>, PostProcDecorator<any>]; type PD2<B extends object, A extends object> = () => [ProcDecorator<B, A>, PostProcDecorator<any>];
type PackChain<T> = export type PackChain<T> =
T extends readonly [] ? ProcDecorator<Empty, Empty> : T extends readonly [] ? ProcDecorator<Empty, Empty> :
T extends readonly [PD1<infer B, infer A>] ? ProcDecorator<B, A> : T extends readonly [PD1<infer B, infer A>] ? ProcDecorator<B, A> :
T extends readonly [PD2<infer B, infer A>] ? ProcDecorator<B, A> : T extends readonly [PD2<infer B, infer A>] ? ProcDecorator<B, A> :
+52 -61
View File
@@ -6,7 +6,7 @@ import {
type ValidatorOptions, type ValidatorOptions,
} from "class-validator"; } from "class-validator";
import { type ClassTransformOptions, plainToInstance } from "class-transformer"; import { type ClassTransformOptions, plainToInstance } from "class-transformer";
import type { InvalidProc, ProcDecorator, ProcDecoratorChain, ResolveChain } from './ProcDecorator/base.js'; import type { Empty, ParseOutType, PostProcDecoratorRunner, ProcDecorator, ProcDecoratorChain, ProcDecoratorRunner, ResolveChain } from './ProcDecorator/base.js';
import { clamp } from 'lodash-es'; import { clamp } from 'lodash-es';
export type APINamespace = { export type APINamespace = {
@@ -31,59 +31,44 @@ type ValidatorType<T> = T extends undefined ? undefined : ClassType<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>;
interface APIExecuter<R extends ValidResponse, E extends InvalidResponse, Q extends RawArgType, PD extends ProcDecoratorChain> { interface APIExecuter<R extends ValidResponse, E extends InvalidResponse, Q extends RawArgType, CTX extends object> {
(query: Q, ctx: ResolveChain<PD>, expressReq: Request, expressRes: Response): Promise<R | E | true>; (query: Q, ctx: CTX, expressReq: Request, expressRes: Response): Promise<R | E | true>;
httpMethod: HttpMethod; httpMethod: HttpMethod;
argValidator?: ValidatorType<Q>; argValidator?: ValidatorType<Q>;
preDecorator: ProcDecorator<any, any>[]; preDecorator: ProcDecoratorRunner<CTX, Empty>;
postDecorator: (ProcDecorator<any, any> | undefined)[]; postDecorator: PostProcDecoratorRunner<CTX>;
} }
export interface iAPI_GET<R extends ValidResponse, E extends InvalidResponse, Q extends RawArgType, PD extends ProcDecoratorChain> extends APIExecuter<R, E, Q, PD> { export interface iAPI_GET<R extends ValidResponse, E extends InvalidResponse, Q extends RawArgType, CTX extends object> extends APIExecuter<R, E, Q, CTX> {
httpMethod: 'get'; httpMethod: 'get';
} }
export interface iAPI_POST<R extends ValidResponse, E extends InvalidResponse, Q extends RawArgType, PD extends ProcDecoratorChain> extends APIExecuter<R, E, Q, PD> { export interface iAPI_POST<R extends ValidResponse, E extends InvalidResponse, Q extends RawArgType, CTX extends object> extends APIExecuter<R, E, Q, CTX> {
httpMethod: 'post'; httpMethod: 'post';
} }
export interface iAPI_PUT<R extends ValidResponse, E extends InvalidResponse, Q extends RawArgType, PD extends ProcDecoratorChain> extends APIExecuter<R, E, Q, PD> { export interface iAPI_PUT<R extends ValidResponse, E extends InvalidResponse, Q extends RawArgType, CTX extends object> extends APIExecuter<R, E, Q, CTX> {
httpMethod: 'put'; httpMethod: 'put';
} }
export interface iAPI_DELETE<R extends ValidResponse, E extends InvalidResponse, Q extends RawArgType, PD extends ProcDecoratorChain> extends APIExecuter<R, E, Q, PD> { export interface iAPI_DELETE<R extends ValidResponse, E extends InvalidResponse, Q extends RawArgType, CTX extends object> extends APIExecuter<R, E, Q, CTX> {
httpMethod: 'delete'; httpMethod: 'delete';
} }
export interface iAPI_PATCH<R extends ValidResponse, E extends InvalidResponse, Q extends RawArgType, PD extends ProcDecoratorChain> extends APIExecuter<R, E, Q, PD> { export interface iAPI_PATCH<R extends ValidResponse, E extends InvalidResponse, Q extends RawArgType, CTX extends object> extends APIExecuter<R, E, Q, CTX> {
httpMethod: 'patch'; httpMethod: 'patch';
} }
export interface iAPI_HEAD<R extends ValidResponse, E extends InvalidResponse, Q extends RawArgType, PD extends ProcDecoratorChain> extends APIExecuter<R, E, Q, PD> { export interface iAPI_HEAD<R extends ValidResponse, E extends InvalidResponse, Q extends RawArgType, CTX extends object> extends APIExecuter<R, E, Q, CTX> {
httpMethod: 'head'; httpMethod: 'head';
} }
function generateAPI<R extends ValidResponse, E extends InvalidResponse, Q extends RawArgType, PD extends ProcDecoratorChain>(httpMethod: HttpMethod, function generateAPI<R extends ValidResponse, E extends InvalidResponse, Q extends RawArgType, CTX extends object>(httpMethod: HttpMethod,
argValidator: ValidatorType<Q> | undefined, argValidator: ValidatorType<Q> | undefined,
procDecoratorChain: PD, preDecorator: ProcDecoratorRunner<CTX, Empty>,
callback: (query: Q, ctx: ResolveChain<PD>, expressReq: Request, expressRes: Response) => Promise<R | E | true>, postDecorator: PostProcDecoratorRunner<CTX>,
): APIExecuter<R, E, Q, PD> { callback: (query: Q, ctx: CTX, expressReq: Request, expressRes: Response) => Promise<R | E | true>,
const preDecorator: ProcDecorator<any, any>[] = []; ): APIExecuter<R, E, Q, CTX> {
const postDecorator: (ProcDecorator<any, any> | undefined)[] = [];
if (procDecoratorChain) {
for (const procGen of procDecoratorChain) {
const proc = procGen();
if (Array.isArray(proc)) {
preDecorator.push(proc[0]);
postDecorator.push(undefined);
}
else {
preDecorator.push(proc);
postDecorator.push(proc);
}
}
}
return Object.assign( return Object.assign(
callback, callback,
{ {
@@ -96,79 +81,85 @@ function generateAPI<R extends ValidResponse, E extends InvalidResponse, Q exten
} }
export function GET<R extends ValidResponse, E extends InvalidResponse, Q extends RawArgType>(argValidator?: ValidatorType<Q>) { export function GET<R extends ValidResponse, E extends InvalidResponse, Q extends RawArgType>(argValidator?: ValidatorType<Q>) {
return <PD extends ProcDecoratorChain>(procDecoratorChain: PD) => { return <CTX extends object>([preDecorator, postDecorator]: readonly [ProcDecoratorRunner<CTX, Empty>, PostProcDecoratorRunner<CTX>]) => {
return (callback: (query: Q, ctx: ResolveChain<PD>, expressReq: Request, expressRes: Response) => Promise<R | E | true>) => { return (callback: (query: Q, ctx: CTX, expressReq: Request, expressRes: Response) => Promise<R | E | true>) => {
return generateAPI<R, E, Q, PD>( return generateAPI<R, E, Q, CTX>(
'get', 'get',
argValidator, argValidator,
procDecoratorChain, preDecorator,
postDecorator,
callback, callback,
) as iAPI_GET<R, E, Q, PD>; ) as iAPI_GET<R, E, Q, CTX>;
} }
} }
} }
export function POST<R extends ValidResponse, E extends InvalidResponse, Q extends RawArgType>(argValidator?: ValidatorType<Q>) { export function POST<R extends ValidResponse, E extends InvalidResponse, Q extends RawArgType>(argValidator?: ValidatorType<Q>) {
return <PD extends ProcDecoratorChain>(procDecoratorChain: PD) => { return <CTX extends object>([preDecorator, postDecorator]: readonly [ProcDecoratorRunner<CTX, Empty>, PostProcDecoratorRunner<CTX>]) => {
return (callback: (query: Q, ctx: ResolveChain<PD>, expressReq: Request, expressRes: Response) => Promise<R | E | true>) => { return (callback: (query: Q, ctx: CTX, expressReq: Request, expressRes: Response) => Promise<R | E | true>) => {
return generateAPI<R, E, Q, PD>( return generateAPI<R, E, Q, CTX>(
'post', 'post',
argValidator, argValidator,
procDecoratorChain, preDecorator,
postDecorator,
callback, callback,
) as iAPI_POST<R, E, Q, PD>; ) as iAPI_POST<R, E, Q, CTX>;
} }
} }
} }
export function PUT<R extends ValidResponse, E extends InvalidResponse, Q extends RawArgType>(argValidator?: ValidatorType<Q>) { export function PUT<R extends ValidResponse, E extends InvalidResponse, Q extends RawArgType>(argValidator?: ValidatorType<Q>) {
return <PD extends ProcDecoratorChain>(procDecoratorChain: PD) => { return <CTX extends object>([preDecorator, postDecorator]: readonly [ProcDecoratorRunner<CTX, Empty>, PostProcDecoratorRunner<CTX>]) => {
return (callback: (query: Q, ctx: ResolveChain<PD>, expressReq: Request, expressRes: Response) => Promise<R | E | true>) => { return (callback: (query: Q, ctx: CTX, expressReq: Request, expressRes: Response) => Promise<R | E | true>) => {
return generateAPI<R, E, Q, PD>( return generateAPI<R, E, Q, CTX>(
'put', 'put',
argValidator, argValidator,
procDecoratorChain, preDecorator,
postDecorator,
callback, callback,
) as iAPI_PUT<R, E, Q, PD>; ) as iAPI_PUT<R, E, Q, CTX>;
} }
} }
} }
export function DELETE<R extends ValidResponse, E extends InvalidResponse, Q extends RawArgType>(argValidator?: ValidatorType<Q>) { export function DELETE<R extends ValidResponse, E extends InvalidResponse, Q extends RawArgType>(argValidator?: ValidatorType<Q>) {
return <PD extends ProcDecoratorChain>(procDecoratorChain: PD) => { return <CTX extends object>([preDecorator, postDecorator]: readonly [ProcDecoratorRunner<CTX, Empty>, PostProcDecoratorRunner<CTX>]) => {
return (callback: (query: Q, ctx: ResolveChain<PD>, expressReq: Request, expressRes: Response) => Promise<R | E | true>) => { return (callback: (query: Q, ctx: CTX, expressReq: Request, expressRes: Response) => Promise<R | E | true>) => {
return generateAPI<R, E, Q, PD>( return generateAPI<R, E, Q, CTX>(
'delete', 'delete',
argValidator, argValidator,
procDecoratorChain, preDecorator,
postDecorator,
callback, callback,
) as iAPI_DELETE<R, E, Q, PD>; ) as iAPI_DELETE<R, E, Q, CTX>;
} }
} }
} }
export function PATCH<R extends ValidResponse, E extends InvalidResponse, Q extends RawArgType>(argValidator?: ValidatorType<Q>) { export function PATCH<R extends ValidResponse, E extends InvalidResponse, Q extends RawArgType>(argValidator?: ValidatorType<Q>) {
return <PD extends ProcDecoratorChain>(procDecoratorChain: PD) => { return <CTX extends object>([preDecorator, postDecorator]: readonly [ProcDecoratorRunner<CTX, Empty>, PostProcDecoratorRunner<CTX>]) => {
return (callback: (query: Q, ctx: ResolveChain<PD>, expressReq: Request, expressRes: Response) => Promise<R | E | true>) => { return (callback: (query: Q, ctx: CTX, expressReq: Request, expressRes: Response) => Promise<R | E | true>) => {
return generateAPI<R, E, Q, PD>( return generateAPI<R, E, Q, CTX>(
'patch', 'patch',
argValidator, argValidator,
procDecoratorChain, preDecorator,
postDecorator,
callback, callback,
) as iAPI_PATCH<R, E, Q, PD>; ) as iAPI_PATCH<R, E, Q, CTX>;
} }
} }
} }
export function HEAD<R extends ValidResponse, E extends InvalidResponse, Q extends RawArgType>(argValidator?: ValidatorType<Q>) { export function HEAD<R extends ValidResponse, E extends InvalidResponse, Q extends RawArgType>(argValidator?: ValidatorType<Q>) {
return <PD extends ProcDecoratorChain>(procDecoratorChain: PD) => { return <CTX extends object>([preDecorator, postDecorator]: readonly [ProcDecoratorRunner<CTX, Empty>, PostProcDecoratorRunner<CTX>]) => {
return (callback: (query: Q, ctx: ResolveChain<PD>, expressReq: Request, expressRes: Response) => Promise<R | E | true>) => { return (callback: (query: Q, ctx: CTX, expressReq: Request, expressRes: Response) => Promise<R | E | true>) => {
return generateAPI<R, E, Q, PD>( return generateAPI<R, E, Q, CTX>(
'head', 'head',
argValidator, argValidator,
procDecoratorChain, preDecorator,
postDecorator,
callback, callback,
) as iAPI_HEAD<R, E, Q, PD>; ) as iAPI_HEAD<R, E, Q, CTX>;
} }
} }
} }
+29 -83
View File
@@ -3,7 +3,7 @@ import type { AnyAPIExecuter, APINamespace, ClassType } from './defs.js';
import { plainToInstance } from 'class-transformer'; import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator'; import { validate } from 'class-validator';
import type { ExtractQuery, RawArgType } from '../apiStructure/defs.js'; import type { ExtractQuery, RawArgType } from '../apiStructure/defs.js';
import type { Empty, InvalidProc, ProcDecorator, ProcDecoratorChain, ResolveChain } from './ProcDecorator/base.js'; import type { DecoratorResult, Empty, ProcDecorator, ProcDecoratorChain, ResolveChain } from './ProcDecorator/base.js';
import clamp from 'lodash-es/clamp.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?: ClassType<Q>): Promise<Q> {
@@ -36,97 +36,43 @@ async function parseBody<Q extends Exclude<RawArgType, undefined>>(req: Request,
return classObject as Q; return classObject as Q;
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type PlainDecorator = ProcDecorator<any, any>;
async function preCtx<PD extends ProcDecorator<any, any>>(preDecorator: PlainDecorator[], req: Request, res: Response): Promise<ResolveChain<PD> | [InvalidProc, ResolveChain<PD>, number]> {
if (!preDecorator) {
return {} as ResolveChain<PD>;
}
let ctx = {} as ResolveChain<PD>;
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<PD extends ProcDecoratorChain>(ctx: ResolveChain<PD>, 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<void> { async function apiRun(query: object, req: Request, res: Response, api: AnyAPIExecuter): Promise<void> {
const ctx = await preCtx(api.preDecorator, req, res); const [preResult, ctx] = await api.preDecorator({}, req, res);
if (Array.isArray(ctx)) { if (!preResult.every((v) => v.result)) {
const [invalidProc, errCtx, idx] = ctx; const lastErr = preResult[preResult.length - 1];
if (idx == 0) { const [postResult,] = await api.postDecorator(ctx, preResult, req, res, false);
res.json(invalidProc); if (!postResult.every((v) => v.result)) {
//회수조차 불가능?
const lastPostErr = postResult[postResult.length - 1];
res.json({
result: false,
path: req.path,
reason: `preDecorator: ${lastErr.type} ${lastErr.info}, postDecorator: ${lastPostErr.type} ${lastPostErr.info}`,
});
return; return;
} }
const result = await postCtx(errCtx, api.postDecorator, req, res, idx);
if (result === undefined) { res.json({
res.json(invalidProc); result: false,
return; path: req.path,
} reason: `preDecorator: ${lastErr.type} ${lastErr.info}`,
const [postInvalidProc,] = result; })
res.json(postInvalidProc);
return; return;
} }
const result = await api(query, ctx, req, res); const result = await api(query, ctx, req, res);
const postResult = await postCtx(ctx, api.postDecorator, req, res); const [postResult,] = await api.postDecorator(ctx, preResult, req, res, true);
if (postResult !== undefined) { if(!postResult.every((v) => v.result)) {
if (result === true) { if(result === true) {
//NOTE: 이미 api에서 response를 보낸 특이 케이스. //NOTE: 이미 api에서 response를 보낸 특이 케이스.
return; return;
} }
const [invalidProc,] = postResult; const lastPostErr = postResult[postResult.length - 1];
invalidProc.alreadyProcessed = true; res.json({
res.json(invalidProc); result: false,
path: req.path,
reason: `postDecorator: ${lastPostErr.type} ${lastPostErr.info}`,
originalResult: result,
});
return; return;
} }