import { CustomError, throwError, CustomErrorOptions } from './result/error.ts';
import { pick } from './utils/pick.ts';
import { listenProcess, MockProcess } from './utils/listen-process.ts';
import { z } from 'zod';
import { randomId } from './utils/random.ts';
import * as schema from './validator/schema.ts';
export type RouterContextT = { code?: number;[key: string]: any };
type ExtractArgs = A extends z.ZodTypeAny ? z.infer : A;
type OptionalKeys = {
[K in keyof T]-?: {} extends Pick ? K : never;
}[keyof T];
type MakeOptional = Omit & Partial>;
type BuildRouteContext = M extends { args?: infer A }
? A extends z.ZodObject
? RouteContext<{ args?: z.infer }, U>
: A extends Record
? RouteContext<{ args?: { [K in keyof A]: z.infer } }, U>
: RouteContext
: RouteContext;
export type RouteContext = {
/**
* 本地自己调用的时候使用,可以标识为当前自调用,那么 auth 就不许重复的校验
* 或者不需要登录的,直接调用
*/
appId?: string;
// run first
query?: { [key: string]: any };
args?: { [key: string]: any };
// response body
/** return body */
body?: number | string | Object;
forward?: (response: { code: number, data?: any, message?: any }) => void;
/** return code */
code?: number;
/** return msg */
message?: string;
/**
* 传递状态
*/
state?: S;
// transfer data
/**
* 当前routerId
*/
currentId?: string;
/**
* 当前路径
*/
currentPath?: string;
/**
* 当前key
*/
currentKey?: string;
/**
* 当前route
*/
currentRoute?: Route;
/**
* 进度
*/
progress?: [string, string][];
// onlyForNextRoute will be clear after next route
nextQuery?: { [key: string]: any };
// end
end?: boolean;
app?: QueryRouter;
error?: any;
/** 请求 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的返回结果,解析了body为data,就类同于 query.post获取的数据*/
run?: (message: { path: string; key?: string; payload?: any }, ctx?: RouteContext) => Promise;
index?: number;
throw?: throwError['throw'];
/** 是否需要序列化, 使用JSON.stringify和JSON.parse */
needSerialize?: boolean;
} & T & U;
export type SimpleObject = Record;
export type Run = (ctx: Required>) => Promise;
export type RunMessage = { path?: string; key?: string; id?: string; payload?: any; };
export type NextRoute = Pick;
export type RouteMiddleware =
| {
path?: string;
key?: string;
id?: string;
}
| string;
export type RouteOpts = {
path?: string;
key?: string;
id?: string;
run?: Run;
nextRoute?: NextRoute; // route to run after this route
description?: string;
metadata?: T;
middleware?: RouteMiddleware[]; // middleware
type?: 'route' | 'middleware';
/**
* $#$ will be used to split path and key
*/
idUsePath?: boolean;
/**
* id 合并的分隔符,默认为 $#$
*/
delimiter?: string;
isDebug?: boolean;
};
export type DefineRouteOpts = Omit;
const pickValue = ['path', 'key', 'id', 'description', 'type', 'middleware', 'metadata'] as const;
export type Skill = {
skill: string;
title: string;
summary?: string;
tags?: string[];
args?: {
[key: string]: any
};
} & T
export const tool = {
schema: z
}
/** */
export const createSkill = (skill: Skill): Skill => {
if (skill.tags) {
const hasOpencode = skill.tags.includes('opencode');
if (!hasOpencode) {
skill.tags.push('opencode');
}
}
return {
args: {},
...skill
};
}
export type RouteInfo = Pick;
type ExtractMetadata = M extends { metadata?: infer Meta } ? Meta extends SimpleObject ? Meta : SimpleObject : SimpleObject;
/**
* @M 是 route的 metadate的类型,默认是 SimpleObject
* @U 是 RouteContext 里 state的类型
*/
export class Route implements throwError {
/**
* 一级路径
*/
path?: string;
/**
* 二级路径
*/
key?: string;
id?: string;
run?: Run>;
nextRoute?: NextRoute; // route to run after this route
description?: string;
metadata?: M;
middleware?: RouteMiddleware[]; // middleware
type? = 'route';
/**
* 是否开启debug,开启后会打印错误信息
*/
isDebug?: boolean;
constructor(path: string = '', key: string = '', opts?: RouteOpts) {
if (!path) {
path = randomId(8, 'rand-');
}
path = path.trim();
key = key.trim();
this.path = path;
this.key = key;
if (opts) {
this.id = opts.id || randomId(12, 'rand-');
if (!opts.id && opts.idUsePath) {
const delimiter = opts.delimiter ?? '$#$';
this.id = path + delimiter + key;
}
this.run = opts.run as Run>;
this.nextRoute = opts.nextRoute;
this.description = opts.description;
this.metadata = opts.metadata as M;
this.type = opts.type || 'route';
this.middleware = opts.middleware || [];
this.key = opts.key || key;
this.path = opts.path || path;
} else {
this.middleware = [];
this.id = randomId(12, 'rand-');
}
this.isDebug = opts?.isDebug ?? false;
}
prompt(description: string): this;
prompt(description: Function): this;
prompt(...args: any[]) {
const [description] = args;
if (typeof description === 'string') {
this.description = description;
} else if (typeof description === 'function') {
this.description = description() || ''; // 如果是Promise,需要addTo App之前就要获取应有的函数了。
}
return this;
}
define(opts: DefineRouteOpts): this;
define(fn: Run>): this;
define(key: string, fn: Run>): this;
define(path: string, key: string, fn: Run>): this;
define(...args: any[]) {
const [path, key, opts] = args;
// 全覆盖,所以opts需要准确,不能由idUsePath 需要check的变量
const setOpts = (opts: DefineRouteOpts) => {
const keys = Object.keys(opts);
const checkList = ['path', 'key', 'run', 'nextRoute', 'description', 'metadata', 'middleware', 'type', 'isDebug'];
for (let item of keys) {
if (!checkList.includes(item)) {
continue;
}
if (item === 'middleware') {
this.middleware = this.middleware.concat(opts[item]);
continue;
}
this[item] = opts[item];
}
};
if (typeof path === 'object') {
setOpts(path);
return this;
}
if (typeof path === 'function') {
this.run = path as Run>;
return this;
}
if (typeof path === 'string' && typeof key === 'function') {
setOpts({ path, run: key });
return this;
}
if (typeof path === 'string' && typeof key === 'string' && typeof opts === 'function') {
setOpts({ path, key, run: opts });
return this;
}
return this;
}
update(opts: DefineRouteOpts, onlyUpdateList?: string[]): this {
const keys = Object.keys(opts);
const defaultCheckList = ['path', 'key', 'run', 'nextRoute', 'description', 'metadata', 'middleware', 'type', 'isDebug'];
const checkList = onlyUpdateList || defaultCheckList;
for (let item of keys) {
if (!checkList.includes(item)) {
continue;
}
if (item === 'middleware') {
this.middleware = this.middleware.concat(opts[item]);
continue;
}
this[item] = opts[item];
}
return this;
}
addTo(router: QueryRouter | { add: (route: Route) => void;[key: string]: any }, opts?: AddOpts) {
router.add(this, opts);
}
throw(...args: any[]) {
CustomError.throw(...args);
}
}
const toJSONSchemaRoute = (route: RouteInfo) => {
const pickValues = pick(route, pickValue as any);
if (pickValues?.metadata?.args) {
pickValues.metadata.args = toJSONSchema(pickValues?.metadata?.args, { mergeObject: false });
}
return pickValues;
}
export const toJSONSchema = schema.toJSONSchema;
export const fromJSONSchema = schema.fromJSONSchema;
/**
* @parmas overwrite 是否覆盖已存在的route,默认true
*/
export type AddOpts = { overwrite?: boolean };
export class QueryRouter implements throwError {
appId: string = '';
routes: Route[];
maxNextRoute = 40;
context?: RouteContext = {} as RouteContext; // default context for call
constructor() {
this.routes = [];
}
/**
* add route
* @param route
* @param opts
*/
add(route: Route, opts?: AddOpts) {
const overwrite = opts?.overwrite ?? true;
const has = this.routes.findIndex((r) => r.path === route.path && r.key === route.key);
if (has !== -1) {
if (!overwrite) {
return;
}
// 如果存在,且overwrite为true,则覆盖
this.routes.splice(has, 1);
}
this.routes.push(route);
}
/**
* remove route by path and key
* @param route
*/
remove(route: Route | { path: string; key?: string }) {
this.routes = this.routes.filter((r) => r.path === route.path && r.key == route.key);
}
/**
* remove route by id
* @param uniqueId
*/
removeById(uniqueId: string) {
this.routes = this.routes.filter((r) => r.id !== uniqueId);
}
/**
* 执行route
* @param path
* @param key
* @param ctx
* @returns
*/
async runRoute(path: string, key: string, ctx?: RouteContext): Promise> {
const route = this.routes.find((r) => r.path === path && r.key === key);
const maxNextRoute = this.maxNextRoute;
ctx = (ctx || {}) as RouteContext;
ctx.currentPath = path;
ctx.currentId = route?.id;
ctx.currentKey = key;
ctx.currentRoute = route;
ctx.index = (ctx.index || 0) + 1;
const progress = [path, key] as [string, string];
if (ctx.progress) {
ctx.progress.push(progress);
} else {
ctx.progress = [progress];
}
if (ctx.index > maxNextRoute) {
ctx.code = 500;
ctx.message = 'Too many nextRoute';
ctx.body = null;
return ctx;
}
// run middleware
if (route && route.middleware && route.middleware.length > 0) {
const errorMiddleware: { path?: string; key?: string; id?: string }[] = [];
const getMiddleware = (m: Route) => {
if (!m.middleware || m.middleware.length === 0) return [];
const routeMiddleware: Route[] = [];
for (let i = 0; i < m.middleware.length; i++) {
const item = m.middleware[i];
let route: Route | undefined;
const isString = typeof item === 'string';
if (isString) {
route = this.routes.find((r) => r.id === item);
} else {
route = this.routes.find((r) => {
if (item.id) {
return r.id === item.id;
} else {
// key 可以是空,所以可以不严格验证
return r.path === item.path && r.key == item.key;
}
});
}
if (!route) {
if (isString) {
errorMiddleware.push({
id: item as string,
});
} else
errorMiddleware.push({
path: m?.path,
key: m?.key,
});
}
const routeMiddlewarePrevious = getMiddleware(route);
if (routeMiddlewarePrevious.length > 0) {
routeMiddleware.push(...routeMiddlewarePrevious);
}
routeMiddleware.push(route);
}
return routeMiddleware;
};
const routeMiddleware = getMiddleware(route);
if (errorMiddleware.length > 0) {
console.error('middleware not found');
ctx.body = errorMiddleware;
ctx.message = 'middleware not found';
ctx.code = 404;
return ctx;
}
for (let i = 0; i < routeMiddleware.length; i++) {
const middleware = routeMiddleware[i];
if (middleware) {
try {
await middleware.run(ctx as Required>);
} catch (e) {
if (route?.isDebug) {
console.error('=====debug====:middlerware error');
console.error('=====debug====:', e);
console.error('=====debug====:[path:key]:', `${route.path}-${route.key}`);
}
if (e instanceof CustomError || e?.code) {
ctx.code = e.code;
ctx.message = e.message;
ctx.body = null;
} else {
console.error(`[router error] fn:${route.path}-${route.key}:${route.id}`);
console.error(`[router error] middleware:${middleware.path}-${middleware.key}:${middleware.id}`);
console.error(e)
ctx.code = 500;
ctx.message = 'Internal Server Error';
ctx.body = null;
}
return ctx;
}
if (ctx.end) {
return ctx;
}
}
}
}
// run route
if (route) {
if (route.run) {
try {
await route.run(ctx as Required>);
} catch (e) {
if (route?.isDebug) {
console.error('=====debug====:route error');
console.error('=====debug====:', e);
console.error('=====debug====:[path:key]:', `${route.path}-${route.key}`);
}
if (e instanceof CustomError) {
ctx.code = e.code;
ctx.message = e.message;
} else {
console.error(`[router error] fn:${route.path}-${route.key}:${route.id}`);
console.error(`[router error] error`, e);
ctx.code = 500;
ctx.message = 'Internal Server Error';
}
ctx.body = null;
return ctx;
}
if (ctx.end) {
// TODO: 提前结束, 以及错误情况
return;
}
if (route.nextRoute) {
let path: string, key: string;
if (route.nextRoute.path || route.nextRoute.key) {
path = route.nextRoute.path;
key = route.nextRoute.key;
} else if (route.nextRoute.id) {
const nextRoute = this.routes.find((r) => r.id === route.nextRoute.id);
if (nextRoute) {
path = nextRoute.path;
key = nextRoute.key;
}
}
if (!path || !key) {
ctx.message = 'nextRoute not found';
ctx.code = 404;
ctx.body = null;
return ctx;
}
ctx.query = { ...ctx.query, ...ctx.nextQuery };
ctx.args = ctx.query;
ctx.nextQuery = {};
return await this.runRoute(path, key, ctx);
}
if (!ctx.code) ctx.code = 200;
return ctx;
} else {
// return Promise.resolve({ code: 404, body: 'Not found runing' });
// 可以不需要run的route,因为不一定是错误
return ctx;
}
}
// 如果没有找到route,返回404,这是因为出现了错误
return Promise.resolve({ code: 404, body: 'Not found' } as RouteContext);
}
/**
* 第一次执行
* @param message
* @param ctx
* @returns
*/
async parse(message: { path: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) {
if (!message?.path) {
return Promise.resolve({ code: 404, body: null, message: 'Not found path' } as RouteContext);
}
const { path, key = '', payload = {}, ...query } = message;
ctx = ctx || {} as RouteContext;
ctx.query = { ...ctx.query, ...query, ...payload };
ctx.args = ctx.query;
ctx.state = { ...ctx?.state };
ctx.throw = this.throw;
ctx.app = this;
ctx.call = this.call.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) {
res.body = JSON.parse(JSON.stringify(res.body || ''));
}
return res;
}
/**
* 返回的数据包含所有的context的请求返回的内容,可做其他处理
* @param message
* @param ctx
* @returns
*/
async call(message: { id?: string; path?: string; key?: string; payload?: any }, ctx?: RouteContext & { [key: string]: any }) {
let path = message.path;
let key = message.key;
// 优先 path + key
if (path) {
return await this.parse({ ...message, path, key }, { ...this.context, ...ctx });
} else if (message.id) {
const route = this.routes.find((r) => r.id === message.id);
if (route) {
path = route.path;
key = route.key;
} else {
return { code: 404, body: null, message: 'Not found route' };
}
return await this.parse({ ...message, path, key }, { ...this.context, ...ctx });
} else {
return { code: 404, body: null, message: 'Not found path' };
}
}
/**
* 请求 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 }) {
const res = await this.call(message, { ...this.context, ...ctx });
return {
code: res.code,
data: res.body,
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函数中使用
* @param ctx
*/
setContext(ctx: RouteContext) {
this.context = ctx as RouteContext;
}
getList(filter?: (route: Route) => boolean): RouteInfo[] {
return this.routes.filter(filter || (() => true)).map((r) => {
const pickValues = pick(r, pickValue as any);
return pickValues;
});
}
/**
* 获取handle函数, 这里会去执行parse函数
*/
getHandle(router: QueryRouter, wrapperFn?: HandleFn, ctx?: RouteContext) {
return async (msg: { id?: string; path?: string; key?: string;[key: string]: any }, handleContext?: RouteContext) => {
try {
const context = { ...ctx, ...handleContext };
const res = await router.call(msg, context) as any;
if (wrapperFn) {
res.data = res.body;
return wrapperFn(res, context);
}
const { code, body, message } = res;
return { code, data: body, message };
} catch (e) {
return { code: 500, message: e.message };
}
};
}
exportRoutes() {
return this.routes.map((r) => {
return r;
});
}
importRoutes(routes: Route[]) {
for (let route of routes) {
this.add(route);
}
}
importRouter(router: QueryRouter) {
this.importRoutes(router.routes);
}
throw(...args: any[]) {
CustomError.throw(...args);
}
hasRoute(path: string, key: string = '') {
return this.routes.find((r) => r.path === path && r.key === key);
}
findRoute(opts?: { path?: string; key?: string; id?: string }) {
const { path, key, id } = opts || {};
return this.routes.find((r) => {
if (id) {
return r.id === id;
}
if (path) {
if (key !== undefined) {
return r.path === path && r.key === key;
}
return r.path === path;
}
return false;
});
}
createRouteList(opts?: { force?: boolean, filter?: (route: Route) => boolean, middleware?: string[] }) {
const hasListRoute = this.hasRoute('router', 'list');
if (!hasListRoute || opts?.force) {
const listRoute = new Route('router', 'list', {
description: '列出当前应用下的所有的路由信息',
middleware: opts?.middleware || [],
run: async (ctx: RouteContext) => {
const tokenUser = ctx.state as unknown as { tokenUser?: any };
let isUser = !!tokenUser;
const list = this.getList(opts?.filter).filter((item) => {
if (item.id === 'auth' || item.id === 'auth-can' || item.id === 'check-auth-admin' || item.id === 'auth-admin') {
return false;
}
return true;
});
ctx.body = {
list: list.map((item) => {
const route = pick(item, ['id', 'path', 'key', 'description', 'middleware', 'metadata'] as const);
return toJSONSchemaRoute(route);
}),
isUser
};
},
});
this.add(listRoute);
}
}
/**
* 等待程序运行, 获取到message的数据,就执行
* params 是预设参数
* emitter = process
* -- .exit
* -- .on
* -- .send
*/
wait(params?: { message: RunMessage }, opts?: {
mockProcess?: MockProcess,
timeout?: number,
getList?: boolean
force?: boolean
filter?: (route: Route) => boolean
routeListMiddleware?: string[]
}) {
const getList = opts?.getList ?? true;
if (getList) {
this.createRouteList({ force: opts?.force, filter: opts?.filter, middleware: opts?.routeListMiddleware });
}
return listenProcess({ app: this as any, params, ...opts });
}
toJSONSchema = toJSONSchema;
fromJSONSchema = fromJSONSchema;
}
type QueryRouterServerOpts = {
handleFn?: HandleFn;
context?: RouteContext;
appId?: string;
initHandle?: boolean;
};
interface HandleFn {
(msg: { path: string;[key: string]: any }, ctx?: any): { code: string; data?: any; message?: string;[key: string]: any };
(res: RouteContext): any;
}
/**
* QueryRouterServer
* @description 移除server相关的功能,只保留router相关的功能,和http.createServer不相关,独立
* @template C 自定义 RouteContext 类型
*/
export class QueryRouterServer extends QueryRouter {
declare appId: string;
handle: any;
declare context: RouteContext;
constructor(opts?: QueryRouterServerOpts) {
super();
const initHandle = opts?.initHandle ?? true;
if (initHandle || opts?.handleFn) {
this.handle = this.getHandle(this, opts?.handleFn, opts?.context);
}
this.setContext({ needSerialize: false, ...opts?.context });
if (opts?.appId) {
this.appId = opts.appId;
} else {
this.appId = randomId(16);
}
}
setHandle(wrapperFn?: HandleFn, ctx?: RouteContext) {
this.handle = this.getHandle(this, wrapperFn, ctx);
}
addRoute(route: Route, opts?: AddOpts) {
this.add(route, opts);
}
Route = Route;
route(opts: RouteOpts & { metadata?: M }): Route>>;
route(path: string, opts?: RouteOpts & { metadata?: M }): Route>>;
route(path: string, key?: string): Route>>;
route(path: string, key?: string, opts?: RouteOpts & { metadata?: M }): Route>>;
route(...args: any[]) {
const [path, key, opts] = args;
if (typeof path === 'object') {
return new Route>>(path.path, path.key, path);
}
if (typeof path === 'string') {
if (opts) {
return new Route>>(path, key, opts);
}
if (key && typeof key === 'object') {
return new Route>>(path, key?.key || '', key);
}
return new Route>>(path, key);
}
return new Route>>(path, key, opts);
}
prompt(description: string) {
return new Route(undefined, undefined, { description });
}
/**
* 调用了handle
* @param param0
* @returns
*/
async run(msg: { id?: string; path?: string; key?: string; payload?: any }, ctx?: Partial>) {
const handle = this.handle;
if (handle) {
return handle(msg, ctx);
}
return super.run(msg, ctx as RouteContext);
}
}
export class Mini extends QueryRouterServer { }