diff --git a/package.json b/package.json index 3ea3eac..f6c3d24 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package", "name": "@kevisual/router", - "version": "0.0.34", + "version": "0.0.36", "description": "", "type": "module", "main": "./dist/router.js", diff --git a/src/app.ts b/src/app.ts index 548a6a6..d5137b4 100644 --- a/src/app.ts +++ b/src/app.ts @@ -16,16 +16,18 @@ type AppOptions = { io?: boolean; ioOpts?: { routerHandle?: RouterHandle; routerContext?: RouteContext; path?: string }; }; -export type AppReqRes = HandleCtx; + +export type AppRouteContext = HandleCtx & RouteContext & { app: App }; /** * 封装了 Router 和 Server 的 App 模块,处理http的请求和响应,内置了 Cookie 和 Token 和 res 的处理 + * U - Route Context的扩展类型 */ -export class App { +export class App { router: QueryRouter; server: Server; io: WsServer; - constructor(opts?: AppOptions) { + constructor(opts?: AppOptions) { const router = opts?.router || new QueryRouter(); const server = opts?.server || new Server(opts?.serverOptions || {}); server.setHandle(router.getHandle(router, opts?.routerHandle, opts?.routerContext)); @@ -62,10 +64,10 @@ export class App { add = this.addRoute; Route = Route; - route(opts: RouteOpts): Route; - route(path: string, key?: string): Route; - route(path: string, opts?: RouteOpts): Route; - route(path: string, key?: string, opts?: RouteOpts): Route; + route(opts: RouteOpts>): Route>; + route(path: string, key?: string): Route>; + route(path: string, opts?: RouteOpts>): Route>; + route(path: string, key?: string, opts?: RouteOpts>): Route>; route(...args: any[]) { const [path, key, opts] = args; if (typeof path === 'object') { @@ -82,8 +84,8 @@ export class App { } return new Route(path, key, opts); } - prompt(description: string): Route>; - prompt(description: Function): Route>; + prompt(description: string): Route> + prompt(description: Function): Route> prompt(...args: any[]) { const [desc] = args; let description = '' @@ -94,14 +96,20 @@ export class App { } return new Route('', '', { description }); } - - async call(message: { id?: string, path?: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) { + + async call(message: { id?: string, path?: string; key?: string; payload?: any }, ctx?: AppRouteContext & { [key: string]: any }) { const router = this.router; return await router.call(message, ctx); } - async queryRoute(path: string, key?: string, payload?: any, ctx?: RouteContext & { [key: string]: any }) { + /** + * @deprecated + */ + async queryRoute(path: string, key?: string, payload?: any, ctx?: AppRouteContext & { [key: string]: any }) { return await this.router.queryRoute({ path, key, payload }, ctx); } + async run(path: string, key?: string, payload?: any, ctx?: AppRouteContext & { [key: string]: any }) { + return await this.router.run({ path, key, payload }, ctx); + } exportRoutes() { return this.router.exportRoutes(); } diff --git a/src/route.ts b/src/route.ts index bc10652..a8adee5 100644 --- a/src/route.ts +++ b/src/route.ts @@ -1,8 +1,6 @@ -import { nanoid, random } from 'nanoid'; +import { nanoid } from 'nanoid'; import { CustomError } from './result/error.ts'; -import { Schema, Rule, createSchema } from './validator/index.ts'; import { pick } from './utils/pick.ts'; -import { get } from 'lodash-es'; import { listenProcess } from './utils/listen-process.ts'; export type RouterContextT = { code?: number;[key: string]: any }; @@ -12,6 +10,7 @@ export type RouteContext = { // response body /** return body */ body?: number | string | Object; + forward?: (response: { code: number, data?: any, message?: any }) => void; /** return code */ code?: number; /** return msg */ @@ -39,20 +38,15 @@ export type RouteContext = { nextQuery?: { [key: string]: any }; // end end?: boolean; - // 处理router manager - // TODO: - /** - * 请求 route的返回结果,包函ctx - */ - queryRouter?: QueryRouter; + app?: QueryRouter; error?: any; - /** 请求 route的返回结果,包函ctx */ + /** 请求 route的返回结果,不解析body为data */ call?: ( message: { path: string; key?: string; payload?: any;[key: string]: any } | { id: string; apyload?: any;[key: string]: any }, ctx?: RouteContext & { [key: string]: any }, ) => Promise; - /** 请求 route的返回结果,不包函ctx */ - queryRoute?: (message: { path: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) => Promise; + /** 请求 route的返回结果,解析了body为data,就类同于 query.post获取的数据*/ + run?: (message: { path: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) => Promise; index?: number; throw?: (code?: number | string, message?: string, tips?: string) => void; /** 是否需要序列化, 使用JSON.stringify和JSON.parse */ @@ -69,29 +63,16 @@ export type RouteMiddleware = id?: string; } | string; -export type RouteOpts = { +export type RouteOpts = { path?: string; key?: string; id?: string; - run?: Run; + run?: Run; nextRoute?: NextRoute; // route to run after this route description?: string; metadata?: { [key: string]: any }; middleware?: RouteMiddleware[]; // middleware type?: 'route' | 'middleware'; - /** - * validator: { - * packageName: { - * type: 'string', - * required: true, - * }, - * } - */ - validator?: { [key: string]: Rule }; - schema?: { [key: string]: any }; - isVerify?: boolean; - verify?: (ctx?: RouteContext, dev?: boolean) => boolean; - verifyKey?: (key: string, ctx?: RouteContext, dev?: boolean) => boolean; /** * $#$ will be used to split path and key */ @@ -102,8 +83,8 @@ export type RouteOpts = { delimiter?: string; isDebug?: boolean; }; -export type DefineRouteOpts = Omit; -const pickValue = ['path', 'key', 'id', 'description', 'type', 'validator', 'middleware', 'metadata'] as const; +export type DefineRouteOpts = Omit; +const pickValue = ['path', 'key', 'id', 'description', 'type', 'middleware', 'metadata'] as const; export type RouteInfo = Pick; export class Route { /** @@ -121,13 +102,7 @@ export class Route { metadata?: { [key: string]: any }; middleware?: RouteMiddleware[]; // middleware type? = 'route'; - private _validator?: { [key: string]: Rule }; - schema?: { [key: string]: any }; data?: any; - /** - * 是否需要验证 - */ - isVerify?: boolean; /** * 是否开启debug,开启后会打印错误信息 */ @@ -151,118 +126,16 @@ export class Route { this.description = opts.description; this.metadata = opts.metadata; this.type = opts.type || 'route'; - this.validator = opts.validator; this.middleware = opts.middleware || []; this.key = opts.key || key; this.path = opts.path || path; - this.isVerify = opts.isVerify ?? true; - this.createSchema(); } else { - this.isVerify = true; this.middleware = []; this.id = nanoid(); } this.isDebug = opts?.isDebug ?? false; } - private createSchema() { - try { - const validator = this.validator; - const keys = Object.keys(validator || {}); - const schemaList = keys.map((key) => { - return { [key]: createSchema(validator[key]) }; - }); - const schema = schemaList.reduce((prev, current) => { - return { ...prev, ...current }; - }, {}); - this.schema = schema; - } catch (e) { - console.error('createSchema error:', e); - } - } - /** - * set validator and create schema - * @param validator - */ - set validator(validator: { [key: string]: Rule }) { - this._validator = validator; - this.createSchema(); - } - get validator() { - return this._validator || {}; - } - /** - * has code, body, message in ctx, return ctx if has error - * @param ctx - * @param dev - * @returns - */ - verify(ctx: RouteContext, dev = false) { - const query = ctx.query || {}; - const schema = this.schema || {}; - const validator = this.validator; - const check = () => { - const queryKeys = Object.keys(validator); - for (let i = 0; i < queryKeys.length; i++) { - const key = queryKeys[i]; - const value = query[key]; - if (schema[key]) { - const result = schema[key].safeParse(value); - if (!result.success) { - const path = result.error.errors[0]?.path?.join?.('.properties.'); - let message = 'Invalid params'; - if (path) { - const keyS = `${key}.properties.${path}.message`; - message = get(validator, keyS, 'Invalid params') as any; - } - throw new CustomError(500, message); - } - } - } - }; - check(); - } - - /** - * Need to manully call return ctx fn and configure body, code, message - * @param key - * @param ctx - * @param dev - * @returns - */ - verifyKey(key: string, ctx: RouteContext, dev = false) { - const query = ctx.query || {}; - const schema = this.schema || {}; - const validator = this.validator; - const check = () => { - const value = query[key]; - if (schema[key]) { - try { - schema[key].parse(value); - } catch (e) { - if (dev) { - return { - message: validator[key].message || 'Invalid params', - path: this.path, - key: this.key, - error: e.message.toString(), - }; - } - return { - message: validator[key].message || 'Invalid params', - path: this.path, - key: this.key, - }; - } - } - }; - const checkRes = check(); - return checkRes; - } - setValidator(validator: { [key: string]: Rule }) { - this.validator = validator; - return this; - } prompt(description: string): this; prompt(description: Function): this; prompt(...args: any[]) { @@ -283,15 +156,11 @@ export class Route { // 全覆盖,所以opts需要准确,不能由idUsePath 需要check的变量 const setOpts = (opts: DefineRouteOpts) => { const keys = Object.keys(opts); - const checkList = ['path', 'key', 'run', 'nextRoute', 'description', 'metadata', 'middleware', 'type', 'validator', 'isVerify', 'isDebug']; + const checkList = ['path', 'key', 'run', 'nextRoute', 'description', 'metadata', 'middleware', 'type', 'isDebug']; for (let item of keys) { if (!checkList.includes(item)) { continue; } - if (item === 'validator') { - this.validator = opts[item]; - continue; - } if (item === 'middleware') { this.middleware = this.middleware.concat(opts[item]); continue; @@ -320,16 +189,12 @@ export class Route { update(opts: DefineRouteOpts, checkList?: string[]): this { const keys = Object.keys(opts); - const defaultCheckList = ['path', 'key', 'run', 'nextRoute', 'description', 'metadata', 'middleware', 'type', 'validator', 'isVerify', 'isDebug']; + const defaultCheckList = ['path', 'key', 'run', 'nextRoute', 'description', 'metadata', 'middleware', 'type', 'isDebug']; checkList = checkList || defaultCheckList; for (let item of keys) { if (!checkList.includes(item)) { continue; } - if (item === 'validator') { - this.validator = opts[item]; - continue; - } if (item === 'middleware') { this.middleware = this.middleware.concat(opts[item]); continue; @@ -360,10 +225,10 @@ export class QueryRouter { } add(route: Route) { - const has = this.routes.find((r) => r.path === route.path && r.key === route.key); - if (has) { + const has = this.routes.findIndex((r) => r.path === route.path && r.key === route.key); + if (has !== -1) { // remove the old route - this.routes = this.routes.filter((r) => r.id !== has.id); + this.routes.splice(has, 1); } this.routes.push(route); } @@ -460,19 +325,6 @@ export class QueryRouter { for (let i = 0; i < routeMiddleware.length; i++) { const middleware = routeMiddleware[i]; if (middleware) { - if (middleware?.isVerify) { - try { - middleware.verify(ctx); - } catch (e) { - if (middleware?.isDebug) { - console.error('=====debug====:', 'middleware verify error:', e.message); - } - ctx.message = e.message; - ctx.code = 500; - ctx.body = null; - return ctx; - } - } try { await middleware.run(ctx); } catch (e) { @@ -503,19 +355,6 @@ export class QueryRouter { // run route if (route) { if (route.run) { - if (route?.isVerify) { - try { - route.verify(ctx); - } catch (e) { - if (route?.isDebug) { - console.error('=====debug====:', 'verify error:', e.message); - } - ctx.message = e.message; - ctx.code = 500; - ctx.body = null; - return ctx; - } - } try { await route.run(ctx); } catch (e) { @@ -588,9 +427,20 @@ export class QueryRouter { ctx.throw = this.throw; ctx.app = this; ctx.call = this.call.bind(this); - ctx.queryRoute = this.queryRoute.bind(this); + ctx.run = this.run.bind(this); ctx.index = 0; ctx.progress = ctx.progress || []; + ctx.forward = (response: { code: number; data?: any; message?: any }) => { + if (response.code) { + ctx.code = response.code; + } + if (response.data !== undefined) { + ctx.body = response.data; + } + if (response.message !== undefined) { + ctx.message = response.message; + } + } const res = await this.runRoute(path, key, ctx); const serialize = ctx.needSerialize ?? true; // 是否需要序列化 if (serialize) { @@ -628,6 +478,7 @@ export class QueryRouter { * 请求 result 的数据 * @param message * @param ctx + * @deprecated use run or call instead * @returns */ async queryRoute(message: { id?: string; path: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) { @@ -638,6 +489,20 @@ export class QueryRouter { message: res.message, }; } + /** + * Router Run获取数据 + * @param message + * @param ctx + * @returns + */ + async run(message: { id?: string; path?: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) { + const res = await this.call(message, { ...this.context, ...ctx }); + return { + code: res.code, + data: res.body, + message: res.message, + }; + } /** * 设置上下文 * @description 这里的上下文是为了在handle函数中使用 @@ -658,17 +523,7 @@ export class QueryRouter { return async (msg: { id?: string; path?: string; key?: string;[key: string]: any }, handleContext?: RouteContext) => { try { const context = { ...ctx, ...handleContext }; - if (msg.id) { - const route = router.routes.find((r) => r.id === msg.id); - if (route) { - msg.path = route.path; - msg.key = route.key; - } else { - return { code: 404, message: 'Not found route' }; - } - } - // @ts-ignore - const res = await router.parse(msg, context); + const res = await router.call(msg, context); if (wrapperFn) { res.data = res.body; return wrapperFn(res, context); @@ -796,34 +651,19 @@ export class QueryRouterServer extends QueryRouter { } /** - * 等于queryRoute,但是调用了handle + * 调用了handle * @param param0 * @returns */ - async run({ path, key, payload }: { path: string; key?: string; payload?: any }) { + async run({ path, key, payload }: { path: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) { const handle = this.handle; - const resultError = (error: string, code = 500) => { - const r = { - code: code, - message: error, - }; - return r; - }; - try { - const end = handle({ path, key, ...payload }); - return end; - } catch (e) { - if (e.code && typeof e.code === 'number') { - return { - code: e.code, - message: e.message, - }; - } else { - return resultError('Router Server error'); - } + if (handle) { + const result = await this.call({ path, key, payload }, ctx); + return handle(result); } + return super.run({ path, key, payload }, ctx); } } -export const Mini = QueryRouterServer \ No newline at end of file +export class Mini extends QueryRouterServer { } \ No newline at end of file diff --git a/src/router-define.ts b/src/router-define.ts index 49dfcdf..7e78d4a 100644 --- a/src/router-define.ts +++ b/src/router-define.ts @@ -11,8 +11,8 @@ type SimpleObject = Record; export function define>( value: T, ): { - [K in keyof T]: T[K] & RouteOpts; -} { + [K in keyof T]: T[K] & RouteOpts; + } { return value as { [K in keyof T]: T[K] & RouteOpts }; } @@ -95,7 +95,7 @@ class QueryChain { * @param queryData * @returns */ - getKey(queryData?: SimpleObject): Pick { + getKey(queryData?: SimpleObject): Pick { const obj = this.omit(this.obj, this.omitKeys); return { ...obj, diff --git a/src/server/server.ts b/src/server/server.ts index e2f2285..883342b 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -53,7 +53,7 @@ export type ServerOpts = { /**path default `/api/router` */ path?: string; /**handle Fn */ - handle?: (msg?: { path: string; key?: string; [key: string]: any }, ctx?: { req: http.IncomingMessage; res: http.ServerResponse }) => any; + handle?: (msg?: { path: string; key?: string;[key: string]: any }, ctx?: { req: http.IncomingMessage; res: http.ServerResponse }) => any; cors?: Cors; httpType?: 'http' | 'https' | 'http2'; httpsKey?: string; @@ -222,7 +222,17 @@ export class Server { } else { this._server.on('request', listener); } - this._server.on('request', this._callback || this.createCallback()); + const callbackListener = this._callback || this.createCallback(); + this._server.on('request', callbackListener); + return () => { + if (Array.isArray(listener)) { + listener.forEach((l) => this._server.removeListener('request', l as Listener)); + } else { + this._server.removeListener('request', listener as Listener); + } + this.hasOn = false; + this._server.removeListener('request', callbackListener); + } } get callback() { return this._callback || this.createCallback(); diff --git a/src/test/app-type.ts b/src/test/app-type.ts new file mode 100644 index 0000000..384a978 --- /dev/null +++ b/src/test/app-type.ts @@ -0,0 +1,13 @@ +import { App } from '../app.ts' + +const app = new App<{ f: string }>(); + +app.route({ + path: 't', + run: async (ctx) => { + // ctx.r + ctx.app; + } +}).define(async (ctx) => { + ctx.f = 'hello'; +}).addTo(app); \ No newline at end of file