@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/package.json CHANGED
@@ -1,12 +1,9 @@
1
1
  {
2
2
  "name": "@kevisual/query",
3
- "version": "0.0.39",
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 && rollup -c",
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
- "@rollup/plugin-node-resolve": "^16.0.3",
25
- "@rollup/plugin-typescript": "^12.3.0",
26
- "rollup": "^4.55.3",
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
- "zustand": "^5.0.10"
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
- "import": "./dist/query-browser.js",
41
- "require": "./dist/query-browser.js"
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
- "tslib": "^2.8.1"
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 = window?.location?.origin || 'http://localhost:51515';
64
- url = new URL(opts.url, origin);
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.body } as SimpleObject);
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
- headers = {
96
- 'Content-Type': 'application/json',
97
- ...headers,
98
- };
99
- body = JSON.stringify(opts.body); // 否则将对象转换为 JSON 字符串
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
+
@@ -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 });
@@ -1,9 +1,9 @@
1
1
  import { adapter } from './adapter.ts';
2
2
  import { QueryWs, QueryWsOpts } from './ws.ts';
3
- import { Query, ClientQuery } from './query.ts';
3
+ import { Query } from './query.ts';
4
4
  import { BaseQuery, QueryOptions, wrapperError } from './query.ts';
5
5
 
6
- export { QueryOpts, QueryWs, ClientQuery, Query, QueryWsOpts, adapter, BaseQuery, wrapperError };
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.beforeRequest) {
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
- }