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

30
test/api.ts Normal file
View File

@@ -0,0 +1,30 @@
import { QueryApi } from '../src/query-api.ts';
export const queryApi = new QueryApi();
export const api = {
"test": {
"test": {
"path": "test",
"key": "test",
"id": "rWfTW4jLlwPWN_LdYXPBO",
"description": "test route",
"type": "route",
"middleware": [],
"metadata": {
"args": {
"a": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "string",
"description": "arg a"
}
}
}
}
},
// Additional routes can be added here
} as const;
const res = await queryApi.post(api.test.test, {
a: 'test'
});

26
test/common.ts Normal file
View File

@@ -0,0 +1,26 @@
import { app } from './router.ts';
import util from 'node:util';
import fs from 'node:fs'
import { createQueryByRoutes } from '../src/create-query/index.ts';
export const showMore = (data: any) => {
return util.inspect(data, { depth: null, colors: true });
}
const routes = await app.run({ path: 'router', key: 'list' })
// console.log('rourtes', showMore(routes.data.list));
const list = routes.data.list
const obj: any = {}
for (const route of list) {
if (!obj[route.path]) {
obj[route.path] = {};
}
obj[route.path][route.key] = route;
}
// console.log('obj', showMore(obj));
const code = createQueryByRoutes(list);
fs.writeFileSync('test/query.ts', code, 'utf-8');

472
test/query.ts Normal file
View File

