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",
|
"name": "@kevisual/query",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kevisual/code-builder": "^0.0.6",
|
"@kevisual/code-builder": "^0.0.6",
|
||||||
"@kevisual/router": "^0.0.75",
|
"@kevisual/router": "^0.0.80",
|
||||||
"@types/node": "^25.2.3",
|
"@types/node": "^25.2.3",
|
||||||
"es-toolkit": "^1.44.0",
|
"es-toolkit": "^1.44.0",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
"packages": {
|
"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/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=="],
|
"@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",
|
"name": "@kevisual/query",
|
||||||
"version": "0.0.47",
|
"version": "0.0.48",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm run clean && bun run bun.config.ts",
|
"build": "npm run clean && bun run bun.config.ts",
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kevisual/code-builder": "^0.0.6",
|
"@kevisual/code-builder": "^0.0.6",
|
||||||
"@kevisual/router": "^0.0.75",
|
"@kevisual/router": "^0.0.80",
|
||||||
"@types/node": "^25.2.3",
|
"@types/node": "^25.2.3",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"es-toolkit": "^1.44.0",
|
"es-toolkit": "^1.44.0",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
|
|
||||||
|
import { toJSONSchema, fromJSONSchema } from '@kevisual/router/browser'
|
||||||
type RouteInfo = {
|
type RouteInfo = {
|
||||||
path: string;
|
path: string;
|
||||||
key: string;
|
key: string;
|
||||||
@@ -44,6 +45,14 @@ export const createQueryByRoutes = (list: RouteInfo[]) => {
|
|||||||
if (!obj[route.path]) {
|
if (!obj[route.path]) {
|
||||||
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;
|
obj[route.path][route.key] = route;
|
||||||
}
|
}
|
||||||
const code = `
|
const code = `
|
||||||
|
|||||||
@@ -64,10 +64,19 @@ type InferType<T> =
|
|||||||
T extends { properties: infer P } ? InferFromJSONSchema<T> : // 处理没有 type 但有 properties 的对象
|
T extends { properties: infer P } ? InferFromJSONSchema<T> : // 处理没有 type 但有 properties 的对象
|
||||||
T;
|
T;
|
||||||
|
|
||||||
|
// 检查是否标记为可选
|
||||||
|
type IsOptional<T> = T extends { optional: true } ? true : false;
|
||||||
|
|
||||||
// 提取 args 对象,将每个 Zod schema 或 JSON Schema 转换为实际类型
|
// 提取 args 对象,将每个 Zod schema 或 JSON Schema 转换为实际类型
|
||||||
|
// 根据 optional 字段分离必需字段和可选字段
|
||||||
type ExtractArgsFromMetadata<T> = T extends { metadata?: { args?: infer A } }
|
type ExtractArgsFromMetadata<T> = T extends { metadata?: { args?: infer A } }
|
||||||
? A extends Record<string, any>
|
? 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
|
||||||
: never;
|
: never;
|
||||||
|
|
||||||
@@ -75,7 +84,7 @@ type ExtractArgsFromMetadata<T> = T extends { metadata?: { args?: infer A } }
|
|||||||
type ApiMethods<P extends { [path: string]: { [key: string]: Pos } }> = {
|
type ApiMethods<P extends { [path: string]: { [key: string]: Pos } }> = {
|
||||||
[Path in keyof P]: {
|
[Path in keyof P]: {
|
||||||
[Key in keyof P[Path]]: (
|
[Key in keyof P[Path]]: (
|
||||||
data?: Partial<ExtractArgsFromMetadata<P[Path][Key]>>,
|
data?: ExtractArgsFromMetadata<P[Path][Key]>,
|
||||||
opts?: DataOpts
|
opts?: DataOpts
|
||||||
) => ReturnType<Query['post']>
|
) => ReturnType<Query['post']>
|
||||||
}
|
}
|
||||||
@@ -97,7 +106,7 @@ export class QueryApi<P extends { [path: string]: { [key: string]: Pos } } = {}>
|
|||||||
// 使用泛型来推断类型
|
// 使用泛型来推断类型
|
||||||
post<T extends Pos>(
|
post<T extends Pos>(
|
||||||
pos: T,
|
pos: T,
|
||||||
data?: Partial<ExtractArgsFromMetadata<T>>,
|
data?: ExtractArgsFromMetadata<T>,
|
||||||
opts?: DataOpts
|
opts?: DataOpts
|
||||||
) {
|
) {
|
||||||
const _pos = pick(pos, ['path', 'key', 'id']);
|
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)) {
|
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']);
|
const _pos = pick(pos, ['path', 'key', 'id']);
|
||||||
if (pos.metadata?.viewItem?.api?.url && !opts.url) {
|
if (pos.metadata?.viewItem?.api?.url && !opts.url) {
|
||||||
opts.url = pos.metadata.viewItem.api.url;
|
opts.url = pos.metadata.viewItem.api.url;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { optional } from 'zod';
|
||||||
import { createQueryApi } from '../src/query-api.ts';
|
import { createQueryApi } from '../src/query-api.ts';
|
||||||
|
|
||||||
// ============ 示例 1: 基础 JSON Schema 推断 ============
|
// ============ 示例 1: 基础 JSON Schema 推断 ============
|
||||||
@@ -9,7 +10,8 @@ const api1 = {
|
|||||||
"metadata": {
|
"metadata": {
|
||||||
"args": {
|
"args": {
|
||||||
"userId": {
|
"userId": {
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"includeProfile": {
|
"includeProfile": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
@@ -53,7 +55,8 @@ const api2 = {
|
|||||||
"domain": {
|
"domain": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"optional": true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,7 +66,7 @@ const api2 = {
|
|||||||
|
|
||||||
const queryApi2 = createQueryApi({ api: api2 });
|
const queryApi2 = createQueryApi({ api: api2 });
|
||||||
|
|
||||||
// ✅ 类型推断正常工作 - data 参数被推断为 { id: string, domain: string }
|
// ✅ 类型推断正常工作 - data 参数被推断为 { id?: string, domain?: string }(没有required字段,所有字段可选)
|
||||||
queryApi2.app_domain_manager.get({
|
queryApi2.app_domain_manager.get({
|
||||||
data: {
|
data: {
|
||||||
id: '123',
|
id: '123',
|
||||||
@@ -208,7 +211,8 @@ const api5 = {
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["active", "inactive"] as const
|
"enum": ["active", "inactive"] as const
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
// optional: true, // 单个 data 参数是可选的
|
||||||
// 没有 required 字段,所以所有字段都是可选的
|
// 没有 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",
|
"path": "test",
|
||||||
"key": "test",
|
"key": "test",
|
||||||
"description": "test route",
|
"description": "test route",
|
||||||
"type": "route",
|
|
||||||
"middleware": [],
|
"middleware": [],
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"args": {
|
"args": {
|
||||||
"a": {
|
"a": {
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"description": "arg a",
|
"description": "arg a",
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"optional": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,16 +40,16 @@ const api = {
|
|||||||
"path": "demo",
|
"path": "demo",
|
||||||
"key": "d1",
|
"key": "d1",
|
||||||
"description": "First demo route demonstrating string and number parameters",
|
"description": "First demo route demonstrating string and number parameters",
|
||||||
"type": "route",
|
|
||||||
"middleware": [],
|
"middleware": [],
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"args": {
|
"args": {
|
||||||
"username": {
|
"username": {
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"description": "The username to be validated, must be between 3 and 20 characters",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"minLength": 3,
|
"minLength": 3,
|
||||||
"maxLength": 20,
|
"maxLength": 20,
|
||||||
"description": "The username to be validated, must be between 3 and 20 characters"
|
"optional": true
|
||||||
},
|
},
|
||||||
"age": {
|
"age": {
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
@@ -94,7 +94,6 @@ const api = {
|
|||||||
"path": "demo",
|
"path": "demo",
|
||||||
"key": "d2",
|
"key": "d2",
|
||||||
"description": "Second demo route for boolean and enum parameters",
|
"description": "Second demo route for boolean and enum parameters",
|
||||||
"type": "route",
|
|
||||||
"middleware": [],
|
"middleware": [],
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"args": {
|
"args": {
|
||||||
@@ -145,7 +144,6 @@ const api = {
|
|||||||
"path": "demo",
|
"path": "demo",
|
||||||
"key": "d3",
|
"key": "d3",
|
||||||
"description": "Third demo route handling array and optional parameters",
|
"description": "Third demo route handling array and optional parameters",
|
||||||
"type": "route",
|
|
||||||
"middleware": [],
|
"middleware": [],
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"args": {
|
"args": {
|
||||||
@@ -182,7 +180,8 @@ const api = {
|
|||||||
"description": "Priority level from 1 to 5, defaults to 3 if not specified",
|
"description": "Priority level from 1 to 5, defaults to 3 if not specified",
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"minimum": 1,
|
"minimum": 1,
|
||||||
"maximum": 5
|
"maximum": 5,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"keywords": {
|
"keywords": {
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
@@ -208,7 +207,6 @@ const api = {
|
|||||||
"path": "demo",
|
"path": "demo",
|
||||||
"key": "d4",
|
"key": "d4",
|
||||||
"description": "Fourth demo route with nested object parameters",
|
"description": "Fourth demo route with nested object parameters",
|
||||||
"type": "route",
|
|
||||||
"middleware": [],
|
"middleware": [],
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"args": {
|
"args": {
|
||||||
@@ -309,7 +307,8 @@ const api = {
|
|||||||
"city",
|
"city",
|
||||||
"country"
|
"country"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false,
|
||||||
|
"optional": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -329,7 +328,6 @@ const api = {
|
|||||||
"path": "demo",
|
"path": "demo",
|
||||||
"key": "d5",
|
"key": "d5",
|
||||||
"description": "Fifth demo route with mixed complex parameters and validation",
|
"description": "Fifth demo route with mixed complex parameters and validation",
|
||||||
"type": "route",
|
|
||||||
"middleware": [],
|
"middleware": [],
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"args": {
|
"args": {
|
||||||
@@ -434,14 +432,16 @@ const api = {
|
|||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"default": false,
|
"default": false,
|
||||||
"description": "Whether to include metadata in response",
|
"description": "Whether to include metadata in response",
|
||||||
"type": "boolean"
|
"type": "boolean",
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"timeout": {
|
"timeout": {
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"description": "Request timeout in milliseconds, between 1s and 30s",
|
"description": "Request timeout in milliseconds, between 1s and 30s",
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"minimum": 1000,
|
"minimum": 1000,
|
||||||
"maximum": 30000
|
"maximum": 30000,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"retry": {
|
"retry": {
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
@@ -449,7 +449,8 @@ const api = {
|
|||||||
"description": "Number of retry attempts on failure",
|
"description": "Number of retry attempts on failure",
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"minimum": 0,
|
"minimum": 0,
|
||||||
"maximum": 5
|
"maximum": 5,
|
||||||
|
"optional": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -463,7 +464,6 @@ const api = {
|
|||||||
"path": "router",
|
"path": "router",
|
||||||
"key": "list",
|
"key": "list",
|
||||||
"description": "列出当前应用下的所有的路由信息",
|
"description": "列出当前应用下的所有的路由信息",
|
||||||
"type": "route",
|
|
||||||
"middleware": []
|
"middleware": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ app.route({
|
|||||||
description: 'First demo route demonstrating string and number parameters',
|
description: 'First demo route demonstrating string and number parameters',
|
||||||
metadata: {
|
metadata: {
|
||||||
args: {
|
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'),
|
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'),
|
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'),
|
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({
|
const schema = z.object({
|
||||||
name: z.string().describe("The name of the person"),
|
name: z.string().describe("The name of the person"),
|
||||||
age: z.number().int().min(0).describe("The age 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"),
|
email: z.string().optional().describe("The email address of the person"),
|
||||||
});
|
})
|
||||||
|
const nameSchema = { name: z.string().optional().describe("The name of the person") }
|
||||||
console.log("JSON Schema for the person object:");
|
// 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(
|
console.log(
|
||||||
JSON.stringify(toJSONSchema(schema), null, 2)
|
JSON.stringify(jsonSchema, null, 2)
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log('shape', schema.shape);
|
// console.log('shape', schema.shape);
|
||||||
console.log('shape name', schema.shape.name.toJSONSchema());
|
// console.log('shape name', schema.shape.name.toJSONSchema());
|
||||||
// const jsonSchema = toJSONSchema(schema);
|
// 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>,所以无法在编译时推断出具体类型
|
// // schema2 的类型是 ZodSchema<any>,所以无法在编译时推断出具体类型
|
||||||
// // 这是 fromJSONSchema 的限制 - JSON Schema 转换会丢失 TypeScript 类型信息
|
// // 这是 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