@strpc ready
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "../def"
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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.`;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
},
|
||||
"references": [
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './defs.js';
|
||||
export * as generator from './generator.js';
|
||||
export * as proc_decorator from './proc_decorator.js';
|
||||
@@ -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;
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "../def"
|
||||
},
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user