refactor: migrate from Rollup to Bun for build configuration
feat: update adapter to use globalThis for origin resolution fix: remove unused ClientQuery export from query.ts chore: update tsconfig to include test files and set rootDir feat: add create-query functionality for dynamic API generation feat: implement QueryApi with enhanced type inference from JSON Schema test: add comprehensive API tests for QueryApi functionality test: create demo routes and schemas for testing purposes docs: add type inference demo for QueryApi usage
This commit is contained in:
136
src/query-api.ts
Normal file
136
src/query-api.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import { DataOpts, Query } from "./query.ts";
|
||||
import { z } from "zod";
|
||||
import { createQueryByRoutes } from "./create-query/index.ts";
|
||||
import { pick } from 'es-toolkit'
|
||||
type Pos = {
|
||||
path?: string;
|
||||
key?: string;
|
||||
id?: string;
|
||||
metadata?: {
|
||||
args?: Record<string, any>;
|
||||
};
|
||||
}
|
||||
|
||||
// JSON Schema 类型推断 - 使用更精确的类型匹配
|
||||
type InferFromJSONSchema<T> =
|
||||
T extends { type: "string"; enum: readonly (infer E)[] } ? E :
|
||||
T extends { type: "string"; enum: (infer E)[] } ? E :
|
||||
T extends { type: "string" } ? string :
|
||||
T extends { type: "number" } ? number :
|
||||
T extends { type: "integer" } ? number :
|
||||
T extends { type: "boolean" } ? boolean :
|
||||
T extends { type: "object"; properties: infer P }
|
||||
? { [K in keyof P]: InferFromJSONSchema<P[K]> }
|
||||
: T extends { type: "array"; items: infer I }
|
||||
? Array<InferFromJSONSchema<I>>
|
||||
: unknown;
|
||||
|
||||
// 统一类型推断:支持 Zod schema 和原始 JSON Schema
|
||||
type InferType<T> =
|
||||
T extends z.ZodType<infer U> ? U : // Zod schema
|
||||
T extends { type: infer TType } ? InferFromJSONSchema<T> : // 任何包含 type 字段的 JSON Schema(忽略 $schema)
|
||||
T;
|
||||
|
||||
// 提取 args 对象,将每个 Zod schema 或 JSON Schema 转换为实际类型
|
||||
type ExtractArgsFromMetadata<T> = T extends { metadata?: { args?: infer A } }
|
||||
? A extends Record<string, any>
|
||||
? { [K in keyof A]: InferType<A[K]> }
|
||||
: never
|
||||
: never;
|
||||
|
||||
// 类型映射:将 API 配置转换为方法签名
|
||||
type ApiMethods<P extends { [path: string]: { [key: string]: Pos } }> = {
|
||||
[Path in keyof P]: {
|
||||
[Key in keyof P[Path]]: (
|
||||
data?: Partial<ExtractArgsFromMetadata<P[Path][Key]>>,
|
||||
opts?: DataOpts
|
||||
) => ReturnType<Query['post']>
|
||||
}
|
||||
}
|
||||
type QueryApiOpts<P extends { [path: string]: { [key: string]: Pos } } = {}> = {
|
||||
query?: Query,
|
||||
api?: P
|
||||
}
|
||||
export class QueryApi<P extends { [path: string]: { [key: string]: Pos } } = {}> {
|
||||
query: Query;
|
||||
|
||||
constructor(opts?: QueryApiOpts<P>) {
|
||||
this.query = opts?.query ?? new Query();
|
||||
if (opts?.api) {
|
||||
this.createApi(opts.api);
|
||||
}
|
||||
}
|
||||
|
||||
// 使用泛型来推断类型
|
||||
post<T extends Pos>(
|
||||
pos: T,
|
||||
data?: Partial<ExtractArgsFromMetadata<T>>,
|
||||
opts?: DataOpts
|
||||
) {
|
||||
const _pos = pick(pos, ['path', 'key', 'id']);
|
||||
return this.query.post({
|
||||
..._pos,
|
||||
payload: data
|
||||
}, opts)
|
||||
}
|
||||
|
||||
createApi(api: P): asserts this is this & ApiMethods<P> {
|
||||
const that = this as any;
|
||||
const apiEntries = Object.entries(api);
|
||||
const keepPaths = ['createApi', 'query', 'post'];
|
||||
|
||||
for (const [path, methods] of apiEntries) {
|
||||
if (keepPaths.includes(path)) continue;
|
||||
|
||||
// 为每个 path 创建命名空间对象
|
||||
if (!that[path]) {
|
||||
that[path] = {};
|
||||
}
|
||||
|
||||
for (const [key, pos] of Object.entries(methods)) {
|
||||
that[path][key] = (data?: Partial<ExtractArgsFromMetadata<typeof pos>>, opts?: DataOpts) => {
|
||||
const _pos = pick(pos, ['path', 'key', 'id']);
|
||||
return that.query.post({
|
||||
..._pos,
|
||||
payload: data
|
||||
}, opts);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建工厂函数,提供更好的类型推断
|
||||
export function createQueryApi<P extends { [path: string]: { [key: string]: Pos } }>(
|
||||
opts?: QueryApiOpts<P>
|
||||
): QueryApi<P> & ApiMethods<P> {
|
||||
return new QueryApi(opts) as QueryApi<P> & ApiMethods<P>;
|
||||
}
|
||||
|
||||
export { createQueryByRoutes };
|
||||
// const demo = {
|
||||
// "test_path": {
|
||||
// "test_key": {
|
||||
// "path": "demo",
|
||||
// "key": "test",
|
||||
// metadata: {
|
||||
// args: {
|
||||
// name: z.string(),
|
||||
// age: z.number(),
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// } as const;
|
||||
|
||||
// // 方式1: 使用工厂函数创建(推荐)
|
||||
// const queryApi = createQueryApi({ query: new Query(), api: demo });
|
||||
|
||||
// // 现在调用时会有完整的类型推断
|
||||
// // data 参数会被推断为 { name?: string, age?: number }
|
||||
// queryApi.test_path.test_key({ name: "test", age: 18 });
|
||||
// // 也可以不传参数
|
||||
// queryApi.test_path.test_key();
|
||||
|
||||
// // 或者只传递 opts
|
||||
// queryApi.test_path.test_key(undefined, { timeout: 5000 });
|
||||
Reference in New Issue
Block a user