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

8
bun.config.ts Normal file
View File

@@ -0,0 +1,8 @@
import { buildWithBun } from '@kevisual/code-builder';
await buildWithBun({ naming: 'query-browser', entry: 'src/query-browser.ts', dts: true, target: 'browser' });
await buildWithBun({ naming: 'query', entry: 'src/query.ts', dts: true, target: 'browser' });
await buildWithBun({ naming: 'query-ws', entry: 'src/ws.ts', dts: true, target: 'browser' });
await buildWithBun({ naming: 'query-adapter', entry: 'src/adapter.ts', dts: true, target: 'browser' });
await buildWithBun({ naming: 'query-api', entry: 'src/query-api.ts', dts: true, target: 'browser' });

View File

@@ -1,8 +1,6 @@
import { App } from '@kevisual/router'; import { App } from '@kevisual/router';
const app = new App({ const app = new App({});
io: true,
});
app app
.route({ .route({
@@ -17,8 +15,3 @@ app
app.listen(4000, () => { app.listen(4000, () => {
console.log('Server is running at http://localhost:4000'); console.log('Server is running at http://localhost:4000');
}); });
app.io.addListener('subscribe', async ({ data, end, ws }) => {
console.log('A user connected', data);
ws.send('Hello World');
});

View File

@@ -1,6 +1,6 @@
// console.log('Hello World'); // console.log('Hello World');
import { adapter, Query } from '@abearxiong/query'; import { adapter, Query } from '@kevisual/query';
import { QueryWs } from '@abearxiong/query/ws'; import { QueryWs } from '@kevisual/query/ws';
window.onload = async () => { window.onload = async () => {
// const res = await adapter({ // const res = await adapter({

View File

@@ -0,0 +1,67 @@
import { QueryApi } from "../src/query-api";
const api = new QueryApi();
// 示例 1: args 的 value 是 string 类型
api.post(
{
path: "/users",
metadata: {
args: {
name: "John",
email: "john@example.com"
}
}
},
{
pos: "someValue", // ✅ TypeScript 会推断 pos 应该是 string 类型
other: "data"
}
);
// 示例 2: args 的 value 是 number 类型
api.post(
{
path: "/products",
metadata: {
args: {
id: 123,
price: 99.99
}
}
},
{
pos: 456, // ✅ TypeScript 会推断 pos 应该是 number 类型
other: "data"
}
);
// 示例 3: args 的 value 是混合类型
api.post(
{
path: "/orders",
metadata: {
args: {
orderId: 123,
status: "pending",
total: 99.99
}
}
},
{
pos: "status", // ✅ TypeScript 会推断 pos 可以是 string 或 number
// pos: 999, // 这也是合法的
other: "data"
}
);
// 示例 4: 没有 metadata 或 args
api.post(
{
path: "/test"
},
{
pos: undefined, // ✅ pos 类型为 never只能是 undefined
other: "data"
}
);

View File

@@ -1,12 +1,9 @@
{ {
"name": "@kevisual/query", "name": "@kevisual/query",
"version": "0.0.40", "version": "0.0.41",
"main": "dist/query-browser.js",
"private": false,
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "npm run clean && rollup -c", "build": "npm run clean && bun run bun.config.ts",
"dev:lib": "rollup -c -w",
"clean": "rm -rf dist" "clean": "rm -rf dist"
}, },
"files": [ "files": [
@@ -21,11 +18,12 @@
"license": "ISC", "license": "ISC",
"description": "", "description": "",
"devDependencies": { "devDependencies": {
"@rollup/plugin-node-resolve": "^16.0.3", "@kevisual/code-builder": "^0.0.6",
"@rollup/plugin-typescript": "^12.3.0", "@kevisual/router": "^0.0.72",
"rollup": "^4.57.1", "@types/node": "^25.2.3",
"rollup-plugin-dts": "^6.3.0",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"es-toolkit": "^1.44.0",
"zod": "^4.3.6",
"zustand": "^5.0.11" "zustand": "^5.0.11"
}, },
"publishConfig": { "publishConfig": {
@@ -36,20 +34,10 @@
"url": "git+ssh://git@github.com/abearxiong/kevisual-query.git" "url": "git+ssh://git@github.com/abearxiong/kevisual-query.git"
}, },
"exports": { "exports": {
".": { ".": "./dist/query-browser.js",
"import": "./dist/query-browser.js", "./query": "./dist/query.js",
"require": "./dist/query-browser.js" "./ws": "./dist/query-ws.js",
"./api": "./dist/query-api.js"
}, },
"./query": { "dependencies": {}
"import": "./dist/query.js",
"require": "./dist/query.js"
},
"./ws": {
"import": "./dist/query-ws.js",
"require": "./dist/query-ws.js"
}
},
"dependencies": {
"tslib": "^2.8.1"
}
} }

497
pnpm-lock.yaml generated
View File

@@ -8,288 +8,55 @@ importers:
.: .:
dependencies: dependencies:
tslib: es-toolkit:
specifier: ^2.8.1 specifier: ^1.44.0
version: 2.8.1 version: 1.44.0
devDependencies: devDependencies:
'@rollup/plugin-node-resolve': '@kevisual/code-builder':
specifier: ^16.0.3 specifier: ^0.0.6
version: 16.0.3(rollup@4.57.1) version: 0.0.6
'@rollup/plugin-typescript': '@kevisual/router':
specifier: ^12.3.0 specifier: ^0.0.72
version: 12.3.0(rollup@4.57.1)(tslib@2.8.1)(typescript@5.9.3) version: 0.0.72
rollup: '@types/node':
specifier: ^4.57.1 specifier: ^25.2.3
version: 4.57.1 version: 25.2.3
rollup-plugin-dts:
specifier: ^6.3.0
version: 6.3.0(rollup@4.57.1)(typescript@5.9.3)
typescript: typescript:
specifier: ^5.9.3 specifier: ^5.9.3
version: 5.9.3 version: 5.9.3
zod:
specifier: ^4.3.6
version: 4.3.6
zustand: zustand:
specifier: ^5.0.11 specifier: ^5.0.11
version: 5.0.11 version: 5.0.11
packages: packages:
'@babel/code-frame@7.28.6': '@kevisual/code-builder@0.0.6':
resolution: {integrity: sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==} resolution: {integrity: sha512-0aqATB31/yw4k4s5/xKnfr4DKbUnx8e3Z3BmKbiXTrc+CqWiWTdlGe9bKI9dZ2Df+xNp6g11W4xM2NICNyyCCw==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-identifier@7.28.5':
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
engines: {node: '>=6.9.0'}
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
'@rollup/plugin-node-resolve@16.0.3':
resolution: {integrity: sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: ^2.78.0||^3.0.0||^4.0.0
peerDependenciesMeta:
rollup:
optional: true
'@rollup/plugin-typescript@12.3.0':
resolution: {integrity: sha512-7DP0/p7y3t67+NabT9f8oTBFE6gGkto4SA6Np2oudYmZE/m1dt8RB0SjL1msMxFpLo631qjRCcBlAbq1ml/Big==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: ^2.14.0||^3.0.0||^4.0.0
tslib: '*'
typescript: '>=3.7.0'
peerDependenciesMeta:
rollup:
optional: true
tslib:
optional: true
'@rollup/pluginutils@5.3.0':
resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
peerDependenciesMeta:
rollup:
optional: true
'@rollup/rollup-android-arm-eabi@4.57.1':
resolution: {integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==}
cpu: [arm]
os: [android]
'@rollup/rollup-android-arm64@4.57.1':
resolution: {integrity: sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==}
cpu: [arm64]
os: [android]
'@rollup/rollup-darwin-arm64@4.57.1':
resolution: {integrity: sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==}
cpu: [arm64]
os: [darwin]
'@rollup/rollup-darwin-x64@4.57.1':
resolution: {integrity: sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==}
cpu: [x64]
os: [darwin]
'@rollup/rollup-freebsd-arm64@4.57.1':
resolution: {integrity: sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==}
cpu: [arm64]
os: [freebsd]
'@rollup/rollup-freebsd-x64@4.57.1':
resolution: {integrity: sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==}
cpu: [x64]
os: [freebsd]
'@rollup/rollup-linux-arm-gnueabihf@4.57.1':
resolution: {integrity: sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.57.1':
resolution: {integrity: sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.57.1':
resolution: {integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.57.1':
resolution: {integrity: sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.57.1':
resolution: {integrity: sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-loong64-musl@4.57.1':
resolution: {integrity: sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==}
cpu: [loong64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-ppc64-gnu@4.57.1':
resolution: {integrity: sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-musl@4.57.1':
resolution: {integrity: sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==}
cpu: [ppc64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-riscv64-gnu@4.57.1':
resolution: {integrity: sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.57.1':
resolution: {integrity: sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.57.1':
resolution: {integrity: sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.57.1':
resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.57.1':
resolution: {integrity: sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-openbsd-x64@4.57.1':
resolution: {integrity: sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==}
cpu: [x64]
os: [openbsd]
'@rollup/rollup-openharmony-arm64@4.57.1':
resolution: {integrity: sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==}
cpu: [arm64]
os: [openharmony]
'@rollup/rollup-win32-arm64-msvc@4.57.1':
resolution: {integrity: sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==}
cpu: [arm64]
os: [win32]
'@rollup/rollup-win32-ia32-msvc@4.57.1':
resolution: {integrity: sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==}
cpu: [ia32]
os: [win32]
'@rollup/rollup-win32-x64-gnu@4.57.1':
resolution: {integrity: sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==}
cpu: [x64]
os: [win32]
'@rollup/rollup-win32-x64-msvc@4.57.1':
resolution: {integrity: sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==}
cpu: [x64]
os: [win32]
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@types/resolve@1.20.2':
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
deepmerge@4.3.1:
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
engines: {node: '>=0.10.0'}
estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
is-core-module@2.16.1:
resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
engines: {node: '>= 0.4'}
is-module@1.0.0:
resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==}
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
picomatch@4.0.3:
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
engines: {node: '>=12'}
resolve@1.22.11:
resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==}
engines: {node: '>= 0.4'}
hasBin: true hasBin: true
rollup-plugin-dts@6.3.0: '@kevisual/router@0.0.72':
resolution: {integrity: sha512-d0UrqxYd8KyZ6i3M2Nx7WOMy708qsV/7fTHMHxCMCBOAe3V/U7OMPu5GkX8hC+cmkHhzGnfeYongl1IgiooddA==} resolution: {integrity: sha512-+HL4FINZsjnoRRa8Qs7xoPg+5/TcHR7jZQ7AHWHogo0BJzCAtnQwmidMQzeGL4z0WKNbbgVhXdz1wAYoxHJZTg==}
engines: {node: '>=16'}
peerDependencies:
rollup: ^3.29.4 || ^4
typescript: ^4.5 || ^5.0
rollup@4.57.1: '@types/node@25.2.3':
resolution: {integrity: sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==} resolution: {integrity: sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
supports-preserve-symlinks-flag@1.0.0: es-toolkit@1.44.0:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} resolution: {integrity: sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==}
engines: {node: '>= 0.4'}
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
typescript@5.9.3: typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
hasBin: true hasBin: true
undici-types@7.16.0:
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
zod@4.3.6:
resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
zustand@5.0.11: zustand@5.0.11:
resolution: {integrity: sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==} resolution: {integrity: sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==}
engines: {node: '>=12.20.0'} engines: {node: '>=12.20.0'}
@@ -310,206 +77,22 @@ packages:
snapshots: snapshots:
'@babel/code-frame@7.28.6': '@kevisual/code-builder@0.0.6': {}
'@kevisual/router@0.0.72':
dependencies: dependencies:
'@babel/helper-validator-identifier': 7.28.5 es-toolkit: 1.44.0
js-tokens: 4.0.0
picocolors: 1.1.1
optional: true
'@babel/helper-validator-identifier@7.28.5': '@types/node@25.2.3':
optional: true
'@jridgewell/sourcemap-codec@1.5.5': {}
'@rollup/plugin-node-resolve@16.0.3(rollup@4.57.1)':
dependencies: dependencies:
'@rollup/pluginutils': 5.3.0(rollup@4.57.1) undici-types: 7.16.0
'@types/resolve': 1.20.2
deepmerge: 4.3.1
is-module: 1.0.0
resolve: 1.22.11
optionalDependencies:
rollup: 4.57.1
'@rollup/plugin-typescript@12.3.0(rollup@4.57.1)(tslib@2.8.1)(typescript@5.9.3)': es-toolkit@1.44.0: {}
dependencies:
'@rollup/pluginutils': 5.3.0(rollup@4.57.1)
resolve: 1.22.11
typescript: 5.9.3
optionalDependencies:
rollup: 4.57.1
tslib: 2.8.1
'@rollup/pluginutils@5.3.0(rollup@4.57.1)':
dependencies:
'@types/estree': 1.0.8
estree-walker: 2.0.2
picomatch: 4.0.3
optionalDependencies:
rollup: 4.57.1
'@rollup/rollup-android-arm-eabi@4.57.1':
optional: true
'@rollup/rollup-android-arm64@4.57.1':
optional: true
'@rollup/rollup-darwin-arm64@4.57.1':
optional: true
'@rollup/rollup-darwin-x64@4.57.1':
optional: true
'@rollup/rollup-freebsd-arm64@4.57.1':
optional: true
'@rollup/rollup-freebsd-x64@4.57.1':
optional: true
'@rollup/rollup-linux-arm-gnueabihf@4.57.1':
optional: true
'@rollup/rollup-linux-arm-musleabihf@4.57.1':
optional: true
'@rollup/rollup-linux-arm64-gnu@4.57.1':
optional: true
'@rollup/rollup-linux-arm64-musl@4.57.1':
optional: true
'@rollup/rollup-linux-loong64-gnu@4.57.1':
optional: true
'@rollup/rollup-linux-loong64-musl@4.57.1':
optional: true
'@rollup/rollup-linux-ppc64-gnu@4.57.1':
optional: true
'@rollup/rollup-linux-ppc64-musl@4.57.1':
optional: true
'@rollup/rollup-linux-riscv64-gnu@4.57.1':
optional: true
'@rollup/rollup-linux-riscv64-musl@4.57.1':
optional: true
'@rollup/rollup-linux-s390x-gnu@4.57.1':
optional: true
'@rollup/rollup-linux-x64-gnu@4.57.1':
optional: true
'@rollup/rollup-linux-x64-musl@4.57.1':
optional: true
'@rollup/rollup-openbsd-x64@4.57.1':
optional: true
'@rollup/rollup-openharmony-arm64@4.57.1':
optional: true
'@rollup/rollup-win32-arm64-msvc@4.57.1':
optional: true
'@rollup/rollup-win32-ia32-msvc@4.57.1':
optional: true
'@rollup/rollup-win32-x64-gnu@4.57.1':
optional: true
'@rollup/rollup-win32-x64-msvc@4.57.1':
optional: true
'@types/estree@1.0.8': {}
'@types/resolve@1.20.2': {}
deepmerge@4.3.1: {}
estree-walker@2.0.2: {}
fsevents@2.3.3:
optional: true
function-bind@1.1.2: {}
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
is-core-module@2.16.1:
dependencies:
hasown: 2.0.2
is-module@1.0.0: {}
js-tokens@4.0.0:
optional: true
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
path-parse@1.0.7: {}
picocolors@1.1.1:
optional: true
picomatch@4.0.3: {}
resolve@1.22.11:
dependencies:
is-core-module: 2.16.1
path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0
rollup-plugin-dts@6.3.0(rollup@4.57.1)(typescript@5.9.3):
dependencies:
magic-string: 0.30.21
rollup: 4.57.1
typescript: 5.9.3
optionalDependencies:
'@babel/code-frame': 7.28.6
rollup@4.57.1:
dependencies:
'@types/estree': 1.0.8
optionalDependencies:
'@rollup/rollup-android-arm-eabi': 4.57.1
'@rollup/rollup-android-arm64': 4.57.1
'@rollup/rollup-darwin-arm64': 4.57.1
'@rollup/rollup-darwin-x64': 4.57.1
'@rollup/rollup-freebsd-arm64': 4.57.1
'@rollup/rollup-freebsd-x64': 4.57.1
'@rollup/rollup-linux-arm-gnueabihf': 4.57.1
'@rollup/rollup-linux-arm-musleabihf': 4.57.1
'@rollup/rollup-linux-arm64-gnu': 4.57.1
'@rollup/rollup-linux-arm64-musl': 4.57.1
'@rollup/rollup-linux-loong64-gnu': 4.57.1
'@rollup/rollup-linux-loong64-musl': 4.57.1
'@rollup/rollup-linux-ppc64-gnu': 4.57.1
'@rollup/rollup-linux-ppc64-musl': 4.57.1
'@rollup/rollup-linux-riscv64-gnu': 4.57.1
'@rollup/rollup-linux-riscv64-musl': 4.57.1
'@rollup/rollup-linux-s390x-gnu': 4.57.1
'@rollup/rollup-linux-x64-gnu': 4.57.1
'@rollup/rollup-linux-x64-musl': 4.57.1
'@rollup/rollup-openbsd-x64': 4.57.1
'@rollup/rollup-openharmony-arm64': 4.57.1
'@rollup/rollup-win32-arm64-msvc': 4.57.1
'@rollup/rollup-win32-ia32-msvc': 4.57.1
'@rollup/rollup-win32-x64-gnu': 4.57.1
'@rollup/rollup-win32-x64-msvc': 4.57.1
fsevents: 2.3.3
supports-preserve-symlinks-flag@1.0.0: {}
tslib@2.8.1: {}
typescript@5.9.3: {} typescript@5.9.3: {}
undici-types@7.16.0: {}
zod@4.3.6: {}
zustand@5.0.11: {} zustand@5.0.11: {}

View File

@@ -1,81 +0,0 @@
// rollup.config.js
import typescript from '@rollup/plugin-typescript';
import resolve from '@rollup/plugin-node-resolve';
import { dts } from 'rollup-plugin-dts';
/**
* @type {import('rollup').RollupOptions}
*/
export default [
{
input: 'src/index.ts', // TypeScript 入口文件
output: {
file: 'dist/query-browser.js', // 输出文件
format: 'es', // 输出格式设置为 ES 模块
},
plugins: [
resolve(), // 使用 @rollup/plugin-node-resolve 解析 node_modules 中的模块
typescript(), // 使用 @rollup/plugin-typescript 处理 TypeScript 文件
],
},
{
input: 'src/index.ts', // TypeScript 入口文件
output: {
file: 'dist/query-browser.d.ts', // 输出文件
format: 'es', // 输出格式设置为 ES 模块
},
plugins: [dts()],
},
{
input: 'src/query.ts',
output: {
file: 'dist/query.js',
format: 'es',
},
plugins: [resolve(), typescript()],
},
{
input: 'src/query.ts',
output: {
file: 'dist/query.d.ts',
format: 'es',
},
plugins: [dts()],
},
{
input: 'src/ws.ts', // TypeScript 入口文件
output: {
file: 'dist/query-ws.js', // 输出文件
format: 'es', // 输出格式设置为 ES 模块
},
plugins: [
resolve(), // 使用 @rollup/plugin-node-resolve 解析 node_modules 中的模块
typescript(), // 使用 @rollup/plugin-typescript 处理 TypeScript 文件
],
},
{
input: 'src/ws.ts', // TypeScript 入口文件
output: {
file: 'dist/query-ws.d.ts', // 输出文件
format: 'es', // 输出格式设置为 ES 模块
},
plugins: [dts()],
},
{
input: 'src/adapter.ts',
output: {
file: 'dist/query-adapter.js',
format: 'es',
},
plugins: [resolve(), typescript()],
},
{
input: 'src/adapter.ts', // TypeScript 入口文件
output: {
file: 'dist/query-adapter.d.ts', // 输出文件
format: 'es', // 输出格式设置为 ES 模块
},
plugins: [dts()],
},
];

View File

@@ -60,7 +60,7 @@ export const adapter = async (opts: AdapterOpts = {}, overloadOpts?: RequestInit
if (opts?.url?.startsWith('http')) { if (opts?.url?.startsWith('http')) {
url = new URL(opts.url); url = new URL(opts.url);
} else { } else {
origin = window?.location?.origin || 'http://localhost:51515'; origin = globalThis?.location?.origin || 'http://localhost:51515';
url = new URL(opts?.url || '', origin); url = new URL(opts?.url || '', origin);
} }
const isGet = method === 'GET'; const isGet = method === 'GET';

130
src/create-query/index.ts Normal file
View File

@@ -0,0 +1,130 @@
type RouteInfo = {
path: string;
key: string;
id: string;
description?: string;
metadata?: {
summary?: string;
args?: Record<string, any>;
};
}
export const createQueryByRoutes = (list: RouteInfo[]) => {
const obj: any = {};
for (const route of list) {
if (!obj[route.path]) {
obj[route.path] = {};
}
obj[route.path][route.key] = route;
}
const code = `
import { createQueryApi } from '@kevisual/query/api';
const api = ${generateApiCode(obj)} as const;
const queryApi = createQueryApi({ api });
export { queryApi };
`
return code;
}
// 生成带注释的对象字符串
function generateApiCode(obj: any): string {
let code = '{\n';
const paths = Object.keys(obj);
for (let i = 0; i < paths.length; i++) {
const path = paths[i];
const methods = obj[path];
code += ` "${path}": {\n`;
const keys = Object.keys(methods);
for (let j = 0; j < keys.length; j++) {
const key = keys[j];
const route = methods[key];
if (route?.id) {
if (route.id.startsWith('rand-')) {
delete route.id; // 删除随机生成的 ID
}
}
const description = route?.metadata?.summary || route?.description || '';
const args = route?.metadata?.args || {};
// 添加 JSDoc 注释
if (description || Object.keys(args).length > 0) {
code += ` /**\n`;
// 添加主描述
if (description) {
// 转义描述中的特殊字符
const escapedDescription = description
.replace(/\\/g, '\\\\') // 转义反斜杠
.replace(/\*/g, '\\*') // 转义星号
.replace(/\n/g, '\n * '); // 处理多行描述
code += ` * ${escapedDescription}\n`;
}
// 添加参数描述
if (Object.keys(args).length > 0) {
if (description) {
code += ` *\n`; // 添加空行分隔
}
code += ` * @param data - Request parameters\n`;
for (const [argName, schema] of Object.entries(args)) {
const argSchema = schema as any;
const argType = argSchema.type || 'unknown';
const argDesc = argSchema.description || '';
// 构建类型信息
let typeInfo = argType;
if (argType === 'string' && argSchema.enum) {
typeInfo = argSchema.enum.map((v: any) => `"${v}"`).join(' | ');
} else if (argType === 'number' || argType === 'integer') {
const constraints = [];
if (argSchema.minimum !== undefined) constraints.push(`min: ${argSchema.minimum}`);
if (argSchema.maximum !== undefined) constraints.push(`max: ${argSchema.maximum}`);
if (argSchema.exclusiveMinimum !== undefined) constraints.push(`> ${argSchema.exclusiveMinimum}`);
if (argSchema.exclusiveMaximum !== undefined) constraints.push(`< ${argSchema.exclusiveMaximum}`);
if (constraints.length > 0) typeInfo += ` (${constraints.join(', ')})`;
} else if (argType === 'string') {
const constraints = [];
if (argSchema.minLength !== undefined) constraints.push(`minLength: ${argSchema.minLength}`);
if (argSchema.maxLength !== undefined) constraints.push(`maxLength: ${argSchema.maxLength}`);
if (argSchema.format) constraints.push(`format: ${argSchema.format}`);
if (constraints.length > 0) typeInfo += ` (${constraints.join(', ')})`;
}
// 转义参数描述
const escapedArgDesc = argDesc
.replace(/\\/g, '\\\\')
.replace(/\*/g, '\\*')
.replace(/\n/g, ' ');
code += ` * @param data.${argName} - {${typeInfo}}${escapedArgDesc ? ' ' + escapedArgDesc : ''}\n`;
}
}
code += ` */\n`;
}
code += ` "${key}": ${JSON.stringify(route, null, 2).split('\n').map((line, idx) =>
idx === 0 ? line : ' ' + line
).join('\n')}`;
if (j < keys.length - 1) {
code += ',';
}
code += '\n';
}
code += ` }`;
if (i < paths.length - 1) {
code += ',';
}
code += '\n';
}
code += '}';
return code;
}

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 });

View File

@@ -1,9 +1,9 @@
import { adapter } from './adapter.ts'; import { adapter } from './adapter.ts';
import { QueryWs, QueryWsOpts } from './ws.ts'; import { QueryWs, QueryWsOpts } from './ws.ts';
import { Query, ClientQuery } from './query.ts'; import { Query } from './query.ts';
import { BaseQuery, QueryOptions, wrapperError } from './query.ts'; import { BaseQuery, QueryOptions, wrapperError } from './query.ts';
export { QueryOpts, QueryWs, ClientQuery, Query, QueryWsOpts, adapter, BaseQuery, wrapperError }; export { QueryOpts, QueryWs, Query, QueryWsOpts, adapter, BaseQuery, wrapperError };
export { QueryOptions } export { QueryOptions }
export type { DataOpts, Result, Data } from './query.ts'; export type { DataOpts, Result, Data } from './query.ts';

View File

@@ -162,6 +162,7 @@ export class Query {
*/ */
async post<R = any, P = any>(body: Data & P, options?: DataOpts): Promise<Result<R>> { async post<R = any, P = any>(body: Data & P, options?: DataOpts): Promise<Result<R>> {
const url = options?.url || this.url; const url = options?.url || this.url;
console.log('query post', url, body, options);
const { headers, adapter, beforeRequest, afterResponse, timeout, ...rest } = options || {}; const { headers, adapter, beforeRequest, afterResponse, timeout, ...rest } = options || {};
const _headers = { ...this.headers, ...headers }; const _headers = { ...this.headers, ...headers };
const _adapter = adapter || this.adapter; const _adapter = adapter || this.adapter;
@@ -301,13 +302,3 @@ export class BaseQuery<T extends Query = Query, R extends { queryChain?: any; qu
return this.query.get(data, options); return this.query.get(data, options);
} }
} }
/**
* @deprecated
* 前端调用后端QueryRouter, 默认路径 /client/router
*/
export class ClientQuery extends Query {
constructor(opts?: QueryOpts) {
super({ ...opts, url: opts?.url || '/client/router' });
}
}

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 是可选的
}

View File

@@ -7,6 +7,7 @@
"sourceMap": false, "sourceMap": false,
"allowJs": true, "allowJs": true,
"newLine": "LF", "newLine": "LF",
"rootDir": ".",
"baseUrl": "./", "baseUrl": "./",
"declaration": false, "declaration": false,
"typeRoots": [ "typeRoots": [
@@ -21,20 +22,17 @@
"paths": { "paths": {
"@/*": [ "@/*": [
"src/*" "src/*"
],
"*": [
"types/*"
] ]
} }
}, },
"include": [ "include": [
"typings.d.ts", "typings.d.ts",
"src/**/*.ts" "src/**/*.ts",
"test/**/*.ts"
], ],
"exclude": [ "exclude": [
"node_modules", "node_modules",
"demo/simple/dist", "demo/simple/dist",
"src/**/*.test.ts", "src/**/*.test.ts",
"rollup.config.js",
] ]
} }