diff --git a/kubb.config.ts b/kubb.config.ts new file mode 100644 index 0000000..b67bb6a --- /dev/null +++ b/kubb.config.ts @@ -0,0 +1,24 @@ +import { defineConfig } from "kubb/config"; +import { pluginFetch } from "@kubb/plugin-fetch"; +import { pluginTs } from "@kubb/plugin-ts"; + +export default defineConfig({ + root: ".", + input: "./src/clients/events.openapi.json", + output: { + path: "./src/clients/events", + barrel: false, + clean: true, + postGenerate: [ + { + name: "fix-extensions", + command: + "find ./src/clients/events -name '*.ts' -type f -exec sed -i '' -E \"s/from '(\\.[^']*)'/from '\\1.ts'/g; s/\\.ts\\.ts'/\\.ts'/g\" {} +", + }, + ], + }, + plugins: [ + pluginTs({ output: { path: "types.ts", mode: "file" } }), + pluginFetch({ output: { path: "client.ts", mode: "file" } }), + ], +}); diff --git a/package-lock.json b/package-lock.json index e1b1291..ef00351 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,12 +15,59 @@ "json-schema-to-ts": "^3.1.1" }, "devDependencies": { + "@kubb/plugin-fetch": "^5.0.0-beta.94", + "@kubb/plugin-ts": "^5.0.0-beta.87", "@types/node": "^26.1.0", + "kubb": "^5.0.0-beta.94", "oxfmt": "^0.57.0", "oxlint": "^1.72.0", "typescript": "^6.0.3" } }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-14.2.1.tgz", + "integrity": "sha512-HmdFw9CDYqM6B25pqGBpNeLCKvGPlIx1EbLrVL0zPvj50CJQUHyBNBw45Muk0kEIkogo1VZvOKHajdMuAzSxRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 20" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + }, + "peerDependencies": { + "@types/json-schema": "^7.0.15" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/runtime": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", @@ -30,6 +77,36 @@ "node": ">=6.9.0" } }, + "node_modules/@clack/core": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.4.3.tgz", + "integrity": "sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@clack/prompts": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.7.0.tgz", + "integrity": "sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@clack/core": "1.4.3", + "fast-string-width": "^3.0.2", + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, "node_modules/@fastify/ajv-compiler": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.5.tgz", @@ -150,6 +227,286 @@ "json-schema-to-ts": "^3.1.0" } }, + "node_modules/@humanwhocodes/momoa": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@humanwhocodes/momoa/-/momoa-2.0.4.tgz", + "integrity": "sha512-RE815I4arJFtt+FVeU1Tgp9/Xvecacji8w/V6XtXsWWH/wz/eNkNbhb+ny/+PlVZjV0rxQpRSQKNKE3lcktHEA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@kubb/adapter-oas": { + "version": "5.0.0-beta.94", + "resolved": "https://registry.npmjs.org/@kubb/adapter-oas/-/adapter-oas-5.0.0-beta.94.tgz", + "integrity": "sha512-qwbirjiBp0ad3sJhJrDVoAEAYZgKmGXOMN4XIlFP74mP2v2Tcub89HaIdsV8uq1Z2JJ7JnFSkSu4Vunqmf2wiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@kubb/ast": "5.0.0-beta.94", + "@kubb/core": "5.0.0-beta.94", + "@readme/openapi-parser": "^6.2.0", + "@scalar/openapi-upgrader": "^0.2.9", + "api-ref-bundler": "0.5.1", + "yaml": "^2.9.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@kubb/ast": { + "version": "5.0.0-beta.94", + "resolved": "https://registry.npmjs.org/@kubb/ast/-/ast-5.0.0-beta.94.tgz", + "integrity": "sha512-lgcI6Op1eRuEt0saRmapvGpTEqHIaYJ8ciBxyyROV4D9NFbo6tPkuKDGDHtAhBNoZ3PQj+WXvR0Tee3xY7tWTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + } + }, + "node_modules/@kubb/cli": { + "version": "5.0.0-beta.94", + "resolved": "https://registry.npmjs.org/@kubb/cli/-/cli-5.0.0-beta.94.tgz", + "integrity": "sha512-2Dr6Xdj99XF4euSwDdHKew/edyLUUdLt4EyWB32DoilWtG9KmbMdnAvq12kW4BeZAh9daArejehrtOd/4lLEzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@clack/prompts": "^1.7.0", + "@kubb/core": "5.0.0-beta.94", + "chokidar": "^5.0.0", + "gunshi": "^0.35.1", + "jiti": "^2.7.0", + "tinyexec": "^1.2.4", + "unconfig": "^7.5.0" + }, + "bin": { + "kubb": "bin/kubb.js" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "@kubb/adapter-oas": "5.0.0-beta.94", + "@kubb/mcp": "5.0.0-beta.94" + }, + "peerDependenciesMeta": { + "@kubb/adapter-oas": { + "optional": true + }, + "@kubb/mcp": { + "optional": true + } + } + }, + "node_modules/@kubb/core": { + "version": "5.0.0-beta.94", + "resolved": "https://registry.npmjs.org/@kubb/core/-/core-5.0.0-beta.94.tgz", + "integrity": "sha512-xthWSSKur/pGQJEYpKQUdnLIuFGL7nj4Sq++NQdEMWSnVtQpAnZ8GKRO2pcucWWTlFulQyHnHt7k8f9Ve/jDww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@kubb/ast": "5.0.0-beta.94" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@kubb/kit": { + "version": "5.0.0-beta.94", + "resolved": "https://registry.npmjs.org/@kubb/kit/-/kit-5.0.0-beta.94.tgz", + "integrity": "sha512-un0ItkmbWT4Hp18qPP37Ma+v4hnE6Ga7eLgot+wS2021Al5LHEzrYYe/tAAS14oraszT/Dw5Ab6K2WOz3l5k5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@kubb/ast": "5.0.0-beta.94", + "@kubb/core": "5.0.0-beta.94" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@kubb/mcp": { + "version": "5.0.0-beta.94", + "resolved": "https://registry.npmjs.org/@kubb/mcp/-/mcp-5.0.0-beta.94.tgz", + "integrity": "sha512-LxOPSabSVMRf+47AaTxB53x9Exp5SY/nUg/VAdd2I7Dq7OIHoS2WxwgiEyCXGOBd1g0HUGm2eH76ERROY57oSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@kubb/adapter-oas": "5.0.0-beta.94", + "@kubb/core": "5.0.0-beta.94", + "@tmcp/adapter-valibot": "^0.1.6", + "@tmcp/transport-stdio": "^0.4.3", + "tmcp": "^1.19.4", + "valibot": "^1.4.2" + }, + "bin": { + "kubb-mcp": "bin/kubb-mcp.cjs" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "@kubb/renderer-jsx": "5.0.0-beta.94" + } + }, + "node_modules/@kubb/parser-md": { + "version": "5.0.0-beta.94", + "resolved": "https://registry.npmjs.org/@kubb/parser-md/-/parser-md-5.0.0-beta.94.tgz", + "integrity": "sha512-fotbf0dRimaJxK2a57MA70nhkdhX6NKS+hE3E6PfcKtbVHit+x0waYlOOlLpaOTEx/cI2s3MIHnLzJZENArjig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@kubb/ast": "5.0.0-beta.94", + "@kubb/core": "5.0.0-beta.94", + "yaml": "^2.9.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@kubb/parser-ts": { + "version": "5.0.0-beta.94", + "resolved": "https://registry.npmjs.org/@kubb/parser-ts/-/parser-ts-5.0.0-beta.94.tgz", + "integrity": "sha512-AEKkamlomFHAID/j24aU4Lprzj9WAaLMe5tT3c+EF3hKPi7TglnBJ8TBRY1PCVlEoPxoGP1XzXnfwNzmOhjHZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@kubb/core": "5.0.0-beta.94", + "typescript": "^6.0.3" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@kubb/plugin-barrel": { + "version": "5.0.0-beta.94", + "resolved": "https://registry.npmjs.org/@kubb/plugin-barrel/-/plugin-barrel-5.0.0-beta.94.tgz", + "integrity": "sha512-DPpWGQNHduOSFfe5gIILzBwZO6caZ5Xf4qmDMu72erNtJReFPhj9q4kXrihGclJYon8K9TtG45JNgX6vS9lbyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@kubb/ast": "5.0.0-beta.94", + "@kubb/core": "5.0.0-beta.94" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "@kubb/core": "5.0.0-beta.94" + } + }, + "node_modules/@kubb/plugin-fetch": { + "version": "5.0.0-beta.94", + "resolved": "https://registry.npmjs.org/@kubb/plugin-fetch/-/plugin-fetch-5.0.0-beta.94.tgz", + "integrity": "sha512-6/mDbQg6JN91/c0yp9mIlnN2VgoWdI1L+5N3p5QqUFOxv3wM9j+NF7oFMtywTdkUxrP7d+FJpeZI54Wj1pMeJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@kubb/plugin-ts": "5.0.0-beta.87", + "@kubb/plugin-zod": "5.0.0-beta.87" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "kubb": "5.0.0-beta.94" + } + }, + "node_modules/@kubb/plugin-ts": { + "version": "5.0.0-beta.87", + "resolved": "https://registry.npmjs.org/@kubb/plugin-ts/-/plugin-ts-5.0.0-beta.87.tgz", + "integrity": "sha512-qBj5ElqHp/KlTvF3VQgeFgAXL1+3ptrpdS9YY7OAWJWLnQ3q/VN1ODvI6KlS8LUl6MjvYu7feNEZCRYbnBCNpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@kubb/parser-ts": "5.0.0-beta.94", + "typescript": "^6.0.3" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "kubb": "5.0.0-beta.94" + } + }, + "node_modules/@kubb/plugin-zod": { + "version": "5.0.0-beta.87", + "resolved": "https://registry.npmjs.org/@kubb/plugin-zod/-/plugin-zod-5.0.0-beta.87.tgz", + "integrity": "sha512-YCF04Wl5VMOLxLoaA/cvOZBH9j4jnzsaIm04AZPiGVc+aZKIJBRDjuGc3lZebDUE34IkRvUEufaba6Onraa9Nw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "kubb": "5.0.0-beta.94" + } + }, + "node_modules/@kubb/renderer-jsx": { + "version": "5.0.0-beta.94", + "resolved": "https://registry.npmjs.org/@kubb/renderer-jsx/-/renderer-jsx-5.0.0-beta.94.tgz", + "integrity": "sha512-Kj8yL7RePTYEkHCXZyH29n6BYdV+PbfZ9Gxde6XL7kfzOFE+Y+QvVRUjaXeRZLxG3xWkGtV6WOAQDi450BD72w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@kubb/ast": "5.0.0-beta.94", + "yaml": "^2.9.0" + }, + "engines": { + "node": ">=22" + } + }, "node_modules/@oxfmt/binding-android-arm-eabi": { "version": "0.57.0", "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.57.0.tgz", @@ -850,6 +1207,97 @@ "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", "license": "MIT" }, + "node_modules/@quansync/fs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@quansync/fs/-/fs-1.0.0.tgz", + "integrity": "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "quansync": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/@readme/better-ajv-errors": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@readme/better-ajv-errors/-/better-ajv-errors-2.4.0.tgz", + "integrity": "sha512-9WODaOAKSl/mU+MYNZ2aHCrkoRSvmQ+1YkLj589OEqqjOAhbn8j7Z+ilYoiTu/he6X63/clsxxAB4qny9/dDzg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/code-frame": "^7.22.5", + "@babel/runtime": "^7.22.5", + "@humanwhocodes/momoa": "^2.0.3", + "jsonpointer": "^5.0.0", + "leven": "^3.1.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "ajv": "4.11.8 - 8" + } + }, + "node_modules/@readme/openapi-parser": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@readme/openapi-parser/-/openapi-parser-6.2.1.tgz", + "integrity": "sha512-15kP7exAU30ycF0u5/nW6J64HFzqxKYp06wErb1+cGU2rYz2dDBtf+63RjGLfGEhXND69OXIZ7RvjS7Tu7s1Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^14.1.1", + "@readme/better-ajv-errors": "^2.3.2", + "@readme/openapi-schemas": "^4.0.0", + "@types/json-schema": "^7.0.15", + "ajv": "^8.20.0", + "ajv-draft-04": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, + "node_modules/@readme/openapi-schemas": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@readme/openapi-schemas/-/openapi-schemas-4.0.0.tgz", + "integrity": "sha512-hrG//9/+RpOZTrUDirU4OzfMqaxCdY5BMpr3YuLf3Rje9rfNsydQJsH9iCPTie9ZRu9xe+0OUD4fHe0JmGwa2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@scalar/openapi-types": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@scalar/openapi-types/-/openapi-types-0.9.1.tgz", + "integrity": "sha512-gkGhSkxSzADaBiNg+ZAbJuwj+ZUmzP2Pg9CWZ7ZP+0fck2WjPeDDM7aAbouAm0aQQMF9xBjSPXSA9a/qTHYaTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/openapi-upgrader": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@scalar/openapi-upgrader/-/openapi-upgrader-0.2.9.tgz", + "integrity": "sha512-D5b0rGLLZgmkO9mdW2j/ND1KBlH1u3RCpr87HPxv9P9ZSr6PtM5iLqFOJq0ACiaHjY2mikCrxgDmnUEhTzRpHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@scalar/openapi-types": "0.9.1" + }, + "engines": { + "node": ">=22" + } + }, "node_modules/@slack/logger": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@slack/logger/-/logger-4.0.1.tgz", @@ -897,6 +1345,46 @@ "npm": ">= 8.6.0" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tmcp/adapter-valibot": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@tmcp/adapter-valibot/-/adapter-valibot-0.1.6.tgz", + "integrity": "sha512-drirZeNinhYLiRSMksN+m//u0ImFxtGRk1Vp425Xp/7CbBXFQdjAG+f7grssyHAukbVTGzmWsMMP6ejrGVErUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@valibot/to-json-schema": "^1.3.0", + "valibot": "^1.1.0" + }, + "peerDependencies": { + "tmcp": "^1.17.0", + "valibot": "^1.1.0" + } + }, + "node_modules/@tmcp/transport-stdio": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@tmcp/transport-stdio/-/transport-stdio-0.4.3.tgz", + "integrity": "sha512-chdmSwk7PSXjkaQId2OAY8ZMFrTFWwckfCyXJuw6b6WFK9IMK95EoU2VDFioybcmawuQox0MNPM5R2DVxlptoA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "tmcp": "^1.16.3" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "26.1.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.0.tgz", @@ -912,6 +1400,16 @@ "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", "license": "MIT" }, + "node_modules/@valibot/to-json-schema": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@valibot/to-json-schema/-/to-json-schema-1.7.1.tgz", + "integrity": "sha512-3qkmU6KXWh8GIThEAW3kuRHPQBMjWkKy+Ppz3WkUucx53DTpOa6siMn4xDGSOhlVyMrDaJTCTMLYPZVAIk1P0A==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "valibot": "^1.4.0" + } + }, "node_modules/abstract-logging": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", @@ -946,6 +1444,21 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/ajv-formats": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", @@ -963,6 +1476,26 @@ } } }, + "node_modules/api-ref-bundler": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/api-ref-bundler/-/api-ref-bundler-0.5.1.tgz", + "integrity": "sha512-g5YOyKvQVDaTpRpDEmRtnAWZbi1Ur/i9qPJRVf6l9wafg+LJuJn6Aze+wI9h+1qpUSgvLE1CGCiOFlrrBwYEyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-crawl": "0.4.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1023,6 +1556,22 @@ "node": ">= 0.4" } }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1065,6 +1614,13 @@ } } }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "dev": true, + "license": "MIT" + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -1193,6 +1749,23 @@ "fast-decode-uri-component": "^1.0.1" } }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, "node_modules/fast-uri": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", @@ -1209,6 +1782,16 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, "node_modules/fastify": { "version": "5.9.0", "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.9.0.tgz", @@ -1359,6 +1942,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gunshi": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/gunshi/-/gunshi-0.35.3.tgz", + "integrity": "sha512-GU3tVKZlGBSXfg8WLfVWt1tfCaVkwzFgyUGCwC35/hUQA8Q/YL3iAmhcr6cbLK8nxXNexozsOHCDDAI/1uuGCQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 22" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -1438,6 +2034,63 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-crawl": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/json-crawl/-/json-crawl-0.4.2.tgz", + "integrity": "sha512-MOKk9cjtpMrP4H1DmG+PtS7aFWg9OuSqxc/wpFcOE++yVnD5Bi5iKoXD6oqs+kltRwJRgZBYMozRNpQpxD/TJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/json-rpc-2.0": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/json-rpc-2.0/-/json-rpc-2.0-1.7.1.tgz", + "integrity": "sha512-JqZjhjAanbpkXIzFE7u8mE/iFblawwlXtONaCvRqI+pyABVz7B4M1EUNpyVW+dZjqgQ2L5HFmZCmOCgUKm00hg==", + "dev": true, + "license": "MIT" + }, "node_modules/json-schema-ref-resolver": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", @@ -1476,6 +2129,52 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kubb": { + "version": "5.0.0-beta.94", + "resolved": "https://registry.npmjs.org/kubb/-/kubb-5.0.0-beta.94.tgz", + "integrity": "sha512-BCQ1SzoceQXBvHLm44ddFEyhN7Eun0RPqb/6MdyYP5thn803AvyallooupLp0VMvLp6/03/7pDMFSY4Xc1tktw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@kubb/adapter-oas": "5.0.0-beta.94", + "@kubb/ast": "5.0.0-beta.94", + "@kubb/cli": "5.0.0-beta.94", + "@kubb/core": "5.0.0-beta.94", + "@kubb/kit": "5.0.0-beta.94", + "@kubb/mcp": "5.0.0-beta.94", + "@kubb/parser-md": "5.0.0-beta.94", + "@kubb/parser-ts": "5.0.0-beta.94", + "@kubb/plugin-barrel": "5.0.0-beta.94", + "@kubb/renderer-jsx": "5.0.0-beta.94", + "unplugin-kubb": "5.0.0-beta.94" + }, + "bin": { + "kubb": "bin/kubb.js" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/light-my-request": { "version": "6.6.0", "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", @@ -1558,6 +2257,14 @@ "node": ">=14.0.0" } }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/oxfmt": { "version": "0.57.0", "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.57.0.tgz", @@ -1715,6 +2422,26 @@ "node": ">=8" } }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/pino": { "version": "10.3.1", "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", @@ -1777,12 +2504,43 @@ "node": ">=10" } }, + "node_modules/quansync": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-1.0.0.tgz", + "integrity": "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, "node_modules/quick-format-unescaped": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", "license": "MIT" }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/real-require": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", @@ -1900,6 +2658,13 @@ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", "license": "MIT" }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, "node_modules/sonic-boom": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", @@ -1918,6 +2683,13 @@ "node": ">= 10.x" } }, + "node_modules/sqids": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/sqids/-/sqids-0.3.0.tgz", + "integrity": "sha512-lOQK1ucVg+W6n3FhRwwSeUijxe93b51Bfz5PMRMihVf1iVkl82ePQG7V5vwrhzB11v0NtsR25PSZRGiSomJaJw==", + "dev": true, + "license": "MIT" + }, "node_modules/thread-stream": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", @@ -1936,6 +2708,16 @@ "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==", "license": "MIT" }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinypool": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-2.1.0.tgz", @@ -1946,6 +2728,20 @@ "node": "^20.0.0 || >=22.0.0" } }, + "node_modules/tmcp": { + "version": "1.19.4", + "resolved": "https://registry.npmjs.org/tmcp/-/tmcp-1.19.4.tgz", + "integrity": "sha512-fMoUJ3Gef9iA0yKZNeW2SQCCKTluCwghUyOz/qxPS8XxrQk6Jlw4lgql3S+s6L//FteLeSJ7erKxMWl727mZoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "json-rpc-2.0": "^1.7.1", + "sqids": "^0.3.0", + "uri-template-matcher": "^1.1.1", + "valibot": "^1.1.0" + } + }, "node_modules/toad-cache": { "version": "3.7.4", "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.4.tgz", @@ -1975,11 +2771,196 @@ "node": ">=14.17" } }, + "node_modules/unconfig": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/unconfig/-/unconfig-7.5.0.tgz", + "integrity": "sha512-oi8Qy2JV4D3UQ0PsopR28CzdQ3S/5A1zwsUwp/rosSbfhJ5z7b90bIyTwi/F7hCLD4SGcZVjDzd4XoUQcEanvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@quansync/fs": "^1.0.0", + "defu": "^6.1.4", + "jiti": "^2.6.1", + "quansync": "^1.0.0", + "unconfig-core": "7.5.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/unconfig-core": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/unconfig-core/-/unconfig-core-7.5.0.tgz", + "integrity": "sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@quansync/fs": "^1.0.0", + "quansync": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/undici-types": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", "license": "MIT" + }, + "node_modules/unplugin": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.3.0.tgz", + "integrity": "sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "picomatch": "^4.0.4", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@farmfe/core": "*", + "@rspack/core": "*", + "bun-types-no-globals": "*", + "esbuild": "*", + "rolldown": "*", + "rollup": "*", + "unloader": "*", + "vite": "*", + "webpack": "*" + }, + "peerDependenciesMeta": { + "@farmfe/core": { + "optional": true + }, + "@rspack/core": { + "optional": true + }, + "bun-types-no-globals": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "rolldown": { + "optional": true + }, + "rollup": { + "optional": true + }, + "unloader": { + "optional": true + }, + "vite": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/unplugin-kubb": { + "version": "5.0.0-beta.94", + "resolved": "https://registry.npmjs.org/unplugin-kubb/-/unplugin-kubb-5.0.0-beta.94.tgz", + "integrity": "sha512-HubKagXXf2fNmfAQ2jz8+RY841dUuRUMB167tCwXbbTdN076f8Zi1StL6ui2YUKQO+PDKV+8BBlxexCfPlpQpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@kubb/adapter-oas": "5.0.0-beta.94", + "@kubb/core": "5.0.0-beta.94", + "@kubb/parser-ts": "5.0.0-beta.94", + "@kubb/plugin-barrel": "5.0.0-beta.94", + "unplugin": "^3.3.0" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "@farmfe/core": "^1.7.0", + "@kubb/core": "^5.0.0-alpha.0", + "@nuxt/kit": "^3 || ^4", + "@nuxt/schema": "^3 || ^4", + "esbuild": "*", + "rolldown": ">=1", + "rollup": "^3 || ^4", + "vite": ">=3", + "webpack": "^5" + }, + "peerDependenciesMeta": { + "@farmfe/core": { + "optional": true + }, + "@nuxt/kit": { + "optional": true + }, + "@nuxt/schema": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "rolldown": { + "optional": true + }, + "rollup": { + "optional": true + }, + "vite": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/uri-template-matcher": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/uri-template-matcher/-/uri-template-matcher-1.1.2.tgz", + "integrity": "sha512-uZc1h12jdO3m/R77SfTEOuo6VbMhgWznaawKpBjRGSJb7i91x5PgI37NQJtG+Cerxkk0yr1pylBY2qG1kQ+aEQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/valibot": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.4.2.tgz", + "integrity": "sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } } } } diff --git a/package.json b/package.json index 2c5f0c3..c2e7ce6 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "author": "Seth Etter ", "type": "module", "scripts": { + "generate": "kubb generate", "test": "node --test", "typecheck": "tsc", "lint": "oxlint", @@ -19,7 +20,10 @@ "json-schema-to-ts": "^3.1.1" }, "devDependencies": { + "@kubb/plugin-fetch": "^5.0.0-beta.94", + "@kubb/plugin-ts": "^5.0.0-beta.87", "@types/node": "^26.1.0", + "kubb": "^5.0.0-beta.94", "oxfmt": "^0.57.0", "oxlint": "^1.72.0", "typescript": "^6.0.3" diff --git a/src/clients/events.openapi.json b/src/clients/events.openapi.json new file mode 100644 index 0000000..37999ce --- /dev/null +++ b/src/clients/events.openapi.json @@ -0,0 +1,92 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "devict-events API", + "version": "0.1.0" + }, + "components": { + "schemas": {} + }, + "paths": { + "/api/upcoming": { + "get": { + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "location": { + "type": "string" + }, + "lat": { + "type": "string" + }, + "lon": { + "type": "string" + }, + "startDate": { + "type": "string", + "format": "date-time" + }, + "endDate": { + "type": "string", + "format": "date-time" + }, + "series": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "image": { + "type": "object", + "properties": { + "path": { + "type": "string" + } + } + } + }, + "required": [ + "name", + "slug", + "location", + "startDate", + "endDate" + ] + } + } + } + } + } + } + } + }, + "/api/openapi.json": { + "get": { + "responses": { + "200": { + "description": "Default Response" + } + } + } + } + } +} diff --git a/src/clients/events/.kubb/client.ts b/src/clients/events/.kubb/client.ts new file mode 100644 index 0000000..5766e34 --- /dev/null +++ b/src/clients/events/.kubb/client.ts @@ -0,0 +1,818 @@ +import { applyHeaderStyles, defaultBodySerializer, defaultPathSerializer, defaultQuerySerializer, isDefaultJsonBody, serializeCookies } from './serializers.ts' +import type { HeadersInit, PathParamStyle, PathSerializer, Serializers, Styles } from './serializers.ts' +import { type StandardSchemaValidator, validateStandardSchema } from './standardSchema.ts' + +/** + * HTTP status codes treated as a success, everything else is an error. + */ +export type SuccessStatusCode = '200' | '201' | '202' | '203' | '204' | '205' | '206' | '207' | '208' | '226' + +/** + * The success members of a per-status responses record. + */ +export type SuccessOf = TResponses[Extract] + +/** + * The error members of a per-status responses record, every documented status that is not a 2xx. + */ +export type ErrorOf = TResponses[Exclude] + +/** + * Converts a response record's string status key to its numeric literal, leaving non-numeric keys like `default` as `number`. + */ +export type ToStatusNumber = TStatus extends `${infer TNumber extends number}` ? TNumber : number + +/** + * The plain body of a per-status response, unwrapping the `{ contentType; data }` union so an error result keeps the bare body union on `error`. + */ +export type DataOf = T extends { contentType: string; data: infer TData } ? TData : T + +/** + * The success variant for a single status, flattened so the negotiated `contentType` sits next to `data` and `switch (result.contentType)` narrows it. + */ +export type SuccessVariant = TEntry extends { contentType: string; data: unknown } + ? TEntry extends { contentType: infer TContentType; data: infer TData } + ? { status: ToStatusNumber; data: TData; error: undefined; contentType: TContentType; request: TRequest; response: TResponse } + : never + : { status: ToStatusNumber; data: TEntry; error: undefined; contentType: string | undefined; request: TRequest; response: TResponse } + +/** + * One result variant for a single documented status, keyed by the numeric `status` so a `switch (result.status)` narrows `data` or `error`. + */ +export type ResultByStatus = TStatus extends SuccessStatusCode + ? SuccessVariant + : { + status: ToStatusNumber + data: undefined + error: DataOf + contentType: string | undefined + request: TRequest + response: TResponse + } + +/** + * The union of every documented status' result variant. + */ +export type ResultUnion = { + [TStatus in keyof TResponses]: ResultByStatus +}[keyof TResponses] + +/** + * The union of just the success (2xx) status variants, selected by status code so an untyped error payload can never widen `data`. + */ +export type SuccessResultUnion = { + [TStatus in Extract]: ResultByStatus +}[Extract] + +/** + * The shape every generated function returns, discriminated by the top-level `status`, narrowing to the 2xx variants under `throwOnError` and to every documented status without it. + */ +export type RequestResult = ThrowOnError extends true + ? [SuccessResultUnion] extends [never] + ? { + status: number + data: SuccessOf + error: undefined + contentType: string | undefined + request: TRequest + response: TResponse + } + : SuccessResultUnion + : [ResultUnion] extends [never] + ? { status: number; data: undefined; error: undefined; contentType: string | undefined; request: TRequest; response: TResponse } + : ResultUnion + +/** + * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. + */ +export type DataShape = { body?: unknown; cookies?: unknown; headers?: unknown; path?: unknown; query?: unknown } + +export type RequestCredentials = 'omit' | 'same-origin' | 'include' +export type ResponseType = 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream' + +/** + * Turns a raw response body into a parsed value, registered per media type as a codec's `deserialize` to handle formats the runtime does not decode itself. + */ +export type Deserializer = (raw: unknown, contentType: string) => T | Promise + +/** + * Serializes a request body for a single media type, registered per content type as a codec's `serialize` to encode formats the default serializer does not handle. + */ +export type ContentBodySerializer = (body: unknown, contentType?: string) => BodyInit | undefined + +/** + * A per-content-type codec registered on `codecs`, keyed by content type. `serialize` encodes the + * request body for that media type and `deserialize` decodes the response body. Either half is + * optional, so a codec can handle one direction. + */ +export type Codec = { + serialize?: ContentBodySerializer + deserialize?: Deserializer +} + +/** + * The per-call content type selection, where a bare string sets the request content type and the object form also sets the response format sent as `Accept`. + */ +export type ContentType = string | { request?: string; response?: string } + +/** + * A Standard Schema validator (zod, valibot, arktype) that parses a value before it is sent or after + * it is received. `runValidator` runs it through `validateStandardSchema`. Wired through the per-call + * `validator.request` / `validator.response` / `validator.error` hooks (`error` runs on the error body when a + * non-2xx call does not throw). + */ +export type Validator = StandardSchemaValidator + +/** + * A resolved security scheme carried on each generated call's `security` array and passed to the `auth` resolver. + */ +export type Auth = { + type: 'http' | 'apiKey' | 'oauth2' | 'openIdConnect' + scheme?: 'bearer' | 'basic' + name?: string + in?: 'header' | 'query' | 'cookie' +} + +/** + * The raw token a consumer returns for a scheme (or `user:password` for basic), or `undefined` to skip it. + */ +export type AuthToken = string | undefined + +/** + * Resolves the token for a security scheme, either a static token or a callback called per scheme until one returns a token. + */ +export type AuthResolver = AuthToken | ((auth: Auth) => AuthToken | Promise) + +/** + * Extra `fetch` init the transport spreads onto every `Request`, an escape hatch for fields the runtime does not set itself such as `cache`, `redirect`, and Next.js's `next`. + */ +export type FetchOptions = RequestInit & { next?: Record } + +/** + * The request a generated function hands to the runtime, with `body` / `headers` / `path` / `query` from the grouped options. + */ +export type RequestConfig = { + baseURL?: string + url?: string + method?: 'GET' | 'PUT' | 'PATCH' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' + path?: Record + query?: unknown + params?: unknown + cookies?: Record + body?: TBody + headers?: HeadersInit + styles?: Styles + signal?: AbortSignal + credentials?: RequestCredentials + options?: FetchOptions + contentType?: ContentType + responseType?: ResponseType + throwOnError?: boolean + client?: ClientInstance + transport?: Transport + serializer?: Serializers + codecs?: Record + validator?: { request?: Validator; response?: Validator; error?: Validator } + security?: Array + auth?: AuthResolver +} + +/** + * The grouped options object passed to every generated function: the request config minus the + * data-shaped keys and the literal `url`, plus the per-operation `Request`. + */ +export type Options = Omit< + RequestConfig, + keyof DataShape | 'url' +> & + TData & { + client?: ClientInstance + throwOnError?: ThrowOnError + } + +/** + * Client-level configuration shared by every call an instance makes, overridden by the per-call `RequestConfig`. + */ +export type ClientConfig = { + baseURL?: string + headers?: HeadersInit + credentials?: RequestCredentials + options?: FetchOptions + throwOnError?: boolean + transport?: Transport + serializer?: Serializers + codecs?: Record + auth?: AuthResolver +} + +/** + * The normalized request the transport receives, with all serialization, auth, and header work already done. + */ +export type ResolvedRequest = { + url: string + method: string + headers: Record + body?: BodyInit + signal?: AbortSignal + credentials?: RequestCredentials + options?: FetchOptions + responseType?: ResponseType +} + +/** + * What a transport returns: the parsed body plus the native request and response objects. + */ +export type TransportResult = { + data: TData + status: number + statusText: string + headers: Headers + contentType?: string + request: TRequest + response: TResponse +} + +/** + * The per-plugin send, supplied to `createClientCore` as `defaultTransport`. + */ +export type Transport = (request: ResolvedRequest) => Promise> + +/** + * The result a resolved call produces before it is cast to `RequestResult` by the generated wrapper. + */ +export type CallResult = { + status: number + data: unknown + error: unknown + contentType: string | undefined + request: TRequest + response: TResponse +} + +export type InterceptorFn = (value: T) => T | Promise + +/** + * A single interceptor channel with a transport-agnostic `use` / `eject` / `update` API. + */ +export type InterceptorStack = { + use: (fn: InterceptorFn) => number + eject: (id: number) => void + update: (id: number, fn: InterceptorFn) => void + run: (value: T) => Promise +} + +/** + * The three interceptor channels every client instance exposes. + */ +export type Interceptors = { + request: InterceptorStack + response: InterceptorStack> + error: InterceptorStack> +} + +/** + * A client instance: the callable send plus configuration, interceptors, and an isolated + * `createClient` factory bound to the same transport. + */ +export type ClientInstance = { + (config: RequestConfig): Promise> + getConfig: () => ClientConfig + setConfig: (config: ClientConfig) => ClientConfig + getUrl: (config: RequestConfig) => string + interceptors: Interceptors + createClient: (config?: ClientConfig) => ClientInstance +} + +/** + * Thrown for a non-2xx response, so a resolved call always means success. + */ +export class ResponseError extends Error { + data: TError + status: number + statusText: string + contentType: string | undefined + request: TRequest + response: TResponse + + constructor(config: { data: TError; status: number; statusText: string; contentType?: string; request: TRequest; response: TResponse }) { + super(`Request failed with status ${config.status}${config.statusText ? ` ${config.statusText}` : ''}`) + this.name = 'ResponseError' + this.data = config.data + this.status = config.status + this.statusText = config.statusText + this.contentType = config.contentType + this.request = config.request + this.response = config.response + } +} + +export type ResponseErrorConfig = ResponseError + +function serializeHeaders(headers: HeadersInit | undefined): Record { + if (!headers) return {} + const entries = Array.isArray(headers) ? headers : Object.entries(headers) + const result: Record = {} + for (const [key, value] of entries) { + if (value === undefined || value === null) continue + result[key] = typeof value === 'string' ? value : typeof value === 'object' ? JSON.stringify(value) : String(value) + } + return result +} + +function mergeHeaders(...sources: Array): Record { + return Object.assign({}, ...sources.map(serializeHeaders)) +} + +function getHeader(headers: Record, name: string): string | undefined { + const key = Object.keys(headers).find((k) => k.toLowerCase() === name.toLowerCase()) + return key ? headers[key] : undefined +} + +function hasHeader(headers: Record, name: string): boolean { + return Object.keys(headers).some((k) => k.toLowerCase() === name.toLowerCase()) +} + +/** + * Joins the URL parts, interpolates URL-encoded `{param}` segments, and appends the serialized query, shared by the send path and `getUrl`. + */ +function serializeUrl({ + parts, + pathParams, + search, + pathSerializer = defaultPathSerializer, + pathStyles, +}: { + parts: Array + pathParams: Record + search: string + pathSerializer?: PathSerializer + pathStyles?: Record +}): string { + const path = parts + .filter(Boolean) + .join('') + .replace(/\{([^{}]+)\}/g, (_, key: string) => pathSerializer({ name: key, value: pathParams[key], options: pathStyles?.[key] })) + return path + (search ? `?${search}` : '') +} + +/** + * Creates a transport-agnostic interceptor channel that runs interceptors in registration order. + */ +export function createInterceptorStack(): InterceptorStack { + let entries: Array<{ id: number; fn: InterceptorFn }> = [] + let counter = 0 + return { + use(fn) { + const id = ++counter + entries.push({ id, fn }) + return id + }, + eject(id) { + entries = entries.filter((entry) => entry.id !== id) + }, + update(id, fn) { + const entry = entries.find((item) => item.id === id) + if (entry) entry.fn = fn + }, + async run(value) { + let result = value + for (const entry of entries) { + result = await entry.fn(result) + } + return result + }, + } +} + +/** + * Walks the per-operation security in order and places the first resolved token on the request, mutating `headers` / `query` in place. + */ +export async function resolveAuth(params: { + security: Array | undefined + auth: AuthResolver | undefined + headers: Record + query: Record +}): Promise { + const { security, auth, headers, query } = params + if (!security?.length || auth === undefined) return + + for (const scheme of security) { + const token = typeof auth === 'function' ? await auth(scheme) : auth + if (token === undefined) continue + + if (scheme.type === 'apiKey') { + const name = scheme.name ?? 'Authorization' + if (scheme.in === 'query') { + if (query[name] === undefined) query[name] = token + } else if (scheme.in === 'cookie') { + headers.Cookie = [headers.Cookie, `${name}=${token}`].filter(Boolean).join('; ') + } else if (!hasHeader(headers, name)) { + headers[name] = token + } + } else if (!hasHeader(headers, 'Authorization')) { + headers.Authorization = scheme.scheme === 'basic' ? `Basic ${btoa(token)}` : `Bearer ${token}` + } + return + } +} + +async function runValidator(validator: Validator | undefined, value: T): Promise { + if (!validator) return value + return validateStandardSchema(validator, value) +} + +/** + * The base media type of a `Content-Type` value, lowercased and stripped of any `; charset=...` parameters. + */ +function baseContentType(value: string | null | undefined): string | undefined { + if (!value) return undefined + return value.split(';')[0]!.trim().toLowerCase() || undefined +} + +/** + * Reads the negotiated response content type from the response headers as a base media type. + */ +function getResponseContentType(headers: Headers | Record | undefined): string | undefined { + if (!headers) return undefined + const value = headers instanceof Headers ? headers.get('Content-Type') : (headers['Content-Type'] ?? headers['content-type']) + return baseContentType(value) +} + +/** + * Normalizes the `contentType` option to its `{ request, response }` form, treating a bare string as the request content type. + */ +function resolveContentType(contentType: ContentType | undefined): { request?: string; response?: string } { + if (typeof contentType === 'string') return { request: contentType } + return contentType ?? {} +} + +/** + * The per-concern serializers for a call, the per-call serializer winning over the client's and + * falling back to the defaults. + */ +function resolveSerializers({ config, requestConfig }: { config: { serializer?: Serializers }; requestConfig: { serializer?: Serializers } }) { + return { + querySerializer: requestConfig.serializer?.query ?? config.serializer?.query ?? defaultQuerySerializer, + bodySerializer: requestConfig.serializer?.body ?? config.serializer?.body ?? defaultBodySerializer, + pathSerializer: requestConfig.serializer?.path ?? config.serializer?.path ?? defaultPathSerializer, + } +} + +/** + * Resolves everything a call needs before it touches the transport: merged headers with the + * negotiated content type, auth on headers or query, serialized cookies, the validated and + * serialized body, and the full URL. + */ +async function resolveRequest({ + config, + requestConfig, +}: { + config: ClientConfig + requestConfig: RequestConfig +}): Promise<{ request: ResolvedRequest; codecs: Record }> { + const { querySerializer, bodySerializer, pathSerializer } = resolveSerializers({ config, requestConfig }) + const codecs = { ...config.codecs, ...requestConfig.codecs } + + const headers = mergeHeaders(config.headers, applyHeaderStyles(requestConfig.headers, requestConfig.styles?.header)) + const { request: requestContentTypeOption, response: responseContentType } = resolveContentType(requestConfig.contentType) + const requestContentType = requestContentTypeOption ?? getHeader(headers, 'content-type') + if (responseContentType && !hasHeader(headers, 'accept')) { + headers['Accept'] = responseContentType + } + + const query: Record = { ...((requestConfig.query ?? requestConfig.params) as Record | undefined) } + + await resolveAuth({ + security: requestConfig.security, + auth: requestConfig.auth ?? config.auth, + headers, + query, + }) + + if (requestConfig.cookies) { + const cookie = serializeCookies(requestConfig.cookies, requestConfig.styles?.cookie) + if (cookie) headers.Cookie = [headers.Cookie, cookie].filter(Boolean).join('; ') + } + + const validatedBody = await runValidator(requestConfig.validator?.request, requestConfig.body) + const requestContentTypeBase = baseContentType(requestContentType) + const contentCodec = requestContentTypeBase ? codecs[requestContentTypeBase] : undefined + const usesDefaultBodySerializer = !contentCodec?.serialize && bodySerializer === defaultBodySerializer + const body = contentCodec?.serialize + ? contentCodec.serialize(validatedBody, requestContentType) + : bodySerializer({ body: validatedBody, contentType: requestContentType, encoding: requestConfig.styles?.body }) + // A FormData body must keep its Content-Type unset so the runtime appends the multipart boundary. + if (body instanceof FormData) { + for (const key of Object.keys(headers)) { + if (key.toLowerCase() === 'content-type') delete headers[key] + } + } else if (requestContentTypeOption) { + headers['Content-Type'] = requestContentTypeOption + } else if (usesDefaultBodySerializer && isDefaultJsonBody(validatedBody) && !hasHeader(headers, 'content-type')) { + headers['Content-Type'] = 'application/json' + } + + const url = serializeUrl({ + parts: [requestConfig.baseURL ?? config.baseURL, requestConfig.url], + pathParams: requestConfig.path ?? {}, + search: querySerializer(query, requestConfig.styles?.query), + pathSerializer, + pathStyles: requestConfig.styles?.path, + }) + + const options = config.options || requestConfig.options ? { ...config.options, ...requestConfig.options } : undefined + + return { + codecs, + request: { + url, + method: (requestConfig.method ?? 'GET').toUpperCase(), + headers, + body, + signal: requestConfig.signal, + credentials: requestConfig.credentials, + options, + responseType: requestConfig.responseType, + }, + } +} + +/** + * Turns a transport result into the call result: decodes the body through the matching codec, + * validates it, and throws a `ResponseError` (after running the error interceptors) for a non-2xx + * response under `throwOnError`. + */ +async function settleResult({ + result, + codecs, + throwOnError, + validator, + errorInterceptors, +}: { + result: TransportResult + codecs: Record + throwOnError: boolean + validator: { response?: Validator; error?: Validator } | undefined + errorInterceptors: InterceptorStack> +}): Promise> { + const isSuccess = result.status >= 200 && result.status < 300 + const contentType = result.contentType ?? getResponseContentType(result.headers) + let decoded = result.data + if (contentType) { + const codec = codecs[contentType] + if (codec?.deserialize) decoded = await codec.deserialize(result.data, contentType) + } + + if (isSuccess) { + const data = await runValidator(validator?.response, decoded) + return { status: result.status, data, error: undefined, contentType, request: result.request, response: result.response } + } + + const error = await runValidator(validator?.error, decoded) + if (throwOnError) { + const responseError = new ResponseError({ + data: error, + status: result.status, + statusText: result.statusText, + contentType, + request: result.request, + response: result.response, + }) + await errorInterceptors.run(responseError) + throw responseError + } + return { status: result.status, data: undefined, error, contentType, request: result.request, response: result.response } +} + +/** + * Builds the shared client core bound to a transport, exported by each plugin as `client` plus a `createClient` factory. + */ +export function createClientCore( + options: { defaultTransport: Transport } & ClientConfig, +): ClientInstance { + const { defaultTransport, ...initialConfig } = options + let config: ClientConfig = { ...initialConfig } + + const interceptors: Interceptors = { + request: createInterceptorStack(), + response: createInterceptorStack>(), + error: createInterceptorStack>(), + } + + const client = (async (requestConfig: RequestConfig): Promise> => { + const transport = requestConfig.transport ?? config.transport ?? defaultTransport + const { request, codecs } = await resolveRequest({ config, requestConfig }) + + const resolvedRequest = await interceptors.request.run(request) + const result = await interceptors.response.run(await transport(resolvedRequest)) + + return settleResult({ + result, + codecs, + throwOnError: requestConfig.throwOnError ?? config.throwOnError ?? true, + validator: requestConfig.validator, + errorInterceptors: interceptors.error, + }) + }) as ClientInstance + + client.getConfig = () => config + client.setConfig = (next) => { + config = { ...config, ...next, headers: { ...serializeHeaders(config.headers), ...serializeHeaders(next.headers) } } + return config + } + client.getUrl = (requestConfig) => { + const { querySerializer, pathSerializer } = resolveSerializers({ config, requestConfig }) + const query: Record = { ...((requestConfig.query ?? requestConfig.params) as Record | undefined) } + return serializeUrl({ + parts: [requestConfig.baseURL ?? config.baseURL, requestConfig.url], + pathParams: requestConfig.path ?? {}, + search: querySerializer(query, requestConfig.styles?.query), + pathSerializer, + pathStyles: requestConfig.styles?.path, + }) + } + client.interceptors = interceptors + client.createClient = (next) => createClientCore({ defaultTransport, ...config, ...next }) + + return client +} + +/** + * Picks a `responseType` from a `Content-Type` header, or `undefined` when it is not recognized. + */ +function detectResponseType(contentType: string | null): ResponseType | undefined { + if (!contentType) return undefined + if (contentType.includes('text/event-stream')) return 'stream' + if (contentType.includes('application/json') || contentType.includes('text/json')) return 'json' + if (contentType.includes('text/')) return 'text' + if (contentType.includes('image/') || contentType.includes('application/octet-stream')) return 'blob' + return undefined +} + +/** + * Parses a `fetch` response body using the `responseType` (explicit or detected from the `Content-Type`), falling back to JSON-then-text. + */ +async function parseResponse(response: Response, responseType?: ResponseType): Promise { + if (response.status === 204 || response.status === 205 || response.status === 304 || !response.body) { + return undefined + } + + switch (responseType ?? detectResponseType(response.headers.get('Content-Type'))) { + case 'text': + case 'document': + return response.text() + case 'blob': + return response.blob() + case 'arraybuffer': + return response.arrayBuffer() + case 'stream': + return response.body ?? undefined + case 'json': { + // An empty body with a JSON content-type would make response.json() throw, so treat it as no data. + const body = await response.text() + return body ? JSON.parse(body) : undefined + } + } + + const text = await response.text() + if (!text) return undefined + try { + return JSON.parse(text) + } catch { + return text + } +} + +/** + * The default transport that sends the resolved request through `globalThis.fetch` and returns the parsed body with the native request and response. + */ +const defaultTransport: Transport = async (request: ResolvedRequest): Promise => { + const init: RequestInit = { + ...request.options, // cache, mode, redirect, keepalive, duplex, next, … + method: request.method, + headers: request.headers, + body: request.body, + signal: request.signal, + } + if (request.credentials) init.credentials = request.credentials + + const nativeRequest = new Request(request.url, init) + const response = await globalThis.fetch(nativeRequest) + const data = await parseResponse(response, request.responseType) + + return { + data, + status: response.status, + statusText: response.statusText, + headers: response.headers, + contentType: getResponseContentType(response.headers), + request: nativeRequest, + response, + } +} + +/** + * One decoded Server-Sent Event, with `data` parsed as JSON when valid and kept as the raw string otherwise. + */ +export type ServerSentEvent = { + data: TData + event?: string + id?: string + retry?: number +} + +async function* readBytes(stream: ReadableStream | AsyncIterable): AsyncGenerator { + if (!('getReader' in stream)) { + yield* stream + return + } + + const reader = stream.getReader() + try { + while (true) { + const { done, value } = await reader.read() + if (done) return + yield value + } + } finally { + await reader.cancel().catch(() => {}) + } +} + +function parseEvent(raw: string): ServerSentEvent | undefined { + const data: Array = [] + const event: ServerSentEvent = { data: undefined as TData } + let seen = false + + for (const line of raw.split('\n')) { + if (!line || line.startsWith(':')) continue + seen = true + const index = line.indexOf(':') + const field = index === -1 ? line : line.slice(0, index) + const value = index === -1 ? '' : line.slice(index + 1).replace(/^ /, '') + if (field === 'data') data.push(value) + else if (field === 'event') event.event = value + else if (field === 'id') event.id = value + else if (field === 'retry' && Number.isFinite(Number(value))) event.retry = Number(value) + } + + if (!seen) return undefined + + if (data.length) { + const joined = data.join('\n') + try { + event.data = JSON.parse(joined) as TData + } catch { + event.data = joined as TData + } + } + return event +} + +/** + * Parses a `text/event-stream` body into typed Server-Sent Events, consumed with `for await` and stopped early by breaking the loop. + */ +export async function* parseEventStream( + stream: ReadableStream | AsyncIterable, +): AsyncGenerator> { + const decoder = new TextDecoder() + const normalize = (text: string) => text.replace(/\r\n|\r/g, '\n') + let buffer = '' + + for await (const chunk of readBytes(stream)) { + const blocks = normalize(buffer + decoder.decode(chunk, { stream: true })).split('\n\n') + buffer = blocks.pop() ?? '' + for (const block of blocks) { + const event = parseEvent(block) + if (event) yield event + } + } + + const event = parseEvent(normalize(buffer + decoder.decode())) + if (event) yield event +} + +/** + * The resolved shape returned by a generated `text/event-stream` operation: the typed event + * `stream` plus the native `response`. + */ +export type EventStreamResult = { + stream: AsyncGenerator> + response: TResponse +} + +/** + * Wraps a transport result whose `data` is a streaming body into an `EventStreamResult`, exposing + * the parsed events as a typed async iterator. Generated SSE operations call this. + */ +export async function toEventStream(result: Promise<{ data: unknown; response: Response }>): Promise> { + const { data, response } = await result + return { + response, + stream: parseEventStream(data as ReadableStream | AsyncIterable), + } +} + +export const client = createClientCore({ defaultTransport }) + +export const createClient = (config?: Parameters[0]) => client.createClient(config) \ No newline at end of file diff --git a/src/clients/events/.kubb/serializers.ts b/src/clients/events/.kubb/serializers.ts new file mode 100644 index 0000000..41e97d5 --- /dev/null +++ b/src/clients/events/.kubb/serializers.ts @@ -0,0 +1,428 @@ +export type HeaderValue = string | number | boolean | null | undefined | object +export type HeadersInit = Array<[string, HeaderValue]> | Record + +/** + * The OpenAPI query-parameter serialization style. `form` is the default; `spaceDelimited` and + * `pipeDelimited` join arrays with a space or pipe, and `deepObject` renders objects as + * `key[prop]=value`. + */ +export type QueryStyle = 'form' | 'spaceDelimited' | 'pipeDelimited' | 'deepObject' + +/** + * The serialization metadata shared by the styled parameter locations: the OpenAPI `style` (typed per + * location through `TStyle`), `explode`, and `allowReserved` (keeps RFC 3986 reserved characters + * unencoded, used by query and request bodies). + */ +export type SerializationStyle = { + style?: TStyle + explode?: boolean + allowReserved?: boolean +} + +/** + * The per-parameter query serialization metadata carried by the generated request. + */ +export type QueryParamStyle = SerializationStyle + +/** + * Serializes the query object into a search string. The optional second argument carries the + * per-parameter OpenAPI `style` / `explode` / `allowReserved` metadata; without it arrays explode + * into repeated keys and nested objects use the `deepObject` style. + */ +export type QuerySerializer = (params: Record, options?: Record) => string + +/** + * The per-parameter cookie serialization metadata carried by the generated request. Cookies use the + * OpenAPI `form` style, so only `explode` is configurable. + */ +export type CookieParamStyle = { + explode?: boolean +} + +/** + * The per-parameter header serialization metadata carried by the generated request. Headers use the + * OpenAPI `simple` style, so only `explode` is configurable. + * + * @example + * ```ts + * // styles.header: { 'X-Ids': { explode: false } }, header [3, 4] -> 'X-Ids: 3,4' + * // styles.header: { 'X-Filter': { explode: true } }, header { role: 'admin' } -> 'X-Filter: role=admin' + * ``` + */ +export type HeaderParamStyle = { + explode?: boolean +} + +/** + * The per-property `encoding` metadata for an `application/x-www-form-urlencoded` or + * `multipart/form-data` request body. `contentType` overrides the part's media type; `style` / + * `explode` / `allowReserved` follow the OpenAPI query rules for urlencoded bodies. + */ +export type BodyEncoding = SerializationStyle & { + contentType?: string +} + +/** + * Serializes the request body. JSON by default; `FormData`, `URLSearchParams`, `Blob`, + * `ArrayBuffer`, and string bodies pass through untouched. The optional `encoding` argument carries + * the per-property OpenAPI `encoding` metadata for form bodies. + */ +export type BodySerializer = (args: { body: unknown; contentType?: string; encoding?: Record }) => BodyInit | undefined + +/** + * The OpenAPI path-parameter serialization style. `simple` is the default and emits the bare value; + * `label` prefixes a `.` and `matrix` prefixes a `;name=` segment. + */ +export type PathStyle = 'simple' | 'label' | 'matrix' + +/** + * The per-parameter serialization metadata carried by the generated request. `style` selects the + * OpenAPI style and `explode` controls how arrays and objects expand. + */ +export type PathParamStyle = SerializationStyle + +/** + * Serializes a single path parameter for interpolation into the URL, honoring the OpenAPI `style` / + * `explode` passed as `options`. Defaults to `simple` style with `explode: false`: primitives are + * URL-encoded, arrays join their members with commas, and objects flatten to `key,value` pairs. + */ +export type PathSerializer = (args: { name: string; value: unknown; options?: PathParamStyle }) => string + +/** + * The per-concern serializers, grouped so they can be set in one place and overridden per client or + * per call. Each field falls back to the matching `default*Serializer` when omitted. + */ +export type Serializers = { + query?: QuerySerializer + body?: BodySerializer + path?: PathSerializer +} + +/** + * The per-parameter OpenAPI `style` / `explode` metadata a generated request carries, grouped by + * location and keyed by parameter name. Mirrors the `serializer` grouping and feeds the default + * serializers; `body` carries the form `encoding` for a urlencoded or multipart body. + */ +export type Styles = { + path?: Record + query?: Record + header?: Record + cookie?: Record + body?: Record +} + +function isFormBody(body: unknown): body is BodyInit { + return ( + body instanceof FormData || + body instanceof URLSearchParams || + body instanceof Blob || + body instanceof ArrayBuffer || + ArrayBuffer.isView(body) || + typeof body === 'string' + ) +} + +export function isDefaultJsonBody(body: unknown): boolean { + return body !== undefined && body !== null && !isFormBody(body) +} + +function appendFormDataValue({ formData, key, value, contentType }: { formData: FormData; key: string; value: unknown; contentType?: string }): void { + if (value === undefined || value === null) return + if (value instanceof Blob) formData.append(key, value) + else if (typeof value === 'object' && !(value instanceof Date)) { + const json = JSON.stringify(value) + // A part's media type can only be set by wrapping the value in a typed Blob. + formData.append(key, contentType ? new Blob([json], { type: contentType }) : json) + } else formData.append(key, toValue(value)) +} + +/** + * Default body serializer: passes binary/form bodies through and JSON-serializes everything else. + * For `multipart/form-data` plain objects become `FormData` and for + * `application/x-www-form-urlencoded` they become `URLSearchParams`. When `encoding` is supplied each + * urlencoded property follows its OpenAPI `style` / `explode` / `allowReserved`. + * + * @example + * ```ts + * defaultBodySerializer({ body: { name: 'odie' } }) // '{"name":"odie"}' + * defaultBodySerializer({ body: { field: 'x' }, contentType: 'multipart/form-data' }) // FormData + * defaultBodySerializer({ body: { tags: ['a', 'b'] }, contentType: 'application/x-www-form-urlencoded', encoding: { tags: { explode: false } } }) // 'tags=a,b' + * defaultBodySerializer({ body: { meta: { a: 1 } }, contentType: 'multipart/form-data', encoding: { meta: { contentType: 'application/json' } } }) // FormData with a typed Blob part + * ``` + */ +export const defaultBodySerializer: BodySerializer = ({ body, contentType, encoding }) => { + if (body === undefined || body === null) return undefined + if (isFormBody(body)) return body as BodyInit + if (contentType?.includes('multipart/form-data')) { + const formData = new FormData() + for (const [key, value] of Object.entries(body as Record)) { + const partContentType = encoding?.[key]?.contentType + if (Array.isArray(value)) for (const item of value) appendFormDataValue({ formData, key, value: item, contentType: partContentType }) + else appendFormDataValue({ formData, key, value, contentType: partContentType }) + } + return formData + } + if (contentType?.includes('application/x-www-form-urlencoded')) { + if (encoding) return serializeUrlencodedBody(body as Record, encoding) + return new URLSearchParams(body as Record) + } + return JSON.stringify(body) +} + +function serializeUrlencodedBody(body: Record, encoding: Record): string { + const parts: Array = [] + for (const [key, value] of Object.entries(body)) { + const propertyEncoding = encoding[key] + parts.push(...(propertyEncoding ? serializeStyledQueryParam({ key, value, options: propertyEncoding }) : serializeDefaultQueryParam(key, value))) + } + return parts.join('&') +} + +function serializeCookie({ name, value, explode }: { name: string; value: unknown; explode: boolean }): string { + if (Array.isArray(value)) { + const items = value.filter(notNullish).map((item) => encodeURIComponent(toValue(item))) + return explode ? items.map((item) => `${name}=${item}`).join('; ') : `${name}=${items.join(',')}` + } + if (isRecord(value)) { + const entries = Object.entries(value).filter(([, item]) => notNullish(item)) + if (explode) return entries.map(([key, item]) => `${key}=${encodeURIComponent(toValue(item))}`).join('; ') + return `${name}=${entries + .flatMap(([key, item]) => [key, item]) + .map((item) => encodeURIComponent(toValue(item))) + .join(',')}` + } + return `${name}=${encodeURIComponent(toValue(value))}` +} + +/** + * Serializes cookie parameters into a `Cookie` header value using the OpenAPI `form` style, joined + * with `; `. Values are URL-encoded and `explode` is honored per parameter. + * + * @example + * ```ts + * serializeCookies({ session: 'abc', ids: [1, 2] }) // 'session=abc; ids=1,2' + * serializeCookies({ ids: [1, 2] }, { ids: { explode: true } }) // 'ids=1; ids=2' + * ``` + */ +export function serializeCookies(cookies: Record, styles?: Record): string { + const parts: Array = [] + for (const [name, value] of Object.entries(cookies)) { + if (value === undefined || value === null) continue + parts.push(serializeCookie({ name, value, explode: styles?.[name]?.explode ?? false })) + } + return parts.join('; ') +} + +function appendQueryValue({ search, key, value }: { search: URLSearchParams; key: string; value: unknown }): void { + if (value === undefined || value === null) return + if (Array.isArray(value)) { + for (const item of value) appendQueryValue({ search, key, value: item }) + return + } + if (isRecord(value)) { + for (const [prop, propValue] of Object.entries(value)) { + appendQueryValue({ search, key: `${key}[${prop}]`, value: propValue }) + } + return + } + search.append(key, toValue(value)) +} + +const queryDelimiters: Record = { form: ',', spaceDelimited: '%20', pipeDelimited: '|', deepObject: ',' } + +function notNullish(value: unknown): boolean { + return value !== undefined && value !== null +} + +/** + * Renders a primitive parameter value as a string, serializing `Date` to ISO-8601 so dates are + * stable across path, query, cookie, and header locations rather than locale-dependent. + */ +function toValue(value: unknown): string { + return value instanceof Date ? value.toISOString() : String(value) +} + +/** + * Percent-encodes a value, keeping RFC 3986 reserved characters intact (used when `allowReserved` is set). + */ +function encodeReserved(value: unknown): string { + return encodeURI(toValue(value)) +} + +/** + * Percent-encodes a value, escaping reserved characters (the default query/path encoder). + */ +function encodeComponent(value: unknown): string { + return encodeURIComponent(toValue(value)) +} + +/** + * Whether a value should expand into bracketed/keyed parts. Arrays and `Date` are excluded so they + * are serialized as a unit (a `Date` becomes an ISO string, not its enumerable own properties). + */ +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) && !(value instanceof Date) +} + +/** + * Expands an object or array into `deepObject` query parts, recursing into nested values so + * `{ a: { b: { c: 1 } } }` becomes `a[b][c]=1`. Primitives terminate the recursion. + */ +function serializeDeepObject({ key, value, encode }: { key: string; value: unknown; encode: (value: unknown) => string }): Array { + if (value === undefined || value === null) return [] + if (Array.isArray(value)) return value.flatMap((item, index) => serializeDeepObject({ key: `${key}[${index}]`, value: item, encode })) + if (isRecord(value)) { + return Object.entries(value) + .filter(([, item]) => notNullish(item)) + .flatMap(([prop, item]) => serializeDeepObject({ key: `${key}[${prop}]`, value: item, encode })) + } + return [`${encode(key)}=${encode(value)}`] +} + +function serializeStyledQueryArray({ + key, + value, + options, + encode, +}: { + key: string + value: Array + options: QueryParamStyle + encode: (value: unknown) => string +}): Array { + const items = value.filter(notNullish) + if (options.explode ?? true) return items.map((item) => `${encode(key)}=${encode(item)}`) + return [`${encode(key)}=${items.map(encode).join(queryDelimiters[options.style ?? 'form'])}`] +} + +function serializeStyledQueryObject({ + key, + value, + options, + encode, +}: { + key: string + value: Record + options: QueryParamStyle + encode: (value: unknown) => string +}): Array { + if ((options.style ?? 'form') === 'deepObject') return serializeDeepObject({ key, value, encode }) + const entries = Object.entries(value).filter(([, item]) => notNullish(item)) + if (options.explode ?? true) return entries.map(([prop, item]) => `${encode(prop)}=${encode(item)}`) + return [ + `${encode(key)}=${entries + .flatMap(([prop, item]) => [prop, item]) + .map(encode) + .join(',')}`, + ] +} + +function serializeStyledQueryParam({ key, value, options }: { key: string; value: unknown; options: QueryParamStyle }): Array { + if (value === undefined || value === null) return [] + const encode = options.allowReserved ? encodeReserved : encodeComponent + if (Array.isArray(value)) return serializeStyledQueryArray({ key, value, options, encode }) + if (isRecord(value)) return serializeStyledQueryObject({ key, value, options, encode }) + return [`${encode(key)}=${encode(value)}`] +} + +function serializeDefaultQueryParam(key: string, value: unknown): Array { + const search = new URLSearchParams() + appendQueryValue({ search, key, value }) + const result = search.toString() + return result ? [result] : [] +} + +/** + * Default query serializer. Members with `options` metadata follow their OpenAPI `style` / `explode` + * / `allowReserved`. Members without it keep the defaults: arrays explode into repeated keys and + * nested objects use the `deepObject` style (`key[prop]=value`). + * + * @example + * ```ts + * defaultQuerySerializer({ id: [3, 4, 5] }) // 'id=3&id=4&id=5' + * defaultQuerySerializer({ id: [3, 4, 5] }, { id: { style: 'form', explode: false } }) // 'id=3,4,5' + * defaultQuerySerializer({ id: [3, 4, 5] }, { id: { style: 'spaceDelimited', explode: false } }) // 'id=3%204%205' + * defaultQuerySerializer({ id: [3, 4, 5] }, { id: { style: 'pipeDelimited', explode: false } }) // 'id=3|4|5' + * defaultQuerySerializer({ a: { b: 1 } }, { a: { style: 'deepObject' } }) // 'a%5Bb%5D=1' + * defaultQuerySerializer({ a: { b: { c: 1 } } }, { a: { style: 'deepObject' } }) // 'a%5Bb%5D%5Bc%5D=1' + * ``` + */ +export const defaultQuerySerializer: QuerySerializer = (params, options) => { + const parts: Array = [] + for (const [key, value] of Object.entries(params)) { + const paramOptions = options?.[key] + parts.push(...(paramOptions ? serializeStyledQueryParam({ key, value, options: paramOptions }) : serializeDefaultQueryParam(key, value))) + } + return parts.join('&') +} + +function serializePathPrimitive({ name, value, style }: { name: string; value: unknown; style: PathStyle }): string { + const encoded = encodeComponent(value) + if (style === 'label') return `.${encoded}` + if (style === 'matrix') return `;${name}=${encoded}` + return encoded +} + +function serializePathArray({ name, value, style, explode }: { name: string; value: Array; style: PathStyle; explode: boolean }): string { + const items = value.map(encodeComponent) + if (style === 'label') return `.${items.join(explode ? '.' : ',')}` + if (style === 'matrix') return explode ? items.map((item) => `;${name}=${item}`).join('') : `;${name}=${items.join(',')}` + return items.join(',') +} + +function serializePathObject({ name, value, style, explode }: { name: string; value: Record; style: PathStyle; explode: boolean }): string { + const members = Object.entries(value).map(([key, item]) => + explode ? `${encodeComponent(key)}=${encodeComponent(item)}` : `${encodeComponent(key)},${encodeComponent(item)}`, + ) + if (style === 'label') return `.${members.join(explode ? '.' : ',')}` + if (style === 'matrix') return explode ? members.map((member) => `;${member}`).join('') : `;${name}=${members.join(',')}` + return members.join(',') +} + +/** + * Default path serializer honoring the OpenAPI `style` / `explode` metadata. Without metadata it + * falls back to `simple` style with `explode: false`. Replaces the previous `String(value)` + * interpolation, which emitted `[object Object]` for object path params. + * + * @example + * ```ts + * defaultPathSerializer({ name: 'id', value: [3, 4, 5] }) // '3,4,5' + * defaultPathSerializer({ name: 'id', value: [3, 4, 5], options: { style: 'label', explode: true } }) // '.3.4.5' + * defaultPathSerializer({ name: 'id', value: [3, 4, 5], options: { style: 'matrix', explode: true } }) // ';id=3;id=4;id=5' + * defaultPathSerializer({ name: 'pt', value: { x: 1, y: 2 } }) // 'x,1,y,2' + * ``` + */ +export const defaultPathSerializer: PathSerializer = ({ name, value, options }) => { + if (value === undefined || value === null) return '' + const style = options?.style ?? 'simple' + const explode = options?.explode ?? false + if (Array.isArray(value)) return serializePathArray({ name, value, style, explode }) + if (isRecord(value)) return serializePathObject({ name, value, style, explode }) + return serializePathPrimitive({ name, value, style }) +} + +function serializeHeaderValue(value: unknown, explode: boolean): string { + if (Array.isArray(value)) return value.filter(notNullish).map(toValue).join(',') + if (!isRecord(value)) return toValue(value) + const entries = Object.entries(value).filter(([, item]) => notNullish(item)) + if (explode) return entries.map(([key, item]) => `${key}=${toValue(item)}`).join(',') + return entries + .flatMap(([key, item]) => [key, item]) + .map(toValue) + .join(',') +} + +/** + * Serializes array and object header parameters with the OpenAPI `simple` style before they are + * merged. Header values are not URL-encoded. Primitive values and headers without metadata pass + * through untouched. + */ +export function applyHeaderStyles(headers: HeadersInit | undefined, styles: Record | undefined): HeadersInit | undefined { + if (!headers || !styles) return headers + const entries = Array.isArray(headers) ? headers : Object.entries(headers) + return entries.map(([key, value]) => { + const style = styles[key] + if (!style || value === undefined || value === null || typeof value !== 'object') return [key, value] as [string, HeaderValue] + return [key, serializeHeaderValue(value, style.explode ?? false)] as [string, HeaderValue] + }) +} \ No newline at end of file diff --git a/src/clients/events/.kubb/standardSchema.ts b/src/clients/events/.kubb/standardSchema.ts new file mode 100644 index 0000000..7e67603 --- /dev/null +++ b/src/clients/events/.kubb/standardSchema.ts @@ -0,0 +1,54 @@ +/** + * A Standard Schema-compatible validator: a minimal duck-type covering Zod v3/v4, valibot, and + * arktype schemas. Only the `~standard.validate` method is required at runtime. + */ +export type StandardSchemaValidator = { + readonly '~standard': { + validate(value: unknown): StandardSchemaResult | Promise> + } +} + +/** + * The two possible outcomes of a Standard Schema `validate` call. A successful result carries + * `value`; a failed result carries `issues`. + */ +export type StandardSchemaResult = { readonly value: TOutput; readonly issues?: undefined } | { readonly issues: ReadonlyArray } + +/** + * One validation issue from a Standard Schema `validate` call. + */ +export type StandardSchemaIssue = { + readonly message?: string + readonly path?: ReadonlyArray +} + +/** + * Thrown by `validateStandardSchema` when validation fails. Carries the raw `issues` array from + * the schema's `validate` result so callers receive a uniform error shape regardless of which + * schema library is in use. + */ +export class ParseError extends Error { + readonly issues: ReadonlyArray + + constructor({ issues, message }: { issues: ReadonlyArray; message?: string }) { + super(message ?? 'Validation failed') + this.name = 'ParseError' + this.issues = issues + } +} + +/** + * Validates `value` against a Standard Schema-compatible `schema`. Returns the parsed output on + * success; throws `ParseError` with the schema's `issues` on failure. Handles both sync and async + * `validate` implementations transparently. + * + * @example + * const pet = await validateStandardSchema(PetSchema, rawData) + */ +export async function validateStandardSchema(schema: StandardSchemaValidator, value: unknown): Promise { + const result = await schema['~standard'].validate(value) + if (result.issues) { + throw new ParseError({ issues: result.issues }) + } + return result.value as TOutput +} \ No newline at end of file diff --git a/src/clients/events/client.ts b/src/clients/events/client.ts new file mode 100644 index 0000000..a68f330 --- /dev/null +++ b/src/clients/events/client.ts @@ -0,0 +1,26 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +import type { Options, RequestResult } from './.kubb/client.ts' +import type { GetApiOpenapiJsonOptions, GetApiOpenapiJsonResponses, GetApiUpcomingOptions, GetApiUpcomingResponses } from './types.ts' +import { client } from './.kubb/client.ts' + +/** + * {@link /api/upcoming} + */ +export function getApiUpcoming(options: Options = {}): Promise> { + const { client: request = client, ...config } = options + + return request({ method: 'GET', url: '/api/upcoming', ...config }) as Promise> +} + +/** + * {@link /api/openapi.json} + */ +export function getApiOpenapiJson(options: Options = {}): Promise> { + const { client: request = client, ...config } = options + + return request({ method: 'GET', url: '/api/openapi.json', ...config }) as Promise> +} \ No newline at end of file diff --git a/src/clients/events/types.ts b/src/clients/events/types.ts new file mode 100644 index 0000000..f99c042 --- /dev/null +++ b/src/clients/events/types.ts @@ -0,0 +1,113 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +/** + * @type array +*/ +export type GetApiUpcomingStatus200 = { + /** + * @type string + */ + name: string; + /** + * @type string + */ + slug: string; + /** + * @type string + */ + location: string; + /** + * @type string | undefined + */ + lat?: string; + /** + * @type string | undefined + */ + lon?: string; + /** + * @description + * Format: `date-time` + * @type string + */ + startDate: string; + /** + * @description + * Format: `date-time` + * @type string + */ + endDate: string; + /** + * @type object | undefined + */ + series?: { + /** + * @type integer | undefined + */ + id?: number; + /** + * @type string | undefined + */ + name?: string; + }; + /** + * @type object | undefined + */ + image?: { + /** + * @type string | undefined + */ + path?: string; + }; +}[]; + +/** + * @type object +*/ +export type GetApiUpcomingOptions = { + body?: never; + path?: never; + query?: never; + headers?: never; +}; + +/** + * @type object +*/ +export type GetApiUpcomingResponses = { + "200": GetApiUpcomingStatus200; +}; + +/** + * @description Union of all possible responses +*/ +export type GetApiUpcomingResponse = GetApiUpcomingStatus200; + +/** + * @type any +*/ +export type GetApiOpenapiJsonStatus200 = any; + +/** + * @type object +*/ +export type GetApiOpenapiJsonOptions = { + body?: never; + path?: never; + query?: never; + headers?: never; +}; + +/** + * @type object +*/ +export type GetApiOpenapiJsonResponses = { + "200": GetApiOpenapiJsonStatus200; +}; + +/** + * @description Union of all possible responses +*/ +export type GetApiOpenapiJsonResponse = GetApiOpenapiJsonStatus200; \ No newline at end of file diff --git a/src/commands/events.ts b/src/commands/events.ts index 80a7fc6..7a98f20 100644 --- a/src/commands/events.ts +++ b/src/commands/events.ts @@ -19,5 +19,5 @@ export async function handleEventsCommand({ // TODO: make this formatting better function formatEvents(events: Event[]): string { - return events.map((e) => e.title).join("\n"); + return events.map((e) => e.name).join("\n"); } diff --git a/src/lib/events.ts b/src/lib/events.ts index c617ff8..ad266fb 100644 --- a/src/lib/events.ts +++ b/src/lib/events.ts @@ -1,11 +1,7 @@ -// TODO: Get this schema from the events repo (https://tangled.org/devict.org/events) -// https://events.devict.org/api/openapi.json -// Write a script to generate a client from the spec above, using the simplest possible -// tools. +import { getApiUpcoming } from "../clients/events/client.ts"; +import { type GetApiUpcomingStatus200 } from "../clients/events/types.ts"; -export type Event = { - title: string; -}; +export type Event = GetApiUpcomingStatus200[number]; export interface EventsClient { getUpcomingEvents(): Promise; @@ -14,7 +10,8 @@ export interface EventsClient { export function makeEventsClient(): EventsClient { return { async getUpcomingEvents(): Promise { - return [{ title: "sup" }]; + const resp = await getApiUpcoming(); + return resp.data; }, }; }