From 2b27b8b427ef4aecf25ca8e4edeae497b0058396 Mon Sep 17 00:00:00 2001 From: Hide_D Date: Sun, 6 Aug 2023 05:49:25 +0000 Subject: [PATCH] =?UTF-8?q?api=20=EA=B5=AC=EC=84=B1=20=EC=A4=80=EB=B9=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/api/base.ts | 80 ++++++++++ server/api/clientAPI.ts | 211 +++++++++++++++++++++++++ server/api/serverAPI.ts | 28 ++++ server/api/{index.ts => serverImpl.ts} | 0 server/dotenv.d.ts | 3 + 5 files changed, 322 insertions(+) create mode 100644 server/api/base.ts create mode 100644 server/api/clientAPI.ts create mode 100644 server/api/serverAPI.ts rename server/api/{index.ts => serverImpl.ts} (100%) diff --git a/server/api/base.ts b/server/api/base.ts new file mode 100644 index 0000000..8723d5e --- /dev/null +++ b/server/api/base.ts @@ -0,0 +1,80 @@ + +import type { Request, Response } from 'express'; + +export interface ValidResponse { + result: true; +} + +export type recoveryMethod = 'refreshEntirePage' | 'retryAPI' | 'gameLogin' | 'gatewayLogin' | 'gatewayPIN'; + +export interface InvalidResponse { + result: false; + reason: string; + recovery?: recoveryMethod; +} + +export const treatedSpecial = Symbol('treatedSpecial'); +export type HttpMethod = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head'; +export type RawArgType = Record | Record[] | undefined; + +export type APINamespace = { + [key: string]: APINamespace + | GET + | POST + | PUT + | DELETE + | PATCH + | HEAD + ; +} + +export abstract class APIExecuter{ + readonly reqType: HttpMethod | HttpMethod[]; + 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 === treatedSpecial) { + return; + } + expressRes.json(result); + } +} + +export abstract class APIBodyParseExecuter extends APIExecuter{ + protected async parseQuery(expressReq: Request): Promise { + throw new Error('Not implemented'); + return expressReq.body; + } +} + +export abstract class GET extends APIExecuter{ + readonly reqType = 'get'; + protected async parseQuery(expressReq: Request): Promise { + expressReq.query; + throw new Error('Not implemented'); + return expressReq.query as unknown 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'; +} \ No newline at end of file diff --git a/server/api/clientAPI.ts b/server/api/clientAPI.ts new file mode 100644 index 0000000..84281da --- /dev/null +++ b/server/api/clientAPI.ts @@ -0,0 +1,211 @@ +import ky from 'ky'; +import type { APIExecuter, APINamespace, HttpMethod, InvalidResponse, RawArgType, ValidResponse } from './base.js'; +import isArray from 'lodash-es/isArray.js'; +import isEmpty from 'lodash-es/isEmpty.js'; + +const apiPath = process.env.API_ROOT_PATH ?? process.env.VITE_API_ROOT_PATH ?? '/api'; + +export async function GET( + args?: ArgType +): Promise; +export async function GET( + args: ArgType | undefined, + returnError: false +): Promise; +export async function GET< + ResultType extends ValidResponse, + ErrorType extends InvalidResponse, + ArgType extends undefined = undefined +>(args: ArgType | undefined, returnError: true): Promise; +export async function GET< + ResultType extends ValidResponse, + ErrorType extends InvalidResponse, + ArgType extends undefined = undefined +>(args?: ArgType, returnError = false): Promise { + console.error(`Can't directly call GET. ${args}, ${returnError}. Use auto-generated path API.`); + return callClientAPI("get", [], args, undefined, true); +} + +export async function POST( + args?: ArgType +): Promise; +export async function POST( + args: ArgType | undefined, + returnError: false +): Promise; +export async function POST< + ResultType extends ValidResponse, + ErrorType extends InvalidResponse, + ArgType extends RawArgType = RawArgType +>(args: ArgType | undefined, returnError: true): Promise; +export async function POST< + ResultType extends ValidResponse, + ErrorType extends InvalidResponse, + ArgType extends RawArgType = RawArgType +>(args?: ArgType, returnError = false): Promise { + console.error(`Can't directly call POST. ${args}, ${returnError}. Use auto-generated path API.`); + return callClientAPI("post", [], args, undefined, true); +} + +export async function PUT( + args?: ArgType +): Promise; +export async function PUT( + args: ArgType | undefined, + returnError: false +): Promise; +export async function PUT< + ResultType extends ValidResponse, + ErrorType extends InvalidResponse, + ArgType extends RawArgType = RawArgType +>(args: ArgType | undefined, returnError: true): Promise; +export async function PUT< + ResultType extends ValidResponse, + ErrorType extends InvalidResponse, + ArgType extends RawArgType = RawArgType +>(args?: ArgType, returnError = false): Promise { + console.error(`Can't directly call PUT. ${args}, ${returnError}. Use auto-generated path API.`); + return callClientAPI("put", [], args, undefined, true); +} + +export async function PATCH( + args?: ArgType +): Promise; +export async function PATCH( + args: ArgType | undefined, + returnError: false +): Promise; +export async function PATCH< + ResultType extends ValidResponse, + ErrorType extends InvalidResponse, + ArgType extends RawArgType = RawArgType +>(args: ArgType | undefined, returnError: true): Promise; +export async function PATCH< + ResultType extends ValidResponse, + ErrorType extends InvalidResponse, + ArgType extends RawArgType = RawArgType +>(args?: ArgType, returnError = false): Promise { + console.error(`Can't directly call PATCH. ${args}, ${returnError}. Use auto-generated path API.`); + return callClientAPI("patch", [], args, undefined, true); +} + +export async function HEAD( + args?: ArgType +): Promise; +export async function HEAD( + args: ArgType | undefined, + returnError: false +): Promise; +export async function HEAD< + ResultType extends ValidResponse, + ErrorType extends InvalidResponse, + ArgType extends undefined = undefined +>(args: ArgType | undefined, returnError: true): Promise; +export async function HEAD< + ResultType extends ValidResponse, + ErrorType extends InvalidResponse, + ArgType extends undefined = undefined +>(args?: ArgType, returnError = false): Promise { + console.error(`Can't directly call HEAD. ${args}, ${returnError}. Use auto-generated path API.`); + return callClientAPI("head", [], args, undefined, true); +} + +export async function DELETE( + args?: ArgType +): Promise; +export async function DELETE( + args: ArgType | undefined, + returnError: false +): Promise; +export async function DELETE< + ResultType extends ValidResponse, + ErrorType extends InvalidResponse, + ArgType extends RawArgType = RawArgType +>(args: ArgType | undefined, returnError: true): Promise; +export async function DELETE< + ResultType extends ValidResponse, + ErrorType extends InvalidResponse, + ArgType extends RawArgType = RawArgType +>(args?: ArgType, returnError = false): Promise { + console.error(`Can't directly call DELETE. ${args}, ${returnError}. Use auto-generated path API.`); + return callClientAPI("patch", [], args, undefined, true); +} + + +export async function callClientAPI( + method: HttpMethod, + path: string | string[], + args: RawArgType, + paramArgs: Record | undefined +): Promise; +export async function callClientAPI( + method: HttpMethod, + path: string | string[], + args: RawArgType, + paramArgs: Record | undefined, + returnError: false +): Promise; +export async function callClientAPI( + method: HttpMethod, + path: string | string[], + args: RawArgType, + paramArgs: Record | undefined, + returnError: true +): Promise; +export async function callClientAPI( + method: HttpMethod, + path: string | string[], + args: RawArgType, + paramArgs: Record | undefined, + returnError = false +): Promise { + if (isArray(path)) { + path = path.join("/"); + } + + if (args && isEmpty(args)) { + args = undefined; + } + + const result = (await (() => { + if (method == "get") { + return ky(apiPath, { + searchParams: { + ...paramArgs, + ...(args as typeof paramArgs), + path, + }, + method, + headers: { + "content-type": "application/json", + }, + timeout: 30000, + retry: 0, + }); + } + return ky(apiPath, { + searchParams: { + ...paramArgs, + path, + }, + method, + json: args, + headers: { + "content-type": "application/json", + }, + timeout: 30000, + retry: 0, + }); + })().json()) as ErrorType | ResultType; + + if (!result.result) { + if (returnError) { + return result; + } + throw result.reason; + } + return result; +} + +//TODO: type이 하나 만들어져야 함. 이것은 typescript의 type system으로. +//TODO: const로 실제 구조가 하나 더 만들어 져야 함. 이것은 위의 type을 satisfy하여야 함. diff --git a/server/api/serverAPI.ts b/server/api/serverAPI.ts new file mode 100644 index 0000000..9752086 --- /dev/null +++ b/server/api/serverAPI.ts @@ -0,0 +1,28 @@ +import { Router } from 'express'; +import type { APIExecuter, APINamespace } from './base.js'; + +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){ + throw new Error('APIExecuter.reqType is not defined'); + } + if(!Array.isArray(executer.reqType)){ + router[executer.reqType](key, async (req, res) => { + await executer.run(req, res); + }); + continue; + } + for(const type of new Set(executer.reqType)){ + router[type](key, async (req, res) => { + await executer.run(req, res); + }); + } + } else { + router.use(key, buildAPISystem(value)); + } + } + return router; +} diff --git a/server/api/index.ts b/server/api/serverImpl.ts similarity index 100% rename from server/api/index.ts rename to server/api/serverImpl.ts diff --git a/server/dotenv.d.ts b/server/dotenv.d.ts index c667cb6..5a28cec 100644 --- a/server/dotenv.d.ts +++ b/server/dotenv.d.ts @@ -8,5 +8,8 @@ declare namespace NodeJS { GAME_DB_PASSWORD: string; SERVER_PORT: string; //숫자 SESSION_SECRET: string; //길게 + + API_ROOT_PATH?: string; + VITE_API_ROOT_PATH?: string; } } \ No newline at end of file