@@ -0,0 +1,472 @@
import { createQueryApi } from '@kevisual/query/api';
const api = {
"test": {
/**
* test route
*
* @param data - Request parameters
* @param data.a - {string} arg a
*/
"test": {
"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"
}
}
}
}
},
"demo": {
/**
* First demo route demonstrating string and number parameters
*
* @param data - Request parameters
* @param data.username - {string (minLength: 3, maxLength: 20)} The username to be validated, must be between 3 and 20 characters
* @param data.age - {number (min: 18, max: 100)} The age of the user, must be between 18 and 100
* @param data.email - {string (format: email)} The email address of the user for notification purposes
* @param data.count - {integer (max: 9007199254740991, > 0)} The number of items to process, must be a positive integer
* @param data.name - {string} The display name of the user
*/
"d1": {
"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",
"type": "string",
"minLength": 3,
"maxLength": 20,
"description": "The username to be validated, must be between 3 and 20 characters"
},
"age": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "number",
"minimum": 18,
"maximum": 100,
"description": "The age of the user, must be between 18 and 100"
},
"email": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "string",
"format": "email",
"pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$",
"description": "The email address of the user for notification purposes"
},
"count": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991,
"description": "The number of items to process, must be a positive integer"
},
"name": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "string",
"description": "The display name of the user"
}
}
}
},
/**
* Second demo route for boolean and enum parameters
*
* @param data - Request parameters
* @param data.isActive - {boolean} Whether the user account is currently active and accessible
* @param data.isAdmin - {boolean} Whether the user has administrative privileges
* @param data.notifications - {boolean} Whether to enable email and push notifications
* @param data.mode - {"read" | "write" | "execute"} The operation mode for the current session
* @param data.verified - {boolean} Whether the user email has been verified
*/
"d2": {
"path": "demo",
"key": "d2",
"description": "Second demo route for boolean and enum parameters",
"type": "route",
"middleware": [],
"metadata": {
"args": {
"isActive": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "boolean",
"description": "Whether the user account is currently active and accessible"
},
"isAdmin": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "boolean",
"description": "Whether the user has administrative privileges"
},
"notifications": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "boolean",
"description": "Whether to enable email and push notifications"
},
"mode": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "string",
"enum": [
"read",
"write",
"execute"
],
"description": "The operation mode for the current session"
},
"verified": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "boolean",
"description": "Whether the user email has been verified"
}
}
}
},
/**
* Third demo route handling array and optional parameters
*
* @param data - Request parameters
* @param data.tags - {array} List of tags associated with the content, between 1 and 10 tags
* @param data.categories - {array} List of category names for filtering and classification
* @param data.ids - {array} Array of numeric identifiers for the resources
* @param data.priority - {number (min: 1, max: 5)} Priority level from 1 to 5, defaults to 3 if not specified
* @param data.keywords - {array} Keywords for search optimization, up to 20 keywords
*/
"d3": {
"path": "demo",
"key": "d3",
"description": "Third demo route handling array and optional parameters",
"type": "route",
"middleware": [],
"metadata": {
"args": {
"tags": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"minItems": 1,
"maxItems": 10,
"type": "array",
"items": {
"type": "string"
},
"description": "List of tags associated with the content, between 1 and 10 tags"
},
"categories": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "array",
"items": {
"type": "string"
},
"description": "List of category names for filtering and classification"
},
"ids": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "array",
"items": {
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
},
"description": "Array of numeric identifiers for the resources"
},
"priority": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "Priority level from 1 to 5, defaults to 3 if not specified",
"type": "number",
"minimum": 1,
"maximum": 5
},
"keywords": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"maxItems": 20,
"type": "array",
"items": {
"type": "string"
},
"description": "Keywords for search optimization, up to 20 keywords"
}
}
}
},
/**
* Fourth demo route with nested object parameters
*
* @param data - Request parameters
* @param data.user - {object} Complete user profile information
* @param data.settings - {object} User preference settings
* @param data.address - {object} Mailing address, optional field
*/
"d4": {
"path": "demo",
"key": "d4",
"description": "Fourth demo route with nested object parameters",
"type": "route",
"middleware": [],
"metadata": {
"args": {
"user": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"id": {
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991,
"description": "Unique identifier for the user"
},
"name": {
"type": "string",
"description": "Full name of the user"
},
"contact": {
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email",
"pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$",
"description": "Primary email address"
},
"phone": {
"description": "Phone number with country code",
"type": "string"
}
},
"required": [
"email"
],
"additionalProperties": false,
"description": "Contact information for the user"
}
},
"required": [
"id",
"name",
"contact"
],
"additionalProperties": false,
"description": "Complete user profile information"
},
"settings": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"theme": {
"type": "string",
"enum": [
"light",
"dark",
"auto"
],
"description": "UI theme preference"
},
"language": {
"default": "en",
"description": "Preferred language code",
"type": "string"
},
"timezone": {
"type": "string",
"description": "Timezone identifier like America/New_York"
}
},
"required": [
"theme",
"language",
"timezone"
],
"additionalProperties": false,
"description": "User preference settings"
},
"address": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "Mailing address, optional field",
"type": "object",
"properties": {
"street": {
"type": "string",
"description": "Street address line"
},
"city": {
"type": "string",
"description": "City name"
},
"country": {
"type": "string",
"description": "Country code or name"
}
},
"required": [
"street",
"city",
"country"
],
"additionalProperties": false
}
}
}
},
/**
* Fifth demo route with mixed complex parameters and validation
*
* @param data - Request parameters
* @param data.query - {string (minLength: 1)} Search query string, minimum 1 character required
* @param data.filters - {object} Advanced search filters configuration
* @param data.pagination - {object} Pagination settings for query results
* @param data.includeMetadata - {boolean} Whether to include metadata in response
* @param data.timeout - {number (min: 1000, max: 30000)} Request timeout in milliseconds, between 1s and 30s
* @param data.retry - {integer (min: 0, max: 5)} Number of retry attempts on failure
*/
"d5": {
"path": "demo",
"key": "d5",
"description": "Fifth demo route with mixed complex parameters and validation",
"type": "route",
"middleware": [],
"metadata": {
"args": {
"query": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "string",
"minLength": 1,
"description": "Search query string, minimum 1 character required"
},
"filters": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"all",
"image",
"video",
"audio",
"document"
],
"description": "Content type filter"
},
"dateRange": {
"description": "Date range filter, optional",
"type": "object",
"properties": {
"start": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"description": "Start date in ISO 8601 format"
},
"end": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"description": "End date in ISO 8601 format"
}
},
"required": [
"start",
"end"
],
"additionalProperties": false
},
"size": {
"description": "Size filter for media content",
"type": "string",
"enum": [
"small",
"medium",
"large",
"extra-large"
]
}
},
"required": [
"type"
],
"additionalProperties": false,
"description": "Advanced search filters configuration"
},
"pagination": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"page": {
"default": 1,
"description": "Page number starting from 1",
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
},
"limit": {
"default": 20,
"description": "Number of items per page, max 100",
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 100
},
"sort": {
"default": "desc",
"description": "Sort order for results",
"type": "string",
"enum": [
"asc",
"desc"
]
}
},
"required": [
"page",
"limit",
"sort"
],
"additionalProperties": false,
"description": "Pagination settings for query results"
},
"includeMetadata": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"default": false,
"description": "Whether to include metadata in response",
"type": "boolean"
},
"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
},
"retry": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"default": 3,
"description": "Number of retry attempts on failure",
"type": "integer",
"minimum": 0,
"maximum": 5
}
}
}
}
},
"router": {
/**
* 列出当前应用下的所有的路由信息
*/
"list": {
"path": "router",
"key": "list",
"description": "列出当前应用下的所有的路由信息",
"type": "route",
"middleware": []
}
}
} as const;
const queryApi = createQueryApi({ api });
export { queryApi };

131
test/router.ts Normal file
View File

