@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
+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;