@strpc ready

This commit is contained in:
2023-09-22 20:32:41 +09:00
parent 3013554cef
commit 0f9d5c2dc9
19 changed files with 1485 additions and 186 deletions
+27
View File
@@ -0,0 +1,27 @@
{
"name": "@strpc/client_ky",
"version": "1.0.0",
"description": "",
"main": "dist/index.js",
"exports": {
".": "./dist/index.js",
"./types": "./dist/types.js"
},
"dependencies": {
"@strpc/def": "workspace:^"
},
"devDependencies": {
"ky": "^1.0.1",
"lodash-es": "^4.17.21"
},
"scripts": {
"build": "tsc --build"
},
"type": "module",
"author": "",
"license": "ISC",
"peerDependencies": {
"ky": "^1.0.1",
"lodash-es": "^4.17.21"
}
}
+158
View File
@@ -0,0 +1,158 @@
import { isArray, isEmpty } from "lodash-es";
import ky from "ky";
import type { HttpMethod, InvalidResponse, RawArgType, ValidResponse, recoveryMethod } from "@strpc/def";
export type globalRecoveryFunction = (method: HttpMethod, path: string, args: RawArgType, recovery: recoveryMethod, result: InvalidResponse) => Promise<{
justRetryAPI?: boolean;
} | void>;
export class APIFailed extends Error {
constructor(public readonly reason: string, public readonly detail: unknown, public readonly recovery?: recoveryMethod) {
super(reason);
}
}
export async function callClientAPI<ResultType extends ValidResponse>(
method: HttpMethod,
apiRoot: string,
path: string | string[],
args: RawArgType,
globalRecovery: globalRecoveryFunction | undefined
): Promise<ResultType>;
export async function callClientAPI<ResultType extends ValidResponse>(
method: HttpMethod,
apiRoot: string,
path: string | string[],
args: RawArgType,
globalRecovery: globalRecoveryFunction | undefined,
returnError: undefined
): Promise<ResultType>;
export async function callClientAPI<ResultType extends ValidResponse>(
method: HttpMethod,
apiRoot: string,
path: string | string[],
args: RawArgType,
globalRecovery: globalRecoveryFunction | undefined,
returnError: false
): Promise<ResultType>;
export async function callClientAPI<ResultType extends ValidResponse, ErrorType extends InvalidResponse>(
method: HttpMethod,
apiRoot: string,
path: string | string[],
args: RawArgType,
globalRecovery: globalRecoveryFunction | undefined,
returnError: true
): Promise<ResultType | ErrorType>;
export async function callClientAPI<ResultType extends ValidResponse, ErrorType extends InvalidResponse>(
method: HttpMethod,
apiRoot: string,
path: string | string[],
args: RawArgType,
globalRecovery: globalRecoveryFunction | undefined,
returnError?: boolean
): Promise<ResultType | ErrorType> {
if (isArray(path)) {
path = [apiRoot, ...path].join("/");
}
else if (path.startsWith("/")) {
path = `${apiRoot}${path}`;
}
else {
path = `${apiRoot}/${path}`;
}
if (args && isEmpty(args)) {
args = undefined;
}
try {
let trialRemain = 3;
let lastErr: ErrorType | undefined = undefined;
// eslint-disable-next-line no-constant-condition
while(trialRemain > 0){
const result = (await (() => {
if (method == "get") {
//TODO: args가 복합 object일 경우의 처리
return ky(path, {
searchParams: args === undefined ? undefined : {
...(args as Record<string, string | number>),
},
method,
headers: {
"content-type": "application/json",
},
timeout: 30000,
retry: 0,
});
}
return ky(path, {
method,
json: args,
headers: {
"content-type": "application/json",
},
timeout: 30000,
retry: 0,
});
})().json()) as ErrorType | ResultType;
if (!result.result) {
lastErr = result;
if(result.recovery && globalRecovery){
const recoveryResult = await globalRecovery(method, path, args, result.recovery, result);
if(recoveryResult !== undefined && recoveryResult.justRetryAPI){
trialRemain--;
continue;
}
}
if (returnError) {
return result;
}
throw new APIFailed(
result.reason,
result.detail,
result.recovery
);
}
return result;
}
if(lastErr === undefined){
throw new Error('lastErr is undefined');
}
if (returnError) {
return lastErr;
}
throw lastErr.reason;
}
catch(err) {
if(!returnError){
throw err;
}
let infoText = ''
if(err instanceof APIFailed){
infoText = err.reason;
}
else if(err instanceof Error){
infoText = err.toString();
}
else if(typeof err == 'string'){
infoText = err;
}
else{
infoText = JSON.stringify(err);
}
return {
result: false,
reason: `failed to fetch(${path}): ${err}`,
detail: err,
} as ErrorType;
}
}
+15
View File
@@ -0,0 +1,15 @@
import { clientAPIPathGen, type APITail, type DefAPINamespace, type RawArgType } from "@strpc/def";
import { callClientAPI, type globalRecoveryFunction } from "./callClientAPI.js";
export { callClientAPI, type globalRecoveryFunction } from "./callClientAPI.js";
export function buildClientAPI<T extends DefAPINamespace>(structure: T, apiRoot = '/api', globalRecovery?: globalRecoveryFunction){
return clientAPIPathGen(structure, (path: string[], tail: APITail) => {
const method = tail.reqType;
return (args?: RawArgType, returnError?: boolean) => {
if (returnError) {
return callClientAPI(method, apiRoot, path.join('/'), args, globalRecovery, returnError);
}
return callClientAPI(method, apiRoot, path.join('/'), args, globalRecovery);
};
});
}
+12
View File
@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
},
"references": [
{
"path": "../def"
},
]
}
+17
View File
@@ -0,0 +1,17 @@
{
"name": "@strpc/def",
"version": "1.0.0",
"description": "",
"main": "dist/index.js",
"exports": {
".": "./dist/index.js",
"./types": "./dist/types.js"
},
"devDependencies": {},
"scripts": {
"build": "tsc --build"
},
"type": "module",
"author": "",
"license": "ISC"
}
+38
View File
@@ -0,0 +1,38 @@
export function clientAPIPathGen<T extends object, V>(
obj: T,
callback: (path: string[], tail: V) => unknown,
path: string[] = [],
): T {
const map = new Map<string, unknown>();
return new Proxy(obj, {
get(target, key) {
if(typeof key === 'symbol'){
throw new Error('Symbol is not supported');
}
const cachedResult = map.get(key);
if(cachedResult !== undefined){
return cachedResult;
}
const nextPath = [...path, key];
let next;
if (key in target) {
next = target[key as keyof typeof target];
}
else {
throw `${nextPath} is not exists`;
}
if (typeof (next) === 'function') {
const result = callback(nextPath, next as V);
map.set(key, result);
return result;
}
const result = clientAPIPathGen<object, V>(next as object, callback, nextPath);
map.set(key, result);
return result;
}
}) as T;
}
+23
View File
@@ -0,0 +1,23 @@
export type {
HttpMethod,
RawArgType,
DefAPINamespace,
APICompatType,
ValidResponse,
InvalidResponse,
recoveryMethod,
InferResponse,
InferError,
InferQuery,
} from './types.js';
export {
type APITail,
GET,
POST,
PUT,
DELETE,
PATCH,
HEAD,
} from './tail.js';
export { clientAPIPathGen } from './clientAPIPathGen.js';
+177
View File
@@ -0,0 +1,177 @@
import type { ArgDeleteAPI, ArgGetAPI, ArgHeadAPI, ArgPatchAPI, ArgPostAPI, ArgPutAPI, EmptyDeleteAPI, EmptyGetAPI, EmptyHeadAPI, EmptyPatchAPI, EmptyPostAPI, EmptyPutAPI, InvalidResponse, RawArgType, ValidResponse } from "./types.js";
export type APITail = ReturnType<typeof GET> | ReturnType<typeof POST> | ReturnType<typeof PUT> | ReturnType<typeof DELETE> | ReturnType<typeof PATCH> | ReturnType<typeof HEAD>;
export function GET(): EmptyGetAPI<ValidResponse, InvalidResponse>;
export function GET<
ResultType extends ValidResponse = ValidResponse,
ErrorType extends InvalidResponse = InvalidResponse,
>(): EmptyGetAPI<ResultType, ErrorType>;
export function GET<
ArgType extends RawArgType,
ResultType extends ValidResponse = ValidResponse,
ErrorType extends InvalidResponse = InvalidResponse,
>(): ArgGetAPI<ArgType, ResultType, ErrorType>;
export function GET<
ArgType extends RawArgType,
ResultType extends ValidResponse = ValidResponse,
ErrorType extends InvalidResponse = InvalidResponse,
>(): ArgGetAPI<ArgType, ResultType, ErrorType> | EmptyGetAPI<ResultType, ErrorType> {
return Object.assign(
fakeGET,
{ reqType: 'get' } as const
)
}
export function POST(): EmptyPostAPI<ValidResponse, InvalidResponse>;
export function POST<
ResultType extends ValidResponse = ValidResponse,
ErrorType extends InvalidResponse = InvalidResponse,
>(): EmptyPostAPI<ResultType, ErrorType>;
export function POST<
ArgType extends RawArgType,
ResultType extends ValidResponse = ValidResponse,
ErrorType extends InvalidResponse = InvalidResponse,
>(): ArgPostAPI<ArgType, ResultType, ErrorType>;
export function POST<
ArgType extends RawArgType,
ResultType extends ValidResponse = ValidResponse,
ErrorType extends InvalidResponse = InvalidResponse,
>(): ArgPostAPI<ArgType, ResultType, ErrorType> | EmptyPostAPI<ResultType, ErrorType> {
return Object.assign(
fakePOST,
{ reqType: 'post' } as const
)
}
export function PUT(): EmptyPutAPI<ValidResponse, InvalidResponse>;
export function PUT<
ResultType extends ValidResponse = ValidResponse,
ErrorType extends InvalidResponse = InvalidResponse,
>(): EmptyPutAPI<ResultType, ErrorType>;
export function PUT<
ArgType extends RawArgType,
ResultType extends ValidResponse = ValidResponse,
ErrorType extends InvalidResponse = InvalidResponse,
>(): ArgPutAPI<ArgType, ResultType, ErrorType>;
export function PUT<
ArgType extends RawArgType,
ResultType extends ValidResponse = ValidResponse,
ErrorType extends InvalidResponse = InvalidResponse,
>(): ArgPutAPI<ArgType, ResultType, ErrorType> | EmptyPutAPI<ResultType, ErrorType> {
return Object.assign(
fakePUT,
{ reqType: 'put' } as const
)
}
export function DELETE(): EmptyDeleteAPI<ValidResponse, InvalidResponse>;
export function DELETE<
ResultType extends ValidResponse = ValidResponse,
ErrorType extends InvalidResponse = InvalidResponse,
>(): EmptyDeleteAPI<ResultType, ErrorType>;
export function DELETE<
ArgType extends RawArgType,
ResultType extends ValidResponse = ValidResponse,
ErrorType extends InvalidResponse = InvalidResponse,
>(): ArgDeleteAPI<ArgType, ResultType, ErrorType>;
export function DELETE<
ArgType extends RawArgType,
ResultType extends ValidResponse = ValidResponse,
ErrorType extends InvalidResponse = InvalidResponse,
>(): ArgDeleteAPI<ArgType, ResultType, ErrorType> | EmptyDeleteAPI<ResultType, ErrorType> {
return Object.assign(
fakeDELETE,
{ reqType: 'delete' } as const
)
}
export function PATCH(): EmptyPatchAPI<ValidResponse, InvalidResponse>;
export function PATCH<
ResultType extends ValidResponse = ValidResponse,
ErrorType extends InvalidResponse = InvalidResponse,
>(): EmptyPatchAPI<ResultType, ErrorType>;
export function PATCH<
ArgType extends RawArgType,
ResultType extends ValidResponse = ValidResponse,
ErrorType extends InvalidResponse = InvalidResponse,
>(): ArgPatchAPI<ArgType, ResultType, ErrorType>;
export function PATCH<
ArgType extends RawArgType,
ResultType extends ValidResponse = ValidResponse,
ErrorType extends InvalidResponse = InvalidResponse,
>(): ArgPatchAPI<ArgType, ResultType, ErrorType> | EmptyPatchAPI<ResultType, ErrorType> {
return Object.assign(
fakePATCH,
{ reqType: 'patch' } as const
)
}
export function HEAD(): EmptyHeadAPI<ValidResponse, InvalidResponse>;
export function HEAD<
ResultType extends ValidResponse = ValidResponse,
ErrorType extends InvalidResponse = InvalidResponse,
>(): EmptyHeadAPI<ResultType, ErrorType>;
export function HEAD<
ArgType extends RawArgType,
ResultType extends ValidResponse = ValidResponse,
ErrorType extends InvalidResponse = InvalidResponse,
>(): ArgHeadAPI<ArgType, ResultType, ErrorType>;
export function HEAD<
ArgType extends RawArgType,
ResultType extends ValidResponse = ValidResponse,
ErrorType extends InvalidResponse = InvalidResponse,
>(): ArgHeadAPI<ArgType, ResultType, ErrorType> | EmptyHeadAPI<ResultType, ErrorType> {
return Object.assign(
fakeHEAD,
{ reqType: 'head' } as const
)
}
async function fakeGET<
ResultType extends ValidResponse,
ErrorType extends InvalidResponse,
ArgType extends undefined = undefined
>(args?: ArgType, returnError = false): Promise<ResultType | ErrorType> {
throw `Can't directly call GET. ${args}, ${returnError}. Use auto-generated path API.`
}
async function fakePOST<
ResultType extends ValidResponse,
ErrorType extends InvalidResponse,
ArgType extends RawArgType = undefined
>(args: ArgType, returnError = false): Promise<ResultType | ErrorType> {
throw `Can't directly call POST. ${args}, ${returnError}. Use auto-generated path API.`
}
async function fakePUT<
ResultType extends ValidResponse,
ErrorType extends InvalidResponse,
ArgType extends RawArgType = undefined
>(args: ArgType, returnError = false): Promise<ResultType | ErrorType> {
throw `Can't directly call PUT. ${args}, ${returnError}. Use auto-generated path API.`;
}
async function fakeDELETE<
ResultType extends ValidResponse,
ErrorType extends InvalidResponse,
ArgType extends RawArgType = undefined
>(args: ArgType, returnError = false): Promise<ResultType | ErrorType> {
throw `Can't directly call DELETE. ${args}, ${returnError}. Use auto-generated path API.`;
}
async function fakePATCH<
ResultType extends ValidResponse,
ErrorType extends InvalidResponse,
ArgType extends RawArgType = undefined
>(args: ArgType, returnError = false): Promise<ResultType | ErrorType> {
throw `Can't directly call PATCH. ${args}, ${returnError}. Use auto-generated path API.`
}
async function fakeHEAD<
ResultType extends ValidResponse,
ErrorType extends InvalidResponse,
ArgType extends RawArgType = undefined
>(args?: ArgType, returnError = false): Promise<ResultType | ErrorType> {
throw `Can't directly call HEAD. ${args}, ${returnError}. Use auto-generated path API.`;
}
+191
View File
@@ -0,0 +1,191 @@
export type HttpMethod = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head';
export type RawArgType = {
[key: string]: unknown;
} | undefined;
export interface ArgAnyAPI<
ArgType extends RawArgType,
ResultType extends ValidResponse,
ErrorType extends InvalidResponse,
Method extends HttpMethod
> {
(args: ArgType): Promise<ResultType>;
(args: ArgType, returnError: false): Promise<ResultType>;
(args: ArgType, returnError: true): Promise<ResultType | ErrorType>;
readonly reqType: Method;
}
export type ArgGetAPI<
ArgType extends RawArgType,
ResultType extends ValidResponse,
ErrorType extends InvalidResponse
> = ArgAnyAPI<ArgType, ResultType, ErrorType, 'get'>;
export type ArgPostAPI<
ArgType extends RawArgType,
ResultType extends ValidResponse,
ErrorType extends InvalidResponse
> = ArgAnyAPI<ArgType, ResultType, ErrorType, 'post'>;
export type ArgPutAPI<
ArgType extends RawArgType,
ResultType extends ValidResponse,
ErrorType extends InvalidResponse
> = ArgAnyAPI<ArgType, ResultType, ErrorType, 'put'>;
export type ArgDeleteAPI<
ArgType extends RawArgType,
ResultType extends ValidResponse,
ErrorType extends InvalidResponse
> = ArgAnyAPI<ArgType, ResultType, ErrorType, 'delete'>;
export type ArgPatchAPI<
ArgType extends RawArgType,
ResultType extends ValidResponse,
ErrorType extends InvalidResponse
> = ArgAnyAPI<ArgType, ResultType, ErrorType, 'patch'>;
export type ArgHeadAPI<
ArgType extends RawArgType,
ResultType extends ValidResponse,
ErrorType extends InvalidResponse
> = ArgAnyAPI<ArgType, ResultType, ErrorType, 'head'>;
export interface EmptyAnyAPI<ResultType extends ValidResponse, ErrorType extends InvalidResponse, Method extends HttpMethod> {
(): Promise<ResultType>;
(args?: undefined): Promise<ResultType>;
(args: undefined, returnError: false): Promise<ResultType>;
(args: undefined, returnError: true): Promise<ResultType | ErrorType>;
readonly reqType: Method;
}
export type EmptyGetAPI<
ResultType extends ValidResponse,
ErrorType extends InvalidResponse
> = EmptyAnyAPI<ResultType, ErrorType, 'get'>;
export type EmptyPostAPI<
ResultType extends ValidResponse,
ErrorType extends InvalidResponse
> = EmptyAnyAPI<ResultType, ErrorType, 'post'>;
export type EmptyPutAPI<
ResultType extends ValidResponse,
ErrorType extends InvalidResponse
> = EmptyAnyAPI<ResultType, ErrorType, 'put'>;
export type EmptyDeleteAPI<
ResultType extends ValidResponse,
ErrorType extends InvalidResponse
> = EmptyAnyAPI<ResultType, ErrorType, 'delete'>;
export type EmptyPatchAPI<
ResultType extends ValidResponse,
ErrorType extends InvalidResponse
> = EmptyAnyAPI<ResultType, ErrorType, 'patch'>;
export type EmptyHeadAPI<
ResultType extends ValidResponse,
ErrorType extends InvalidResponse
> = EmptyAnyAPI<ResultType, ErrorType, 'head'>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ArgTypeOf<T> = T extends AnyAPI<infer A, any, any> ? A : never;
export type AnyAPI<
ArgType extends RawArgType,
ResultType extends ValidResponse = ValidResponse,
ErrorType extends InvalidResponse = InvalidResponse
> = ArgType extends undefined ? EmptyAnyAPI<ResultType, ErrorType, HttpMethod> : ArgAnyAPI<ArgType, ResultType, ErrorType, HttpMethod>;
export type GetAPI<
ArgType extends RawArgType,
ResultType extends ValidResponse = ValidResponse,
ErrorType extends InvalidResponse = InvalidResponse
> = ArgType extends undefined ? EmptyGetAPI<ResultType, ErrorType> : ArgGetAPI<ArgType, ResultType, ErrorType>;
export type PostAPI<
ArgType extends RawArgType,
ResultType extends ValidResponse = ValidResponse,
ErrorType extends InvalidResponse = InvalidResponse
> = ArgType extends undefined ? EmptyPostAPI<ResultType, ErrorType> : ArgPostAPI<ArgType, ResultType, ErrorType>;
export type PutAPI<
ArgType extends RawArgType,
ResultType extends ValidResponse = ValidResponse,
ErrorType extends InvalidResponse = InvalidResponse
> = ArgType extends undefined ? EmptyPutAPI<ResultType, ErrorType> : ArgPutAPI<ArgType, ResultType, ErrorType>;
export type DeleteAPI<
ArgType extends RawArgType,
ResultType extends ValidResponse = ValidResponse,
ErrorType extends InvalidResponse = InvalidResponse
> = ArgType extends undefined ? EmptyDeleteAPI<ResultType, ErrorType> : ArgDeleteAPI<ArgType, ResultType, ErrorType>;
export type PatchAPI<
ArgType extends RawArgType,
ResultType extends ValidResponse = ValidResponse,
ErrorType extends InvalidResponse = InvalidResponse
> = ArgType extends undefined ? EmptyPatchAPI<ResultType, ErrorType> : ArgPatchAPI<ArgType, ResultType, ErrorType>;
export type HeadAPI<
ArgType extends RawArgType,
ResultType extends ValidResponse = ValidResponse,
ErrorType extends InvalidResponse = InvalidResponse
> = ArgType extends undefined ? EmptyHeadAPI<ResultType, ErrorType> : ArgHeadAPI<ArgType, ResultType, ErrorType>;
export interface ValidResponse {
result: true;
}
export type recoveryMethod = 'hardRefresh' | 'refreshEntireProcess' | 'retryAPI' | 'login';
export interface InvalidResponse {
result: false;
reason: string;
recovery?: recoveryMethod;
detail?: string | string[] | Record<string, string | string[]>;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Callable = (...args: any) => any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type InferQuery<T extends AnyAPI<any, any, any>> = T extends EmptyAnyAPI<any, any, HttpMethod> ? undefined : T extends ArgAnyAPI<infer Q, any, any, HttpMethod> ? Q : never;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type InferResponse<T extends AnyAPI<any, any, any>> = T extends EmptyAnyAPI<infer R, any, HttpMethod> ? R : T extends ArgAnyAPI<any, infer R, any, HttpMethod> ? R : never;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type InferError<T extends AnyAPI<any, any, any>> = T extends EmptyAnyAPI<any, infer E, HttpMethod> ? E : T extends ArgAnyAPI<any, any, infer E, HttpMethod> ? E : never;
export type DefAPINamespace = {
[key: string]: DefAPINamespace
// eslint-disable-next-line @typescript-eslint/no-explicit-any
| ArgGetAPI<any, any, any>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
| ArgPostAPI<any, any, any>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
| ArgPutAPI<any, any, any>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
| ArgDeleteAPI<any, any, any>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
| ArgPatchAPI<any, any, any>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
| ArgHeadAPI<any, any, any>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
| EmptyGetAPI<any, any>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
| EmptyPostAPI<any, any>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
| EmptyPutAPI<any, any>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
| EmptyDeleteAPI<any, any>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
| EmptyPatchAPI<any, any>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
| EmptyHeadAPI<any, any>
;
}
export type APICompatType<T extends Callable> = T extends AnyAPI<infer A, infer R, infer E> ? AnyAPI<A, R, E> : never;
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
},
"references": [
]
}
+32
View File
@@ -0,0 +1,32 @@
{
"name": "@strpc/express",
"version": "1.0.0",
"description": "",
"main": "dist/index.js",
"scripts": {
"build": "tsc --build"
},
"exports": {
".": "./dist/index.js",
"./generator": "./dist/generator.js",
"./proc_decorator": "./dist/proc_decorator.js"
},
"type": "module",
"author": "",
"license": "ISC",
"dependencies": {
"@strpc/def": "workspace:^"
},
"devDependencies": {
"express": "^4.18.2",
"express-session": "^1.17.3",
"lodash-es": "^4.17.21",
"zod": "^3.22.2"
},
"peerDependencies": {
"express": "^4.18.2",
"express-session": "^1.17.3",
"lodash-es": "^4.17.21",
"zod": "^3.22.2"
}
}
+213
View File
@@ -0,0 +1,213 @@
import type { Request, Response } from 'express';
import type { AnyAPI, Callable, DefAPINamespace, DeleteAPI, GetAPI, HeadAPI, HttpMethod, InferError, InferQuery, InferResponse, InvalidResponse, PatchAPI, PostAPI, PutAPI, recoveryMethod } from '@strpc/def/types';
import type { Empty, PostProcDecoratorRunner, ProcDecoratorRunner } from './proc_decorator.js';
import type { ZodType } from 'zod';
export type APINamespace = {
[key: string]: APINamespace
// eslint-disable-next-line @typescript-eslint/no-explicit-any
| iAPI_GET<any, any>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
| iAPI_POST<any, any>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
| iAPI_PUT<any, any>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
| iAPI_DELETE<any, any>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
| iAPI_PATCH<any, any>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
| iAPI_HEAD<any, any>
;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AnyAPIExecuter = APIExecuter<any, any>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnyAnyAPI = AnyAPI<any, any, any>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
interface APIExecuter<T extends AnyAnyAPI, CTX extends object> {
(query: InferQuery<T>, ctx: CTX, expressReq: Request, expressRes: Response): Promise<InferResponse<T> | InferError<T> | true>;
httpMethod: HttpMethod;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
argValidator?: ZodType<InferQuery<T>>;
preDecorator: ProcDecoratorRunner<CTX, Empty>;
postDecorator: PostProcDecoratorRunner<CTX>;
}
export interface iAPI_GET<T extends AnyAnyAPI, CTX extends object> extends APIExecuter<T, CTX> {
httpMethod: 'get';
}
export interface iAPI_POST<T extends AnyAnyAPI, CTX extends object> extends APIExecuter<T, CTX> {
httpMethod: 'post';
}
export interface iAPI_PUT<T extends AnyAnyAPI, CTX extends object> extends APIExecuter<T, CTX> {
httpMethod: 'put';
}
export interface iAPI_DELETE<T extends AnyAnyAPI, CTX extends object> extends APIExecuter<T, CTX> {
httpMethod: 'delete';
}
export interface iAPI_PATCH<T extends AnyAnyAPI, CTX extends object> extends APIExecuter<T, CTX> {
httpMethod: 'patch';
}
export interface iAPI_HEAD<T extends AnyAnyAPI, CTX extends object> extends APIExecuter<T, CTX> {
httpMethod: 'head';
}
/** API의 반환형. ValidResponse | InvalidResponse | true */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type APIReturnType<T extends AnyAnyAPI> = Promise<InferResponse<T> | InferError<T> | true>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function generateAPI<T extends AnyAnyAPI, CTX extends object>(httpMethod: HttpMethod,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
argValidator: ZodType<InferQuery<T>> | undefined,
preDecorator: ProcDecoratorRunner<CTX, Empty>,
postDecorator: PostProcDecoratorRunner<CTX>,
callback: (query: InferQuery<T>, ctx: CTX, expressReq: Request, expressRes: Response) => Promise<InferResponse<T> | InferError<T> | true>,
): APIExecuter<T, CTX> {
return Object.assign(
callback,
{
httpMethod,
argValidator,
preDecorator,
postDecorator,
}
);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function GET<T extends GetAPI<any, any, any>>(argValidator?: ZodType<InferQuery<T>>) {
return <CTX extends object>([preDecorator, postDecorator]: readonly [ProcDecoratorRunner<CTX, Empty>, PostProcDecoratorRunner<CTX>]) => {
return (callback: (query: InferQuery<T>, ctx: CTX, expressReq: Request, expressRes: Response) => Promise<InferResponse<T> | InferError<T> | true>) => {
return generateAPI<T, CTX>(
'get',
argValidator,
preDecorator,
postDecorator,
callback,
) as iAPI_GET<T, CTX>;
}
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function POST<T extends PostAPI<any, any, any>>(argValidator?: ZodType<InferQuery<T>>) {
return <CTX extends object>([preDecorator, postDecorator]: readonly [ProcDecoratorRunner<CTX, Empty>, PostProcDecoratorRunner<CTX>]) => {
return (callback: (query: InferQuery<T>, ctx: CTX, expressReq: Request, expressRes: Response) => Promise<InferResponse<T> | InferError<T> | true>) => {
return generateAPI<T, CTX>(
'post',
argValidator,
preDecorator,
postDecorator,
callback,
) as iAPI_POST<T, CTX>;
}
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function PUT<T extends PutAPI<any, any, any>>(argValidator?: ZodType<InferQuery<T>>) {
return <CTX extends object>([preDecorator, postDecorator]: readonly [ProcDecoratorRunner<CTX, Empty>, PostProcDecoratorRunner<CTX>]) => {
return (callback: (query: InferQuery<T>, ctx: CTX, expressReq: Request, expressRes: Response) => Promise<InferResponse<T> | InferError<T> | true>) => {
return generateAPI<T, CTX>(
'put',
argValidator,
preDecorator,
postDecorator,
callback,
) as iAPI_PUT<T, CTX>;
}
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function DELETE<T extends DeleteAPI<any, any, any>, Z extends ZodType<InferQuery<T>>>(argValidator?: Z) {
return <CTX extends object>([preDecorator, postDecorator]: readonly [ProcDecoratorRunner<CTX, Empty>, PostProcDecoratorRunner<CTX>]) => {
return (callback: (query: InferQuery<T>, ctx: CTX, expressReq: Request, expressRes: Response) => Promise<InferResponse<T> | InferError<T> | true>) => {
return generateAPI<T, CTX>(
'delete',
argValidator,
preDecorator,
postDecorator,
callback,
) as iAPI_DELETE<T, CTX>;
}
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function PATCH<T extends PatchAPI<any, any, any>>(argValidator?: ZodType<InferQuery<T>>) {
return <CTX extends object>([preDecorator, postDecorator]: readonly [ProcDecoratorRunner<CTX, Empty>, PostProcDecoratorRunner<CTX>]) => {
return (callback: (query: InferQuery<T>, ctx: CTX, expressReq: Request, expressRes: Response) => Promise<InferResponse<T> | InferError<T> | true>) => {
return generateAPI<T, CTX>(
'patch',
argValidator,
preDecorator,
postDecorator,
callback,
) as iAPI_PATCH<T, CTX>;
}
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function HEAD<T extends HeadAPI<any, any, any>>(argValidator?: ZodType<InferQuery<T>>) {
return <CTX extends object>([preDecorator, postDecorator]: readonly [ProcDecoratorRunner<CTX, Empty>, PostProcDecoratorRunner<CTX>]) => {
return (callback: (query: InferQuery<T>, ctx: CTX, expressReq: Request, expressRes: Response) => Promise<InferResponse<T> | InferError<T> | true>) => {
return generateAPI<T, CTX>(
'head',
argValidator,
preDecorator,
postDecorator,
callback,
) as iAPI_HEAD<T, CTX>;
}
}
}
export function raiseError(reason: string, recovery?: recoveryMethod): InvalidResponse {
if (recovery) {
return {
result: false,
reason,
recovery,
};
}
return {
result: false,
reason,
};
}
export type APIServerType<T extends Callable> =
// eslint-disable-next-line @typescript-eslint/no-explicit-any
T extends GetAPI<infer Q, infer R, infer E> ? iAPI_GET<GetAPI<Q, R, E>, any> :
// eslint-disable-next-line @typescript-eslint/no-explicit-any
T extends PostAPI<infer Q, infer R, infer E> ? iAPI_POST<PostAPI<Q, R, E>, any> :
// eslint-disable-next-line @typescript-eslint/no-explicit-any
T extends PutAPI<infer Q, infer R, infer E> ? iAPI_PUT<PutAPI<Q, R, E>, any> :
// eslint-disable-next-line @typescript-eslint/no-explicit-any
T extends DeleteAPI<infer Q, infer R, infer E> ? iAPI_DELETE<DeleteAPI<Q, R, E>, any> :
// eslint-disable-next-line @typescript-eslint/no-explicit-any
T extends PatchAPI<infer Q, infer R, infer E> ? iAPI_PATCH<PatchAPI<Q, R, E>, any> :
// eslint-disable-next-line @typescript-eslint/no-explicit-any
T extends HeadAPI<infer Q, infer R, infer E> ? iAPI_HEAD<HeadAPI<Q, R, E>, any> :
never;
export type APINamespaceType<T extends DefAPINamespace> = {
[K in keyof T]:
T[K] extends Callable ? APIServerType<T[K]> :
T[K] extends DefAPINamespace ? APINamespaceType<T[K]> :
never;
};
+161
View File
@@ -0,0 +1,161 @@
import { Router, type Request, type Response } from 'express';
import type { AnyAPIExecuter, APINamespace } from './defs.js';
import type { RawArgType } from '@strpc/def';
import type { SafeParseReturnType, ZodType, z } from 'zod';
import { flatten } from 'lodash-es';
const PRINT_API_CALL = true;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async function parseParam<Q extends RawArgType>(req: Request, argValidator?: ZodType<Q>): Promise<SafeParseReturnType<Q, Q>> {
if (!argValidator) {
return {
success: true,
data: req.query as Q
}
}
const query = req.query;
return await argValidator.safeParseAsync(query);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async function parseBody<Q extends RawArgType>(req: Request, argValidator?: ZodType<Q>): Promise<SafeParseReturnType<Q, Q>> {
if (!argValidator) {
return {
success: true,
data: req.body as Q
}
}
const query = req.body;
return await argValidator.safeParseAsync(query);
}
async function apiRun<Q extends RawArgType>(query: Q, req: Request, res: Response, api: AnyAPIExecuter): Promise<void> {
const [preResult, ctx] = await api.preDecorator({}, req, res);
if (!preResult.every((v) => v.result)) {
const lastErr = preResult.pop() as typeof preResult[0];
const [postResult,] = await api.postDecorator(ctx, preResult, req, res, false);
const postErrors = postResult.filter((obj) => !obj.result);
if (postErrors.length) {
//회수조차 불가능?
if (PRINT_API_CALL) {
console.log(JSON.stringify([(new Date).toISOString(), req.ip, req.path, false, 'preDecorator', lastErr.type, lastErr.info]));
}
res.json({
result: false,
path: req.path,
reason: lastErr.info,
recovery: lastErr.recovery,
detail: {
type: 'decorator',
preDecorator: [lastErr.type, lastErr.info],
postDecorator: flatten(postErrors.map((obj) => [obj.type, obj.info])),
},
});
return;
}
if (PRINT_API_CALL) {
console.log(JSON.stringify([(new Date).toISOString(), req.ip, req.path, false, 'preDecorator', lastErr.type, lastErr.info]));
}
res.json({
result: false,
path: req.path,
reason: lastErr.info,
recovery: lastErr.recovery,
detail: {
type: 'decorator',
preDecorator: [lastErr.type, lastErr.info],
},
})
return;
}
const result = await api(query, ctx, req, res);
const [postResult,] = await api.postDecorator(ctx, preResult, req, res, true);
const postErrors = postResult.filter((obj) => !obj.result);
if (postErrors.length) {
const lastErr = postErrors.pop() as typeof postErrors[0];
if (PRINT_API_CALL) {
console.log(JSON.stringify([(new Date).toISOString(), req.ip, req.path, false, 'postDecorator', lastErr.type, lastErr.info]));
}
if (result === true) {
//NOTE: 이미 api에서 response를 보낸 특이 케이스.
return;
}
res.json({
result: false,
path: req.path,
reason: lastErr?.info,
recovery: lastErr?.recovery,
detail: {
type: 'decorator',
preDecorator: flatten(postErrors.map((obj) => [obj.type, obj.info])),
},
originalResult: result,
});
return;
}
if (PRINT_API_CALL) {
if(result === true){
console.log(JSON.stringify([(new Date).toISOString(), req.ip, req.path, true, 'passThrough']));
}
else{
let tmp_result = true;
let tmp_reason = undefined;
if('result' in result){
tmp_result = result.result;
}
if('reason' in result){
tmp_reason = result.reason;
}
console.log(JSON.stringify([(new Date).toISOString(), req.ip, req.path, tmp_result, tmp_reason]));
}
}
if (result !== true) {
res.json(result);
}
}
export function buildAPISystem<N extends APINamespace, Q extends AnyAPIExecuter>(api: N | Q): Router {
const router = Router();
if (typeof api === 'function') {
throw 'root api cannot be function';
}
for (const [key, value] of Object.entries(api)) {
const rkey = `/${key}`;
if (typeof value !== 'function') {
router.use(rkey, buildAPISystem(value));
continue;
}
const executer = value;
if (!executer.httpMethod) {
throw new Error('APIExecuter.reqType is not defined');
}
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(queryResult.data, req, res, executer);
});
}
return router;
}
+3
View File
@@ -0,0 +1,3 @@
export * from './defs.js';
export * as generator from './generator.js';
export * as proc_decorator from './proc_decorator.js';
+187
View File
@@ -0,0 +1,187 @@
import type { recoveryMethod } from "@strpc/def";
import type { Request, Response } from "express";
type MayBePromise<T> = T | Promise<T>;
export type Empty = Record<string, never>;
export type DecoratorResultTrue = {
result: true;
type?: string;
info?: string;
recovery?: recoveryMethod;
};
export type DecoratorResultFalse = {
result: false;
type: string;
info: string;
recovery?: recoveryMethod;
}
export type DecoratorResult = DecoratorResultTrue | DecoratorResultFalse;
export type DecoratorStack = DecoratorResult[];
export interface ProcDecorator<Out extends object, In extends object = Empty> {
(inCtx: In & Partial<Out>, req: Request, res: Response)
: MayBePromise<[DecoratorResultTrue, Out]>
| MayBePromise<[DecoratorResultFalse, In & Partial<Out>]>;
}
export interface PostProcDecorator<T extends object> {
(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>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type PlainPostDecorator = PostProcDecorator<any>;
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 ProcDecoratorChain = readonly ((() => PlainDecorator) | (() => [PlainDecorator, PlainPostDecorator]))[];
export type ResolveChain<T> = T extends undefined ? Empty : T extends ProcDecoratorChain ? Resolve<PackChain<T>> : never;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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
export type ParseOutType<T> = T extends ProcDecorator<infer B, object> ? B : never;
export const EmptyProcDecorator: readonly [ProcDecoratorRunner<Empty, Empty>, PostProcDecoratorRunner<Empty>] = [
async (ctx) => [[], ctx], async (ctx, stack) => [stack, ctx]
];
export function declProcDecorators<T extends ProcDecoratorChain>(...decorators: T) {
type OutType = ParseOutType<PackChain<T>>;
const preDecorator: PlainDecorator[] = [];
const postDecorator: (PlainPostDecorator | undefined)[] = [];
if (decorators) {
for (const procGen of decorators) {
const proc = procGen();
if (Array.isArray(proc)) {
preDecorator.push(proc[0]);
postDecorator.push(proc[1]);
}
else {
preDecorator.push(proc);
postDecorator.push(undefined);
}
}
}
const packedDecorators: readonly [ProcDecoratorRunner<OutType, Empty>, PostProcDecoratorRunner<OutType>] = [
async (ctx, req, res) => {
let rctx = ctx as unknown as OutType;
const decoratorStack: DecoratorStack = [];
if (!preDecorator) {
return [decoratorStack, rctx];
}
for (const [idx, proc] of preDecorator.entries()) {
try {
const [stackResult, newCtx] = await proc(rctx, req, res);
decoratorStack.push(stackResult);
if (stackResult.result) {
rctx = newCtx;
continue;
}
return [decoratorStack, newCtx];
}
catch (e) {
while (decoratorStack.length > idx) {
decoratorStack.pop();
}
decoratorStack.push({
result: false,
type: 'PreThrow',
info: `internal error: ${e}`,
});
return [decoratorStack, rctx];
}
}
return [decoratorStack, rctx];
},
async (ctx, stack, req, res) => {
if (!postDecorator.length) {
return [stack, ctx];
}
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) {
if (isValidRoute) {
stack.pop();
}
ctx = nextCtx;
continue;
}
isValidRoute = false;
stack[idx] = postStackResult;
ctx = nextCtx;
}
catch (e) {
isValidRoute = false;
stack[idx] = {
result: false,
type: 'PostThrow',
info: `internal error: ${e}`,
};
}
}
return [stack, ctx];
},
] as const;
return packedDecorators;
}
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>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type PD2<B extends object, A extends object> = () => [ProcDecorator<B, A>, PostProcDecorator<any>];
export type PackChain<T> =
T extends readonly [] ? ProcDecorator<Empty, Empty> :
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 [PD1<infer B, infer A>, PD1<infer D, infer C>, ... infer R] ? PackChain<[() => Compose2<B, A, D, C>, ...R]> :
T extends readonly [PD2<infer B, infer A>, PD1<infer D, infer C>, ... infer R] ? PackChain<[() => Compose2<B, A, D, C>, ...R]> :
T extends readonly [PD1<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;
type Resolve<T> = T extends ProcDecorator<infer B, infer A> ? Empty extends A ? B : never : never;
+12
View File
@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
},
"references": [
{
"path": "../def"
},
]
}