@kevisual/query 0.0.39 → 0.0.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/query-adapter.js +107 -122
- package/dist/query-api.d.ts +305 -0
- package/dist/query-api.js +432 -0
- package/dist/query-browser.d.ts +4 -8
- package/dist/query-browser.js +483 -571
- package/dist/query-ws.js +163 -181
- package/dist/query.d.ts +4 -8
- package/dist/query.js +278 -354
- package/package.json +14 -26
- package/src/adapter.ts +10 -8
- package/src/create-query/index.ts +130 -0
- package/src/query-api.ts +136 -0
- package/src/query-browser.ts +2 -2
- package/src/query.ts +5 -11
package/package.json
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kevisual/query",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"main": "dist/query-browser.js",
|
|
5
|
-
"private": false,
|
|
3
|
+
"version": "0.0.41",
|
|
6
4
|
"type": "module",
|
|
7
5
|
"scripts": {
|
|
8
|
-
"build": "npm run clean &&
|
|
9
|
-
"dev:lib": "rollup -c -w",
|
|
6
|
+
"build": "npm run clean && bun run bun.config.ts",
|
|
10
7
|
"clean": "rm -rf dist"
|
|
11
8
|
},
|
|
12
9
|
"files": [
|
|
@@ -21,12 +18,13 @@
|
|
|
21
18
|
"license": "ISC",
|
|
22
19
|
"description": "",
|
|
23
20
|
"devDependencies": {
|
|
24
|
-
"@
|
|
25
|
-
"@
|
|
26
|
-
"
|
|
27
|
-
"rollup-plugin-dts": "^6.3.0",
|
|
21
|
+
"@kevisual/code-builder": "^0.0.6",
|
|
22
|
+
"@kevisual/router": "^0.0.72",
|
|
23
|
+
"@types/node": "^25.2.3",
|
|
28
24
|
"typescript": "^5.9.3",
|
|
29
|
-
"
|
|
25
|
+
"es-toolkit": "^1.44.0",
|
|
26
|
+
"zod": "^4.3.6",
|
|
27
|
+
"zustand": "^5.0.11"
|
|
30
28
|
},
|
|
31
29
|
"publishConfig": {
|
|
32
30
|
"access": "public"
|
|
@@ -36,20 +34,10 @@
|
|
|
36
34
|
"url": "git+ssh://git@github.com/abearxiong/kevisual-query.git"
|
|
37
35
|
},
|
|
38
36
|
"exports": {
|
|
39
|
-
".":
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
"./query": {
|
|
44
|
-
"import": "./dist/query.js",
|
|
45
|
-
"require": "./dist/query.js"
|
|
46
|
-
},
|
|
47
|
-
"./ws": {
|
|
48
|
-
"import": "./dist/query-ws.js",
|
|
49
|
-
"require": "./dist/query-ws.js"
|
|
50
|
-
}
|
|
37
|
+
".": "./dist/query-browser.js",
|
|
38
|
+
"./query": "./dist/query.js",
|
|
39
|
+
"./ws": "./dist/query-ws.js",
|
|
40
|
+
"./api": "./dist/query-api.js"
|
|
51
41
|
},
|
|
52
|
-
"dependencies": {
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
}
|
|
42
|
+
"dependencies": {}
|
|
43
|
+
}
|
package/src/adapter.ts
CHANGED
|
@@ -60,13 +60,13 @@ export const adapter = async (opts: AdapterOpts = {}, overloadOpts?: RequestInit
|
|
|
60
60
|
if (opts?.url?.startsWith('http')) {
|
|
61
61
|
url = new URL(opts.url);
|
|
62
62
|
} else {
|
|
63
|
-
origin =
|
|
64
|
-
url = new URL(opts
|
|
63
|
+
origin = globalThis?.location?.origin || 'http://localhost:51515';
|
|
64
|
+
url = new URL(opts?.url || '', origin);
|
|
65
65
|
}
|
|
66
66
|
const isGet = method === 'GET';
|
|
67
67
|
const oldSearchParams = url.searchParams;
|
|
68
68
|
if (isGet) {
|
|
69
|
-
let searchParams = new URLSearchParams({ ...Object.fromEntries(oldSearchParams), ...opts
|
|
69
|
+
let searchParams = new URLSearchParams({ ...Object.fromEntries(oldSearchParams), ...opts?.params, ...opts?.body } as SimpleObject);
|
|
70
70
|
url.search = searchParams.toString();
|
|
71
71
|
} else {
|
|
72
72
|
const params = {
|
|
@@ -92,11 +92,13 @@ export const adapter = async (opts: AdapterOpts = {}, overloadOpts?: RequestInit
|
|
|
92
92
|
} else if (isPostFile) {
|
|
93
93
|
body = opts.body as FormData; // 如果是文件上传,直接使用 FormData
|
|
94
94
|
} else {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
95
|
+
if (opts.body && typeof opts.body === 'object' && !(opts.body instanceof FormData)) {
|
|
96
|
+
headers = {
|
|
97
|
+
'Content-Type': 'application/json',
|
|
98
|
+
...headers,
|
|
99
|
+
};
|
|
100
|
+
body = JSON.stringify(opts.body); // 否则将对象转换为 JSON 字符串
|
|
101
|
+
}
|
|
100
102
|
}
|
|
101
103
|
return fetch(url, {
|
|
102
104
|
method: method.toUpperCase(),
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
|
|
2
|
+
type RouteInfo = {
|
|
3
|
+
path: string;
|
|
4
|
+
key: string;
|
|
5
|
+
id: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
metadata?: {
|
|
8
|
+
summary?: string;
|
|
9
|
+
args?: Record<string, any>;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export const createQueryByRoutes = (list: RouteInfo[]) => {
|
|
13
|
+
const obj: any = {};
|
|
14
|
+
for (const route of list) {
|
|
15
|
+
if (!obj[route.path]) {
|
|
16
|
+
obj[route.path] = {};
|
|
17
|
+
}
|
|
18
|
+
obj[route.path][route.key] = route;
|
|
19
|
+
}
|
|
20
|
+
const code = `
|
|
21
|
+
import { createQueryApi } from '@kevisual/query/api';
|
|
22
|
+
const api = ${generateApiCode(obj)} as const;
|
|
23
|
+
const queryApi = createQueryApi({ api });
|
|
24
|
+
export { queryApi };
|
|
25
|
+
`
|
|
26
|
+
return code;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 生成带注释的对象字符串
|
|
30
|
+
function generateApiCode(obj: any): string {
|
|
31
|
+
let code = '{\n';
|
|
32
|
+
const paths = Object.keys(obj);
|
|
33
|
+
|
|
34
|
+
for (let i = 0; i < paths.length; i++) {
|
|
35
|
+
const path = paths[i];
|
|
36
|
+
const methods = obj[path];
|
|
37
|
+
|
|
38
|
+
code += ` "${path}": {\n`;
|
|
39
|
+
|
|
40
|
+
const keys = Object.keys(methods);
|
|
41
|
+
for (let j = 0; j < keys.length; j++) {
|
|
42
|
+
const key = keys[j];
|
|
43
|
+
const route = methods[key];
|
|
44
|
+
if (route?.id) {
|
|
45
|
+
if (route.id.startsWith('rand-')) {
|
|
46
|
+
delete route.id; // 删除随机生成的 ID
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const description = route?.metadata?.summary || route?.description || '';
|
|
50
|
+
const args = route?.metadata?.args || {};
|
|
51
|
+
|
|
52
|
+
// 添加 JSDoc 注释
|
|
53
|
+
if (description || Object.keys(args).length > 0) {
|
|
54
|
+
code += ` /**\n`;
|
|
55
|
+
|
|
56
|
+
// 添加主描述
|
|
57
|
+
if (description) {
|
|
58
|
+
// 转义描述中的特殊字符
|
|
59
|
+
const escapedDescription = description
|
|
60
|
+
.replace(/\\/g, '\\\\') // 转义反斜杠
|
|
61
|
+
.replace(/\*/g, '\\*') // 转义星号
|
|
62
|
+
.replace(/\n/g, '\n * '); // 处理多行描述
|
|
63
|
+
code += ` * ${escapedDescription}\n`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 添加参数描述
|
|
67
|
+
if (Object.keys(args).length > 0) {
|
|
68
|
+
if (description) {
|
|
69
|
+
code += ` *\n`; // 添加空行分隔
|
|
70
|
+
}
|
|
71
|
+
code += ` * @param data - Request parameters\n`;
|
|
72
|
+
|
|
73
|
+
for (const [argName, schema] of Object.entries(args)) {
|
|
74
|
+
const argSchema = schema as any;
|
|
75
|
+
const argType = argSchema.type || 'unknown';
|
|
76
|
+
const argDesc = argSchema.description || '';
|
|
77
|
+
|
|
78
|
+
// 构建类型信息
|
|
79
|
+
let typeInfo = argType;
|
|
80
|
+
if (argType === 'string' && argSchema.enum) {
|
|
81
|
+
typeInfo = argSchema.enum.map((v: any) => `"${v}"`).join(' | ');
|
|
82
|
+
} else if (argType === 'number' || argType === 'integer') {
|
|
83
|
+
const constraints = [];
|
|
84
|
+
if (argSchema.minimum !== undefined) constraints.push(`min: ${argSchema.minimum}`);
|
|
85
|
+
if (argSchema.maximum !== undefined) constraints.push(`max: ${argSchema.maximum}`);
|
|
86
|
+
if (argSchema.exclusiveMinimum !== undefined) constraints.push(`> ${argSchema.exclusiveMinimum}`);
|
|
87
|
+
if (argSchema.exclusiveMaximum !== undefined) constraints.push(`< ${argSchema.exclusiveMaximum}`);
|
|
88
|
+
if (constraints.length > 0) typeInfo += ` (${constraints.join(', ')})`;
|
|
89
|
+
} else if (argType === 'string') {
|
|
90
|
+
const constraints = [];
|
|
91
|
+
if (argSchema.minLength !== undefined) constraints.push(`minLength: ${argSchema.minLength}`);
|
|
92
|
+
if (argSchema.maxLength !== undefined) constraints.push(`maxLength: ${argSchema.maxLength}`);
|
|
93
|
+
if (argSchema.format) constraints.push(`format: ${argSchema.format}`);
|
|
94
|
+
if (constraints.length > 0) typeInfo += ` (${constraints.join(', ')})`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 转义参数描述
|
|
98
|
+
const escapedArgDesc = argDesc
|
|
99
|
+
.replace(/\\/g, '\\\\')
|
|
100
|
+
.replace(/\*/g, '\\*')
|
|
101
|
+
.replace(/\n/g, ' ');
|
|
102
|
+
|
|
103
|
+
code += ` * @param data.${argName} - {${typeInfo}}${escapedArgDesc ? ' ' + escapedArgDesc : ''}\n`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
code += ` */\n`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
code += ` "${key}": ${JSON.stringify(route, null, 2).split('\n').map((line, idx) =>
|
|
111
|
+
idx === 0 ? line : ' ' + line
|
|
112
|
+
).join('\n')}`;
|
|
113
|
+
|
|
114
|
+
if (j < keys.length - 1) {
|
|
115
|
+
code += ',';
|
|
116
|
+
}
|
|
117
|
+
code += '\n';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
code += ` }`;
|
|
121
|
+
if (i < paths.length - 1) {
|
|
122
|
+
code += ',';
|
|
123
|
+
}
|
|
124
|
+
code += '\n';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
code += '}';
|
|
128
|
+
return code;
|
|
129
|
+
}
|
|
130
|
+
|
package/src/query-api.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { DataOpts, Query } from "./query.ts";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { createQueryByRoutes } from "./create-query/index.ts";
|
|
4
|
+
import { pick } from 'es-toolkit'
|
|
5
|
+
type Pos = {
|
|
6
|
+
path?: string;
|
|
7
|
+
key?: string;
|
|
8
|
+
id?: string;
|
|
9
|
+
metadata?: {
|
|
10
|
+
args?: Record<string, any>;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// JSON Schema 类型推断 - 使用更精确的类型匹配
|
|
15
|
+
type InferFromJSONSchema<T> =
|
|
16
|
+
T extends { type: "string"; enum: readonly (infer E)[] } ? E :
|
|
17
|
+
T extends { type: "string"; enum: (infer E)[] } ? E :
|
|
18
|
+
T extends { type: "string" } ? string :
|
|
19
|
+
T extends { type: "number" } ? number :
|
|
20
|
+
T extends { type: "integer" } ? number :
|
|
21
|
+
T extends { type: "boolean" } ? boolean :
|
|
22
|
+
T extends { type: "object"; properties: infer P }
|
|
23
|
+
? { [K in keyof P]: InferFromJSONSchema<P[K]> }
|
|
24
|
+
: T extends { type: "array"; items: infer I }
|
|
25
|
+
? Array<InferFromJSONSchema<I>>
|
|
26
|
+
: unknown;
|
|
27
|
+
|
|
28
|
+
// 统一类型推断:支持 Zod schema 和原始 JSON Schema
|
|
29
|
+
type InferType<T> =
|
|
30
|
+
T extends z.ZodType<infer U> ? U : // Zod schema
|
|
31
|
+
T extends { type: infer TType } ? InferFromJSONSchema<T> : // 任何包含 type 字段的 JSON Schema(忽略 $schema)
|
|
32
|
+
T;
|
|
33
|
+
|
|
34
|
+
// 提取 args 对象,将每个 Zod schema 或 JSON Schema 转换为实际类型
|
|
35
|
+
type ExtractArgsFromMetadata<T> = T extends { metadata?: { args?: infer A } }
|
|
36
|
+
? A extends Record<string, any>
|
|
37
|
+
? { [K in keyof A]: InferType<A[K]> }
|
|
38
|
+
: never
|
|
39
|
+
: never;
|
|
40
|
+
|
|
41
|
+
// 类型映射:将 API 配置转换为方法签名
|
|
42
|
+
type ApiMethods<P extends { [path: string]: { [key: string]: Pos } }> = {
|
|
43
|
+
[Path in keyof P]: {
|
|
44
|
+
[Key in keyof P[Path]]: (
|
|
45
|
+
data?: Partial<ExtractArgsFromMetadata<P[Path][Key]>>,
|
|
46
|
+
opts?: DataOpts
|
|
47
|
+
) => ReturnType<Query['post']>
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
type QueryApiOpts<P extends { [path: string]: { [key: string]: Pos } } = {}> = {
|
|
51
|
+
query?: Query,
|
|
52
|
+
api?: P
|
|
53
|
+
}
|
|
54
|
+
export class QueryApi<P extends { [path: string]: { [key: string]: Pos } } = {}> {
|
|
55
|
+
query: Query;
|
|
56
|
+
|
|
57
|
+
constructor(opts?: QueryApiOpts<P>) {
|
|
58
|
+
this.query = opts?.query ?? new Query();
|
|
59
|
+
if (opts?.api) {
|
|
60
|
+
this.createApi(opts.api);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 使用泛型来推断类型
|
|
65
|
+
post<T extends Pos>(
|
|
66
|
+
pos: T,
|
|
67
|
+
data?: Partial<ExtractArgsFromMetadata<T>>,
|
|
68
|
+
opts?: DataOpts
|
|
69
|
+
) {
|
|
70
|
+
const _pos = pick(pos, ['path', 'key', 'id']);
|
|
71
|
+
return this.query.post({
|
|
72
|
+
..._pos,
|
|
73
|
+
payload: data
|
|
74
|
+
}, opts)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
createApi(api: P): asserts this is this & ApiMethods<P> {
|
|
78
|
+
const that = this as any;
|
|
79
|
+
const apiEntries = Object.entries(api);
|
|
80
|
+
const keepPaths = ['createApi', 'query', 'post'];
|
|
81
|
+
|
|
82
|
+
for (const [path, methods] of apiEntries) {
|
|
83
|
+
if (keepPaths.includes(path)) continue;
|
|
84
|
+
|
|
85
|
+
// 为每个 path 创建命名空间对象
|
|
86
|
+
if (!that[path]) {
|
|
87
|
+
that[path] = {};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
for (const [key, pos] of Object.entries(methods)) {
|
|
91
|
+
that[path][key] = (data?: Partial<ExtractArgsFromMetadata<typeof pos>>, opts?: DataOpts) => {
|
|
92
|
+
const _pos = pick(pos, ['path', 'key', 'id']);
|
|
93
|
+
return that.query.post({
|
|
94
|
+
..._pos,
|
|
95
|
+
payload: data
|
|
96
|
+
}, opts);
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 创建工厂函数,提供更好的类型推断
|
|
104
|
+
export function createQueryApi<P extends { [path: string]: { [key: string]: Pos } }>(
|
|
105
|
+
opts?: QueryApiOpts<P>
|
|
106
|
+
): QueryApi<P> & ApiMethods<P> {
|
|
107
|
+
return new QueryApi(opts) as QueryApi<P> & ApiMethods<P>;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export { createQueryByRoutes };
|
|
111
|
+
// const demo = {
|
|
112
|
+
// "test_path": {
|
|
113
|
+
// "test_key": {
|
|
114
|
+
// "path": "demo",
|
|
115
|
+
// "key": "test",
|
|
116
|
+
// metadata: {
|
|
117
|
+
// args: {
|
|
118
|
+
// name: z.string(),
|
|
119
|
+
// age: z.number(),
|
|
120
|
+
// }
|
|
121
|
+
// }
|
|
122
|
+
// }
|
|
123
|
+
// }
|
|
124
|
+
// } as const;
|
|
125
|
+
|
|
126
|
+
// // 方式1: 使用工厂函数创建(推荐)
|
|
127
|
+
// const queryApi = createQueryApi({ query: new Query(), api: demo });
|
|
128
|
+
|
|
129
|
+
// // 现在调用时会有完整的类型推断
|
|
130
|
+
// // data 参数会被推断为 { name?: string, age?: number }
|
|
131
|
+
// queryApi.test_path.test_key({ name: "test", age: 18 });
|
|
132
|
+
// // 也可以不传参数
|
|
133
|
+
// queryApi.test_path.test_key();
|
|
134
|
+
|
|
135
|
+
// // 或者只传递 opts
|
|
136
|
+
// queryApi.test_path.test_key(undefined, { timeout: 5000 });
|
package/src/query-browser.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { adapter } from './adapter.ts';
|
|
2
2
|
import { QueryWs, QueryWsOpts } from './ws.ts';
|
|
3
|
-
import { Query
|
|
3
|
+
import { Query } from './query.ts';
|
|
4
4
|
import { BaseQuery, QueryOptions, wrapperError } from './query.ts';
|
|
5
5
|
|
|
6
|
-
export { QueryOpts, QueryWs,
|
|
6
|
+
export { QueryOpts, QueryWs, Query, QueryWsOpts, adapter, BaseQuery, wrapperError };
|
|
7
7
|
export { QueryOptions }
|
|
8
8
|
export type { DataOpts, Result, Data } from './query.ts';
|
|
9
9
|
|
package/src/query.ts
CHANGED
|
@@ -105,6 +105,9 @@ export class Query {
|
|
|
105
105
|
stop?: boolean;
|
|
106
106
|
// 默认不使用ws
|
|
107
107
|
qws: QueryWs;
|
|
108
|
+
/**
|
|
109
|
+
* 默认是 /client/router或者 默认是 /api/router
|
|
110
|
+
*/
|
|
108
111
|
isClient = false;
|
|
109
112
|
constructor(opts?: QueryOptions) {
|
|
110
113
|
this.adapter = opts?.adapter || adapter;
|
|
@@ -114,7 +117,7 @@ export class Query {
|
|
|
114
117
|
'Content-Type': 'application/json',
|
|
115
118
|
};
|
|
116
119
|
this.timeout = opts?.timeout || 60000 * 3; // 默认超时时间为 60s * 3
|
|
117
|
-
if (opts
|
|
120
|
+
if (opts?.beforeRequest) {
|
|
118
121
|
this.beforeRequest = opts.beforeRequest;
|
|
119
122
|
} else {
|
|
120
123
|
this.beforeRequest = async (opts) => {
|
|
@@ -159,6 +162,7 @@ export class Query {
|
|
|
159
162
|
*/
|
|
160
163
|
async post<R = any, P = any>(body: Data & P, options?: DataOpts): Promise<Result<R>> {
|
|
161
164
|
const url = options?.url || this.url;
|
|
165
|
+
console.log('query post', url, body, options);
|
|
162
166
|
const { headers, adapter, beforeRequest, afterResponse, timeout, ...rest } = options || {};
|
|
163
167
|
const _headers = { ...this.headers, ...headers };
|
|
164
168
|
const _adapter = adapter || this.adapter;
|
|
@@ -298,13 +302,3 @@ export class BaseQuery<T extends Query = Query, R extends { queryChain?: any; qu
|
|
|
298
302
|
return this.query.get(data, options);
|
|
299
303
|
}
|
|
300
304
|
}
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* @deprecated
|
|
304
|
-
* 前端调用后端QueryRouter, 默认路径 /client/router
|
|
305
|
-
*/
|
|
306
|
-
export class ClientQuery extends Query {
|
|
307
|
-
constructor(opts?: QueryOpts) {
|
|
308
|
-
super({ ...opts, url: opts?.url || '/client/router' });
|
|
309
|
-
}
|
|
310
|
-
}
|