@@ -0,0 +1,131 @@
import { App } from '@kevisual/router';
import { z } from 'zod';
export const app = new App({});
app
.route({
path: 'test',
key: 'test',
description: 'test route',
metadata: {
args: {
a: z.string().optional().describe('arg a'),
}
}
})
.define(async (ctx) => {
ctx.body = 'test';
})
.addTo(app);
app.route({
path: 'demo',
key: 'd1',
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'),
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'),
name: z.string().describe('The display name of the user'),
}
}
}).define(async (ctx) => {
ctx.body = 'demo1';
}).addTo(app);
app.route({
path: 'demo',
key: 'd2',
description: 'Second demo route for boolean and enum parameters',
metadata: {
args: {
isActive: z.boolean().describe('Whether the user account is currently active and accessible'),
isAdmin: z.boolean().describe('Whether the user has administrative privileges'),
notifications: z.boolean().describe('Whether to enable email and push notifications'),
mode: z.enum(['read', 'write', 'execute']).describe('The operation mode for the current session'),
verified: z.boolean().describe('Whether the user email has been verified'),
}
}
}).define(async (ctx) => {
ctx.body = 'demo2';
}).addTo(app);
app.route({
path: 'demo',
key: 'd3',
description: 'Third demo route handling array and optional parameters',
metadata: {
args: {
tags: z.array(z.string()).min(1).max(10).describe('List of tags associated with the content, between 1 and 10 tags'),
categories: z.array(z.string()).describe('List of category names for filtering and classification'),
ids: z.array(z.number().int().positive()).describe('Array of numeric identifiers for the resources'),
priority: z.number().min(1).max(5).optional().describe('Priority level from 1 to 5, defaults to 3 if not specified'),
keywords: z.array(z.string()).max(20).describe('Keywords for search optimization, up to 20 keywords'),
}
}
}).define(async (ctx) => {
ctx.body = 'demo3';
}).addTo(app);
app.route({
path: 'demo',
key: 'd4',
description: 'Fourth demo route with nested object parameters',
metadata: {
args: {
user: z.object({
id: z.number().int().positive().describe('Unique identifier for the user'),
name: z.string().describe('Full name of the user'),
contact: z.object({
email: z.email().describe('Primary email address'),
phone: z.string().optional().describe('Phone number with country code'),
}).describe('Contact information for the user'),
}).describe('Complete user profile information'),
settings: z.object({
theme: z.enum(['light', 'dark', 'auto']).describe('UI theme preference'),
language: z.string().default('en').describe('Preferred language code'),
timezone: z.string().describe('Timezone identifier like America/New_York'),
}).describe('User preference settings'),
address: z.object({
street: z.string().describe('Street address line'),
city: z.string().describe('City name'),
country: z.string().describe('Country code or name'),
}).optional().describe('Mailing address, optional field'),
}
}
}).define(async (ctx) => {
ctx.body = 'demo4';
}).addTo(app);
app.route({
path: 'demo',
key: 'd5',
description: 'Fifth demo route with mixed complex parameters and validation',
metadata: {
args: {
query: z.string().min(1).describe('Search query string, minimum 1 character required'),
filters: z.object({
type: z.enum(['all', 'image', 'video', 'audio', 'document']).describe('Content type filter'),
dateRange: z.object({
start: z.iso.datetime().describe('Start date in ISO 8601 format'),
end: z.iso.datetime().describe('End date in ISO 8601 format'),
}).optional().describe('Date range filter, optional'),
size: z.enum(['small', 'medium', 'large', 'extra-large']).optional().describe('Size filter for media content'),
}).describe('Advanced search filters configuration'),
pagination: z.object({
page: z.number().int().positive().default(1).describe('Page number starting from 1'),
limit: z.number().int().positive().max(100).default(20).describe('Number of items per page, max 100'),
sort: z.enum(['asc', 'desc']).default('desc').describe('Sort order for results'),
}).describe('Pagination settings for query results'),
includeMetadata: z.boolean().default(false).describe('Whether to include metadata in response'),
timeout: z.number().min(1000).max(30000).optional().describe('Request timeout in milliseconds, between 1s and 30s'),
retry: z.number().int().min(0).max(5).default(3).describe('Number of retry attempts on failure'),
}
}
}).define(async (ctx) => {
ctx.body = 'demo5';
}).addTo(app);
app.createRouteList()

43
test/schema.ts Normal file
View File

@@ -0,0 +1,43 @@
import z, { toJSONSchema } from "zod";
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:");
console.log(
JSON.stringify(toJSONSchema(schema), null, 2)
);
const jsonSchema = toJSONSchema(schema);
const schema2 = z.fromJSONSchema(jsonSchema);
// schema2 的类型是 ZodSchema<any>,所以无法在编译时推断出具体类型
// 这是 fromJSONSchema 的限制 - JSON Schema 转换会丢失 TypeScript 类型信息
schema2.parse({
name: "John Doe",
age: 30, // 添加必需的 age 字段
email: "",
})
type Schema2Type = z.infer<typeof schema2>;
// Schema2Type 被推断为 any
// 对比:原始 schema 的类型推断是正常的
type OriginalSchemaType = z.infer<typeof schema>;
// OriginalSchemaType = { name: string; age: number; email?: string | undefined }
const v: Schema2Type = {
name: "John Doe",
email: ""
}
// 如果使用原始 schema,类型推断会正常工作:
const v2: OriginalSchemaType = {
name: "John Doe",
age: 30,
// email 是可选的
}