chore: bump @kevisual/router to version 0.0.80, update QueryApi to handle optional fields in metadata args, and enhance JSON Schema conversion
This commit is contained in:
4
bun.lock
4
bun.lock
@@ -6,7 +6,7 @@
|
||||
"name": "@kevisual/query",
|
||||
"devDependencies": {
|
||||
"@kevisual/code-builder": "^0.0.6",
|
||||
"@kevisual/router": "^0.0.75",
|
||||
"@kevisual/router": "^0.0.80",
|
||||
"@types/node": "^25.2.3",
|
||||
"es-toolkit": "^1.44.0",
|
||||
"typescript": "^5.9.3",
|
||||
@@ -18,7 +18,7 @@
|
||||
"packages": {
|
||||
"@kevisual/code-builder": ["@kevisual/code-builder@0.0.6", "", { "bin": { "code-builder": "bin/code.js", "builder": "bin/code.js" } }, "sha512-0aqATB31/yw4k4s5/xKnfr4DKbUnx8e3Z3BmKbiXTrc+CqWiWTdlGe9bKI9dZ2Df+xNp6g11W4xM2NICNyyCCw=="],
|
||||
|
||||
"@kevisual/router": ["@kevisual/router@0.0.75", "", { "dependencies": { "es-toolkit": "^1.44.0" } }, "sha512-WBDRKMjNYTP7ymkUUtiQwWYIcqnc+TGo3rFuRze8ovYV2UN5cQxIkIfsDbgWOdV1/v9b57gtiJvJRqWjCBWKRg=="],
|
||||
"@kevisual/router": ["@kevisual/router@0.0.80", "", { "dependencies": { "es-toolkit": "^1.44.0" } }, "sha512-rVwi6Yf411bnNm2x94lMm+s4Csw0Yb7u/aj+VJJ59iouAYhjLuL7Rs1EcARhnQf47cegBJi6zozfGHgLsLHN2w=="],
|
||||
|
||||
"@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="],
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kevisual/query",
|
||||
"version": "0.0.47",
|
||||
"version": "0.0.48",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "npm run clean && bun run bun.config.ts",
|
||||
@@ -19,7 +19,7 @@
|
||||
"description": "",
|
||||
"devDependencies": {
|
||||
"@kevisual/code-builder": "^0.0.6",
|
||||
"@kevisual/router": "^0.0.75",
|
||||
"@kevisual/router": "^0.0.80",
|
||||
"@types/node": "^25.2.3",
|
||||
"typescript": "^5.9.3",
|
||||
"es-toolkit": "^1.44.0",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
|
||||
import { toJSONSchema, fromJSONSchema } from '@kevisual/router/browser'
|
||||
type RouteInfo = {
|
||||
path: string;
|
||||
key: string;
|
||||
@@ -44,6 +45,14 @@ export const createQueryByRoutes = (list: RouteInfo[]) => {
|
||||
if (!obj[route.path]) {
|
||||
obj[route.path] = {};
|
||||
}
|
||||
if (route.metadata?.args) {
|
||||
const args = route.metadata.args;
|
||||
if (args?.$schema) {
|
||||
// 将 args 转换为 JSON Schema
|
||||
const jsonSchema = fromJSONSchema(args);
|
||||
route.metadata.args = toJSONSchema(jsonSchema);
|
||||
}
|
||||
}
|
||||
obj[route.path][route.key] = route;
|
||||
}
|
||||
const code = `
|
||||
|
||||
@@ -64,10 +64,19 @@ type InferType<T> =
|
||||
T extends { properties: infer P } ? InferFromJSONSchema<T> : // 处理没有 type 但有 properties 的对象
|
||||
T;
|
||||
|
||||
// 检查是否标记为可选
|
||||
type IsOptional<T> = T extends { optional: true } ? true : false;
|
||||
|
||||
// 提取 args 对象,将每个 Zod schema 或 JSON Schema 转换为实际类型
|
||||
// 根据 optional 字段分离必需字段和可选字段
|
||||
type ExtractArgsFromMetadata<T> = T extends { metadata?: { args?: infer A } }
|
||||
? A extends Record<string, any>
|
||||
? { [K in keyof A]: InferType<A[K]> }
|
||||
? (
|
||||
// 必需字段(没有 optional: true)
|
||||
{ [K in keyof A as IsOptional<A[K]> extends true ? never : K]: InferType<A[K]> } &
|
||||
// 可选字段(有 optional: true)
|
||||
{ [K in keyof A as IsOptional<A[K]> extends true ? K : never]?: InferType<A[K]> }
|
||||
)
|
||||
: never
|
||||
: never;
|
||||
|
||||
@@ -75,7 +84,7 @@ type ExtractArgsFromMetadata<T> = T extends { metadata?: { args?: infer A } }
|
||||
type ApiMethods<P extends { [path: string]: { [key: string]: Pos } }> = {
|
||||
[Path in keyof P]: {
|
||||
[Key in keyof P[Path]]: (
|
||||
data?: Partial<ExtractArgsFromMetadata<P[Path][Key]>>,
|
||||
data?: ExtractArgsFromMetadata<P[Path][Key]>,
|
||||
opts?: DataOpts
|
||||
) => ReturnType<Query['post']>
|
||||
}
|
||||
@@ -97,7 +106,7 @@ export class QueryApi<P extends { [path: string]: { [key: string]: Pos } } = {}>
|
||||
// 使用泛型来推断类型
|
||||
post<T extends Pos>(
|
||||
pos: T,
|
||||
data?: Partial<ExtractArgsFromMetadata<T>>,
|
||||
data?: ExtractArgsFromMetadata<T>,
|
||||
opts?: DataOpts
|
||||
) {
|
||||
const _pos = pick(pos, ['path', 'key', 'id']);
|
||||
@@ -121,7 +130,7 @@ export class QueryApi<P extends { [path: string]: { [key: string]: Pos } } = {}>
|
||||
}
|
||||
|
||||
for (const [key, pos] of Object.entries(methods)) {
|
||||
that[path][key] = (data?: Partial<ExtractArgsFromMetadata<typeof pos>>, opts: DataOpts = {}) => {
|
||||
that[path][key] = (data?: 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;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { optional } from 'zod';
|
||||
import { createQueryApi } from '../src/query-api.ts';
|
||||
|
||||
// ============ 示例 1: 基础 JSON Schema 推断 ============
|
||||
@@ -9,7 +10,8 @@ const api1 = {
|
||||
"metadata": {
|
||||
"args": {
|
||||
"userId": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
"includeProfile": {
|
||||
"type": "boolean"
|
||||
@@ -53,7 +55,8 @@ const api2 = {
|
||||
"domain": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"optional": true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,7 +66,7 @@ const api2 = {
|
||||
|
||||
const queryApi2 = createQueryApi({ api: api2 });
|
||||
|
||||
// ✅ 类型推断正常工作 - data 参数被推断为 { id: string, domain: string }
|
||||
// ✅ 类型推断正常工作 - data 参数被推断为 { id?: string, domain?: string }(没有required字段,所有字段可选)
|
||||
queryApi2.app_domain_manager.get({
|
||||
data: {
|
||||
id: '123',
|
||||
@@ -208,7 +211,8 @@ const api5 = {
|
||||
"type": "string",
|
||||
"enum": ["active", "inactive"] as const
|
||||
}
|
||||
}
|
||||
},
|
||||
// optional: true, // 单个 data 参数是可选的
|
||||
// 没有 required 字段,所以所有字段都是可选的
|
||||
}
|
||||
}
|
||||
|
||||
13
test/query-test.ts
Normal file
13
test/query-test.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { queryApi } from "./query.ts";
|
||||
|
||||
queryApi.test.test({
|
||||
a: 'test'
|
||||
})
|
||||
|
||||
queryApi.demo.d1({
|
||||
name: 'Alice',
|
||||
age: 30,
|
||||
email: '',
|
||||
count: 5,
|
||||
// username: ''
|
||||
})
|
||||
@@ -12,14 +12,14 @@ const api = {
|
||||
"path": "test",
|
||||
"key": "test",
|
||||
"description": "test route",
|
||||
"type": "route",
|
||||
"middleware": [],
|
||||
"metadata": {
|
||||
"args": {
|
||||
"a": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "arg a",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,16 +40,16 @@ const api = {
|
||||
"path": "demo",
|
||||
"key": "d1",
|
||||
"description": "First demo route demonstrating string and number parameters",
|
||||
"type": "route",
|
||||
"middleware": [],
|
||||
"metadata": {
|
||||
"args": {
|
||||
"username": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "The username to be validated, must be between 3 and 20 characters",
|
||||
"type": "string",
|
||||
"minLength": 3,
|
||||
"maxLength": 20,
|
||||
"description": "The username to be validated, must be between 3 and 20 characters"
|
||||
"optional": true
|
||||
},
|
||||
"age": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
@@ -94,7 +94,6 @@ const api = {
|
||||
"path": "demo",
|
||||
"key": "d2",
|
||||
"description": "Second demo route for boolean and enum parameters",
|
||||
"type": "route",
|
||||
"middleware": [],
|
||||
"metadata": {
|
||||
"args": {
|
||||
@@ -145,7 +144,6 @@ const api = {
|
||||
"path": "demo",
|
||||
"key": "d3",
|
||||
"description": "Third demo route handling array and optional parameters",
|
||||
"type": "route",
|
||||
"middleware": [],
|
||||
"metadata": {
|
||||
"args": {
|
||||
@@ -182,7 +180,8 @@ const api = {
|
||||
"description": "Priority level from 1 to 5, defaults to 3 if not specified",
|
||||
"type": "number",
|
||||
"minimum": 1,
|
||||
"maximum": 5
|
||||
"maximum": 5,
|
||||
"optional": true
|
||||
},
|
||||
"keywords": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
@@ -208,7 +207,6 @@ const api = {
|
||||
"path": "demo",
|
||||
"key": "d4",
|
||||
"description": "Fourth demo route with nested object parameters",
|
||||
"type": "route",
|
||||
"middleware": [],
|
||||
"metadata": {
|
||||
"args": {
|
||||
@@ -309,7 +307,8 @@ const api = {
|
||||
"city",
|
||||
"country"
|
||||
],
|
||||
"additionalProperties": false
|
||||
"additionalProperties": false,
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -329,7 +328,6 @@ const api = {
|
||||
"path": "demo",
|
||||
"key": "d5",
|
||||
"description": "Fifth demo route with mixed complex parameters and validation",
|
||||
"type": "route",
|
||||
"middleware": [],
|
||||
"metadata": {
|
||||
"args": {
|
||||
@@ -434,14 +432,16 @@ const api = {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"default": false,
|
||||
"description": "Whether to include metadata in response",
|
||||
"type": "boolean"
|
||||
"type": "boolean",
|
||||
"optional": true
|
||||
},
|
||||
"timeout": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "Request timeout in milliseconds, between 1s and 30s",
|
||||
"type": "number",
|
||||
"minimum": 1000,
|
||||
"maximum": 30000
|
||||
"maximum": 30000,
|
||||
"optional": true
|
||||
},
|
||||
"retry": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
@@ -449,7 +449,8 @@ const api = {
|
||||
"description": "Number of retry attempts on failure",
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 5
|
||||
"maximum": 5,
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -463,7 +464,6 @@ const api = {
|
||||
"path": "router",
|
||||
"key": "list",
|
||||
"description": "列出当前应用下的所有的路由信息",
|
||||
"type": "route",
|
||||
"middleware": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ app.route({
|
||||
description: 'First demo route demonstrating string and number parameters',
|
||||
metadata: {
|
||||
args: {
|
||||
username: z.string().min(3).max(20).describe('The username to be validated, must be between 3 and 20 characters'),
|
||||
username: z.string().min(3).max(20).optional().describe('The username to be validated, must be between 3 and 20 characters'),
|
||||
age: z.number().min(18).max(100).describe('The age of the user, must be between 18 and 100'),
|
||||
email: z.email().describe('The email address of the user for notification purposes'),
|
||||
count: z.number().int().positive().describe('The number of items to process, must be a positive integer'),
|
||||
|
||||
@@ -1,21 +1,32 @@
|
||||
import z, { toJSONSchema } from "zod";
|
||||
|
||||
import z from "zod";
|
||||
import { toJSONSchema, fromJSONSchema } from './test-schema.ts'
|
||||
const schema = z.object({
|
||||
name: z.string().describe("The name of the person"),
|
||||
age: z.number().int().min(0).describe("The age of the person"),
|
||||
email: z.string().optional().describe("The email address of the person"),
|
||||
});
|
||||
|
||||
console.log("JSON Schema for the person object:");
|
||||
})
|
||||
const nameSchema = { name: z.string().optional().describe("The name of the person") }
|
||||
// console.log("JSON Schema for the person object:");
|
||||
// console.log(
|
||||
// JSON.stringify(toJSONSchema(nameSchema), null, 2)
|
||||
// );
|
||||
console.log("\n--- 自定义 override ---");
|
||||
const jsonSchema = toJSONSchema(nameSchema)
|
||||
console.log(
|
||||
JSON.stringify(toJSONSchema(schema), null, 2)
|
||||
JSON.stringify(jsonSchema, null, 2)
|
||||
);
|
||||
|
||||
console.log('shape', schema.shape);
|
||||
console.log('shape name', schema.shape.name.toJSONSchema());
|
||||
// console.log('shape', schema.shape);
|
||||
// console.log('shape name', schema.shape.name.toJSONSchema());
|
||||
// const jsonSchema = toJSONSchema(schema);
|
||||
|
||||
// const schema2 = z.fromJSONSchema(jsonSchema);
|
||||
let schema2 = fromJSONSchema<false>(jsonSchema);
|
||||
|
||||
console.log("\n--- 从 JSON Schema 反向转换回 Zod schema ---");
|
||||
console.log('schema2 nameSchema', schema2.name.safeParse("John Doe"));
|
||||
// console.log('schema2', schema2.safeParse({ name: "John Doe", }));
|
||||
// console.log('schema2 email', schema2.email.safeParse(undefined));
|
||||
// console.log('schema2 age', schema2.age.safeParse(1));
|
||||
|
||||
// // schema2 的类型是 ZodSchema<any>,所以无法在编译时推断出具体类型
|
||||
// // 这是 fromJSONSchema 的限制 - JSON Schema 转换会丢失 TypeScript 类型信息
|
||||
|
||||
100
test/test-schema.ts
Normal file
100
test/test-schema.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { z } from "zod";
|
||||
const extractArgs = (args: any) => {
|
||||
if (args && typeof args === 'object' && typeof args.shape === 'object') {
|
||||
return args.shape as z.ZodRawShape;
|
||||
}
|
||||
return args || {};
|
||||
};
|
||||
|
||||
type ZodOverride = (opts: { jsonSchema: any; path: string[]; zodSchema: z.ZodTypeAny }) => void;
|
||||
/**
|
||||
* 剥离第一层schema,转换为JSON Schema,无论是skill还是其他的infer比纯粹的zod object schema更合适,因为它可能包含其他的字段,而不仅仅是schema
|
||||
* @param args
|
||||
* @returns
|
||||
*/
|
||||
export const toJSONSchema = (args: any, opts?: { mergeObject?: boolean, override?: ZodOverride }): { [key: string]: any } => {
|
||||
const mergeObject = opts?.mergeObject ?? false;
|
||||
if (!args) return {};
|
||||
const _override = ({ jsonSchema, path, zodSchema }) => {
|
||||
if (Array.isArray(path) && path.length > 0) {
|
||||
return
|
||||
}
|
||||
const isOptional = (zodSchema as any).isOptional?.();
|
||||
if (isOptional) {
|
||||
// 添加自定义属性
|
||||
jsonSchema.optional = true;
|
||||
}
|
||||
}
|
||||
const isError = (keys: string[]) => {
|
||||
const errorKeys: string[] = ["toJSONSchema", "def", "type", "parse"]
|
||||
const hasErrorKeys = errorKeys.every(key => keys.includes(key));
|
||||
return hasErrorKeys;
|
||||
}
|
||||
const override: any = opts?.override || _override;
|
||||
if (mergeObject) {
|
||||
if (typeof args === 'object' && typeof args.toJSONSchema === 'function') {
|
||||
return args.toJSONSchema();
|
||||
}
|
||||
if (isError(Object.keys(args))) {
|
||||
return {};
|
||||
}
|
||||
// 如果 mergeObject 为 true,直接将整个对象转换为 JSON Schema
|
||||
// 先检测是否是一个错误的 schema
|
||||
const schema = z.object(args);
|
||||
return schema.toJSONSchema();
|
||||
}
|
||||
// 如果 args 本身是一个 zod object schema,先提取 shape
|
||||
args = extractArgs(args);
|
||||
let keys = Object.keys(args);
|
||||
if (isError(keys)) {
|
||||
console.error(`[toJSONSchema error]: 解析到的 schema 可能不正确,包含了zod默认的value的schema. 请检查输入的 schema 是否正确。`);
|
||||
args = {};
|
||||
keys = [];
|
||||
}
|
||||
if (mergeObject) {
|
||||
|
||||
}
|
||||
let newArgs: { [key: string]: any } = {};
|
||||
for (let key of keys) {
|
||||
const item = args[key] as z.ZodAny;
|
||||
if (item && typeof item === 'object' && typeof item.toJSONSchema === 'function') {
|
||||
newArgs[key] = item.toJSONSchema({ override });
|
||||
} else {
|
||||
newArgs[key] = args[key]; // 可能不是schema
|
||||
}
|
||||
}
|
||||
return newArgs;
|
||||
}
|
||||
export const fromJSONSchema = <Merge extends boolean = false>(args: any = {}, opts?: { mergeObject?: boolean }) => {
|
||||
let resultArgs: any = null;
|
||||
const mergeObject = opts?.mergeObject ?? false;
|
||||
if (args["$schema"] || (args.type === 'object' && args.properties && typeof args.properties === 'object')) {
|
||||
// 可能是整个schema
|
||||
const objectSchema = z.fromJSONSchema(args);
|
||||
const extract = extractArgs(objectSchema);
|
||||
const keys = Object.keys(extract);
|
||||
const newArgs: { [key: string]: any } = {};
|
||||
for (let key of keys) {
|
||||
newArgs[key] = extract[key];
|
||||
}
|
||||
resultArgs = newArgs;
|
||||
}
|
||||
if (!resultArgs) {
|
||||
const keys = Object.keys(args);
|
||||
const newArgs: { [key: string]: any } = {};
|
||||
for (let key of keys) {
|
||||
const item = args[key];
|
||||
// fromJSONSchema 可能会失败,所以先 optional,等使用的时候再验证
|
||||
newArgs[key] = z.fromJSONSchema(item)
|
||||
if (item.optional) {
|
||||
newArgs[key] = newArgs[key].optional();
|
||||
}
|
||||
}
|
||||
resultArgs = newArgs;
|
||||
}
|
||||
if (mergeObject) {
|
||||
resultArgs = z.object(resultArgs);
|
||||
}
|
||||
type ResultArgs = Merge extends true ? z.ZodObject<{ [key: string]: any }> : { [key: string]: z.ZodTypeAny };
|
||||
return resultArgs as unknown as ResultArgs;
|
||||
}
|
||||
Reference in New Issue
Block a user