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:
2026-02-17 21:39:41 +08:00
parent 7adedc0552
commit ecb69ba326
18 changed files with 1106 additions and 591 deletions

136
src/query-api.ts Normal file
View 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 });