172 lines
5.6 KiB
TypeScript
172 lines
5.6 KiB
TypeScript
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>;
|
||
viewItem?: Record<string, any>;
|
||
url?: string;
|
||
source?: string;
|
||
[key: string]: any;
|
||
};
|
||
}
|
||
|
||
// JSON Schema 类型推断 - 使用更精确的类型匹配
|
||
type InferFromJSONSchema<T> =
|
||
// 处理带 enum 的字符串类型
|
||
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: "array"; items: infer I }
|
||
? Array<InferFromJSONSchema<I>>
|
||
: // 对象类型 - 带 required 字段(必需字段 + 可选字段)
|
||
// 注意:必须在 additionalProperties 检查之前,因为两者可能同时存在
|
||
T extends { type: "object"; properties: infer P; required: infer R extends readonly string[] }
|
||
? {
|
||
[K in keyof P as K extends R[number] ? K : never]: InferFromJSONSchema<P[K]>
|
||
} & {
|
||
[K in keyof P as K extends R[number] ? never : K]?: InferFromJSONSchema<P[K]>
|
||
}
|
||
: // 对象类型 - 不带 required 字段(所有字段可选)
|
||
T extends { type: "object"; properties: infer P }
|
||
? {
|
||
[K in keyof P]?: InferFromJSONSchema<P[K]>
|
||
}
|
||
: // 对象类型 - 只有 additionalProperties(纯动态对象)
|
||
T extends { type: "object"; additionalProperties: infer A }
|
||
? (A extends false ? Record<string, never> : Record<string, any>)
|
||
: // 默认情况 - 如果是对象但没有 type 字段,尝试递归推断
|
||
T extends Record<string, any>
|
||
? T extends { properties: infer P; required: infer R extends readonly string[] }
|
||
? {
|
||
[K in keyof P as K extends R[number] ? K : never]: InferFromJSONSchema<P[K]>
|
||
} & {
|
||
[K in keyof P as K extends R[number] ? never : K]?: InferFromJSONSchema<P[K]>
|
||
}
|
||
: T extends { properties: infer P }
|
||
? { [K in keyof P]?: InferFromJSONSchema<P[K]> }
|
||
: unknown
|
||
: 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 extends { properties: infer P } ? InferFromJSONSchema<T> : // 处理没有 type 但有 properties 的对象
|
||
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']);
|
||
if (pos.metadata?.viewItem?.api?.url && !opts.url) {
|
||
opts.url = pos.metadata.viewItem.api.url;
|
||
}
|
||
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 });
|