@kevisual/query 0.0.40 → 0.0.42
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 -124
- package/dist/query-api.d.ts +317 -0
- package/dist/query-api.js +449 -0
- package/dist/query-browser.d.ts +1 -8
- package/dist/query-browser.js +489 -582
- package/dist/query-ws.js +163 -181
- package/dist/query.d.ts +1 -8
- package/dist/query.js +278 -359
- package/package.json +13 -25
- package/src/adapter.ts +1 -1
- package/src/create-query/index.ts +159 -0
- package/src/query-api.ts +136 -0
- package/src/query-browser.ts +2 -2
- package/src/query.ts +1 -10
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.42",
|
|
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,11 +18,12 @@
|
|
|
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",
|
|
25
|
+
"es-toolkit": "^1.44.0",
|
|
26
|
+
"zod": "^4.3.6",
|
|
29
27
|
"zustand": "^5.0.11"
|
|
30
28
|
},
|
|
31
29
|
"publishConfig": {
|
|
@@ -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,7 +60,7 @@ 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 =
|
|
63
|
+
origin = globalThis?.location?.origin || 'http://localhost:51515';
|
|
64
64
|
url = new URL(opts?.url || '', origin);
|
|
65
65
|
}
|
|
66
66
|
const isGet = method === 'GET';
|
|
@@ -0,0 +1,159 @@
|
|
|
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
|
+
viewItem?: {
|
|
11
|
+
type?: string;
|
|
12
|
+
api?: {
|
|
13
|
+
query?: any;
|
|
14
|
+
},
|
|
15
|
+
worker?: {
|
|
16
|
+
worker?: any;
|
|
17
|
+
},
|
|
18
|
+
context?: {
|
|
19
|
+
router?: any;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const removeViewItemFromRoutes = (list: RouteInfo[]) => {
|
|
25
|
+
for (const route of list) {
|
|
26
|
+
if (route.metadata?.viewItem) {
|
|
27
|
+
if (route.metadata.viewItem?.api?.query) {
|
|
28
|
+
delete route.metadata.viewItem.api.query;
|
|
29
|
+
}
|
|
30
|
+
if (route.metadata.viewItem?.worker?.worker) {
|
|
31
|
+
delete route.metadata.viewItem.worker.worker;
|
|
32
|
+
}
|
|
33
|
+
if (route.metadata.viewItem?.context?.router) {
|
|
34
|
+
delete route.metadata.viewItem.context.router;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return list;
|
|
39
|
+
}
|
|
40
|
+
export const createQueryByRoutes = (list: RouteInfo[]) => {
|
|
41
|
+
const obj: any = {};
|
|
42
|
+
list = removeViewItemFromRoutes(list);
|
|
43
|
+
for (const route of list) {
|
|
44
|
+
if (!obj[route.path]) {
|
|
45
|
+
obj[route.path] = {};
|
|
46
|
+
}
|
|
47
|
+
obj[route.path][route.key] = route;
|
|
48
|
+
}
|
|
49
|
+
const code = `
|
|
50
|
+
import { createQueryApi } from '@kevisual/query/api';
|
|
51
|
+
const api = ${generateApiCode(obj)} as const;
|
|
52
|
+
const queryApi = createQueryApi({ api });
|
|
53
|
+
export { queryApi };
|
|
54
|
+
`
|
|
55
|
+
return code;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 生成带注释的对象字符串
|
|
59
|
+
function generateApiCode(obj: any): string {
|
|
60
|
+
let code = '{\n';
|
|
61
|
+
const paths = Object.keys(obj);
|
|
62
|
+
|
|
63
|
+
for (let i = 0; i < paths.length; i++) {
|
|
64
|
+
const path = paths[i];
|
|
65
|
+
const methods = obj[path];
|
|
66
|
+
|
|
67
|
+
code += ` "${path}": {\n`;
|
|
68
|
+
|
|
69
|
+
const keys = Object.keys(methods);
|
|
70
|
+
for (let j = 0; j < keys.length; j++) {
|
|
71
|
+
const key = keys[j];
|
|
72
|
+
const route = methods[key];
|
|
73
|
+
if (route?.id) {
|
|
74
|
+
if (route.id.startsWith('rand-')) {
|
|
75
|
+
delete route.id; // 删除随机生成的 ID
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const description = route?.metadata?.summary || route?.description || '';
|
|
79
|
+
const args = route?.metadata?.args || {};
|
|
80
|
+
|
|
81
|
+
// 添加 JSDoc 注释
|
|
82
|
+
if (description || Object.keys(args).length > 0) {
|
|
83
|
+
code += ` /**\n`;
|
|
84
|
+
|
|
85
|
+
// 添加主描述
|
|
86
|
+
if (description) {
|
|
87
|
+
// 转义描述中的特殊字符
|
|
88
|
+
const escapedDescription = description
|
|
89
|
+
.replace(/\\/g, '\\\\') // 转义反斜杠
|
|
90
|
+
.replace(/\*/g, '\\*') // 转义星号
|
|
91
|
+
.replace(/\n/g, '\n * '); // 处理多行描述
|
|
92
|
+
code += ` * ${escapedDescription}\n`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 添加参数描述
|
|
96
|
+
if (Object.keys(args).length > 0) {
|
|
97
|
+
if (description) {
|
|
98
|
+
code += ` *\n`; // 添加空行分隔
|
|
99
|
+
}
|
|
100
|
+
code += ` * @param data - Request parameters\n`;
|
|
101
|
+
|
|
102
|
+
for (const [argName, schema] of Object.entries(args)) {
|
|
103
|
+
const argSchema = schema as any;
|
|
104
|
+
const argType = argSchema.type || 'unknown';
|
|
105
|
+
const argDesc = argSchema.description || '';
|
|
106
|
+
|
|
107
|
+
// 构建类型信息
|
|
108
|
+
let typeInfo = argType;
|
|
109
|
+
if (argType === 'string' && argSchema.enum) {
|
|
110
|
+
typeInfo = argSchema.enum.map((v: any) => `"${v}"`).join(' | ');
|
|
111
|
+
} else if (argType === 'number' || argType === 'integer') {
|
|
112
|
+
const constraints = [];
|
|
113
|
+
if (argSchema.minimum !== undefined) constraints.push(`min: ${argSchema.minimum}`);
|
|
114
|
+
if (argSchema.maximum !== undefined) constraints.push(`max: ${argSchema.maximum}`);
|
|
115
|
+
if (argSchema.exclusiveMinimum !== undefined) constraints.push(`> ${argSchema.exclusiveMinimum}`);
|
|
116
|
+
if (argSchema.exclusiveMaximum !== undefined) constraints.push(`< ${argSchema.exclusiveMaximum}`);
|
|
117
|
+
if (constraints.length > 0) typeInfo += ` (${constraints.join(', ')})`;
|
|
118
|
+
} else if (argType === 'string') {
|
|
119
|
+
const constraints = [];
|
|
120
|
+
if (argSchema.minLength !== undefined) constraints.push(`minLength: ${argSchema.minLength}`);
|
|
121
|
+
if (argSchema.maxLength !== undefined) constraints.push(`maxLength: ${argSchema.maxLength}`);
|
|
122
|
+
if (argSchema.format) constraints.push(`format: ${argSchema.format}`);
|
|
123
|
+
if (constraints.length > 0) typeInfo += ` (${constraints.join(', ')})`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 转义参数描述
|
|
127
|
+
const escapedArgDesc = argDesc
|
|
128
|
+
.replace(/\\/g, '\\\\')
|
|
129
|
+
.replace(/\*/g, '\\*')
|
|
130
|
+
.replace(/\n/g, ' ');
|
|
131
|
+
|
|
132
|
+
code += ` * @param data.${argName} - {${typeInfo}}${escapedArgDesc ? ' ' + escapedArgDesc : ''}\n`;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
code += ` */\n`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
code += ` "${key}": ${JSON.stringify(route, null, 2).split('\n').map((line, idx) =>
|
|
140
|
+
idx === 0 ? line : ' ' + line
|
|
141
|
+
).join('\n')}`;
|
|
142
|
+
|
|
143
|
+
if (j < keys.length - 1) {
|
|
144
|
+
code += ',';
|
|
145
|
+
}
|
|
146
|
+
code += '\n';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
code += ` }`;
|
|
150
|
+
if (i < paths.length - 1) {
|
|
151
|
+
code += ',';
|
|
152
|
+
}
|
|
153
|
+
code += '\n';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
code += '}';
|
|
157
|
+
return code;
|
|
158
|
+
}
|
|
159
|
+
|
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
|
@@ -162,6 +162,7 @@ export class Query {
|
|
|
162
162
|
*/
|
|
163
163
|
async post<R = any, P = any>(body: Data & P, options?: DataOpts): Promise<Result<R>> {
|
|
164
164
|
const url = options?.url || this.url;
|
|
165
|
+
console.log('query post', url, body, options);
|
|
165
166
|
const { headers, adapter, beforeRequest, afterResponse, timeout, ...rest } = options || {};
|
|
166
167
|
const _headers = { ...this.headers, ...headers };
|
|
167
168
|
const _adapter = adapter || this.adapter;
|
|
@@ -301,13 +302,3 @@ export class BaseQuery<T extends Query = Query, R extends { queryChain?: any; qu
|
|
|
301
302
|
return this.query.get(data, options);
|
|
302
303
|
}
|
|
303
304
|
}
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* @deprecated
|
|
307
|
-
* 前端调用后端QueryRouter, 默认路径 /client/router
|
|
308
|
-
*/
|
|
309
|
-
export class ClientQuery extends Query {
|
|
310
|
-
constructor(opts?: QueryOpts) {
|
|
311
|
-
super({ ...opts, url: opts?.url || '/client/router' });
|
|
312
|
-
}
|
|
313
|
-
}
|