From 2eda5a2c459203e19bf7b38cc83d9485bd85e775 Mon Sep 17 00:00:00 2001 From: DevScoopee Date: Sat, 17 Oct 2026 06:26:27 -0400 Subject: [PATCH] A generated TypeScript client now ships in-repo at clients/typescript, derived from openapi.yaml and enforced in CI. Types are never hand-written: schema.d.ts is produced by openapi-typescript, and openapi-fetch provides a typed createLensClient. CI regenerates the client on every run and fails if the committed output diverges from the spec, then typechecks it. This session repaired the two files that had been corrupted, added the documented usage example and README section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The client is generated from openapi.yaml into clients/typescript/src/schema.d.ts and wrapped by a typed createLensClient in client.ts, with index.ts exposing the public surface. CI runs npm run client:gen, then git diff --exit-code -- clients/typescript to fail on drift, then npm run client:typecheck; package.json gained those two scripts plus the openapi-typescript and openapi-fetch dev dependencies, and openapi.yaml had its duplicate operation id renamed (getPriceHistory → getPriceSnapshots). The working-tree README.md and clients/typescript/src/example.ts had been reduced to all-zero bytes, so I restored the README from git and added a TypeScript Client section documenting regeneration and a short typed example, recreated example.ts as a compiling, spec-derived usage example, and added a minor changeset. Closes #276 --- .changeset/typed-typescript-client.md | 12 + .github/workflows/ci.yml | 6 + README.md | 37 ++ clients/typescript/src/client.ts | 23 + clients/typescript/src/example.ts | 50 +++ clients/typescript/src/index.ts | 3 + clients/typescript/src/schema.d.ts | 587 ++++++++++++++++++++++++++ clients/typescript/tsconfig.json | 14 + openapi.yaml | 2 +- package-lock.json | 289 ++++++++++++- package.json | 4 + 11 files changed, 1020 insertions(+), 7 deletions(-) create mode 100644 .changeset/typed-typescript-client.md create mode 100644 clients/typescript/src/client.ts create mode 100644 clients/typescript/src/example.ts create mode 100644 clients/typescript/src/index.ts create mode 100644 clients/typescript/src/schema.d.ts create mode 100644 clients/typescript/tsconfig.json diff --git a/.changeset/typed-typescript-client.md b/.changeset/typed-typescript-client.md new file mode 100644 index 00000000..3007d92e --- /dev/null +++ b/.changeset/typed-typescript-client.md @@ -0,0 +1,12 @@ +--- +'lens': minor +--- + +Add a generated TypeScript client at `clients/typescript`. `npm run client:gen` +regenerates `clients/typescript/src/schema.d.ts` from `openapi.yaml` with +`openapi-typescript`, and `openapi-fetch` gives a typed `createLensClient`, so +every request and response type is derived from the spec and nothing is +hand-maintained. CI regenerates the client and fails if the committed copy +differs from `openapi.yaml`, then typechecks it with +`npm run client:typecheck`. `clients/typescript/src/example.ts` is a documented +usage example; the README explains how to regenerate and use the client. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a3ecf75e..9b927282 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,12 @@ jobs: cache: npm - run: npm ci || npm install - run: node node_modules/prisma/build/index.js generate + - name: Generate TypeScript client from openapi.yaml + run: npm run client:gen + - name: Fail if the committed client is out of date + run: git diff --exit-code -- clients/typescript + - name: Typecheck the generated TypeScript client + run: npm run client:typecheck - run: npx tsc --noEmit - run: npm run build - run: npm test --if-present diff --git a/README.md b/README.md index 552ad3de..3aae6a4f 100644 --- a/README.md +++ b/README.md @@ -264,6 +264,43 @@ Or interactively at [http://localhost:3002/graphiql](http://localhost:3002/graph > **Note:** the current `GATED_ROUTES` map in [`src/middleware/x402.ts`](src/middleware/x402.ts) gates `/price`, `/pools`, and `/candles` only — `/graphql` is not gated. If you intend price data from GraphQL to require the same payment as REST, extend `GATED_ROUTES` (or add a per-resolver guard). +## TypeScript Client + +A typed TypeScript client is generated from [`openapi.yaml`](openapi.yaml) with +[`openapi-typescript`](https://github.com/openapi-ts/openapi-typescript) and +[`openapi-fetch`](https://github.com/openapi-ts/openapi-typescript/tree/main/packages/openapi-fetch). +It is committed at [`clients/typescript`](clients/typescript) — **no type is +hand-written**, so a change to the spec is a change to the client. + +```bash +# Regenerate clients/typescript/src/schema.d.ts from the spec +npm run client:gen + +# Type-check the generated client +npm run client:typecheck +``` + +CI runs `npm run client:gen` and fails if the committed client differs from the +regenerated output, so the client can never drift from `openapi.yaml`. + +```typescript +import { createLensClient } from "./clients/typescript/src/index.js"; + +const client = createLensClient({ baseUrl: "http://localhost:3002" }); + +const { data, error } = await client.GET("/price/{assetA}/{assetB}", { + params: { path: { assetA: "XLM", assetB: "USDC" } }, +}); +if (error) throw new Error(`request failed: ${error}`); + +// `data` is `AggregatedPrice`, derived from the spec. +console.log(`XLM/USDC vwap1h: ${data?.vwap1h}`); +``` + +A runnable version lives in +[`clients/typescript/src/example.ts`](clients/typescript/src/example.ts); run it +against a local server with `npx tsx clients/typescript/src/example.ts`. + ## Documentation Detailed system design and data flow diagrams can be found in the [Architecture Overview](docs/architecture.md). The API specification is available in [OpenAPI 3.0 format](openapi.yaml) and is auto-published to GitHub Pages at https://miracle656.github.io/lens/openapi.json. diff --git a/clients/typescript/src/client.ts b/clients/typescript/src/client.ts new file mode 100644 index 00000000..5aedab7e --- /dev/null +++ b/clients/typescript/src/client.ts @@ -0,0 +1,23 @@ +import createClient, { type Client, type ClientOptions } from "openapi-fetch"; +import type { paths } from "./schema.js"; + +export type { paths } from "./schema.js"; + +export type LensClient = Client; + +export type LensClientOptions = Omit & { + baseUrl?: string; + fetch?: typeof globalThis.fetch; +}; + +export const DEFAULT_BASE_URL = "http://localhost:3002"; + +export function createLensClient( + options: LensClientOptions = {}, +): LensClient { + return createClient({ + baseUrl: options.baseUrl ?? DEFAULT_BASE_URL, + fetch: options.fetch, + headers: options.headers, + }); +} diff --git a/clients/typescript/src/example.ts b/clients/typescript/src/example.ts new file mode 100644 index 00000000..0f6401a6 --- /dev/null +++ b/clients/typescript/src/example.ts @@ -0,0 +1,50 @@ +/** + * Example: typed Lens client usage. + * + * Every request and response type below comes from `schema.d.ts`, which is + * generated from `openapi.yaml` by `npm run client:gen`. Nothing here is + * hand-maintained, so a spec change shows up as a type error at compile time. + * + * Run it against a local server with: + * + * npx tsx clients/typescript/src/example.ts + */ +import { createLensClient, type paths } from "./client.js"; + +/** A price response, derived from the spec — never written by hand. */ +type AggregatedPrice = paths["/price/{assetA}/{assetB}"]["get"]["responses"][200]["content"]["application/json"]; + +const client = createLensClient({ baseUrl: "http://localhost:3002" }); + +async function main(): Promise { + const status = await client.GET("/status"); + if (status.error) { + throw new Error(`GET /status failed with ${status.response.status}`); + } + console.log(`watching ${status.data.watchedPairs?.length ?? 0} pairs`); + console.log(`last indexed ledger: ${status.data.lastIndexedLedger}`); + + const price = await client.GET("/price/{assetA}/{assetB}", { + params: { path: { assetA: "XLM", assetB: "USDC" } }, + }); + if (price.error) { + throw new Error(`GET /price failed with ${price.response.status}`); + } + + const data: AggregatedPrice | undefined = price.data; + if (!data) { + throw new Error("GET /price returned no body"); + } + console.log(`XLM/USDC best route: ${data.bestRoute}`); + console.log(`XLM/USDC vwap1h: ${data.vwap1h}`); + + const snapshots = await client.GET("/prices/history", { + params: { query: { pair: "XLM/USDC", interval: "1h" } }, + }); + if (snapshots.error) { + throw new Error(`GET /prices/history failed with ${snapshots.response.status}`); + } + console.log(`${snapshots.data?.count ?? 0} snapshots returned`); +} + +void main(); diff --git a/clients/typescript/src/index.ts b/clients/typescript/src/index.ts new file mode 100644 index 00000000..e2cdef67 --- /dev/null +++ b/clients/typescript/src/index.ts @@ -0,0 +1,3 @@ +export { createLensClient, DEFAULT_BASE_URL } from "./client.js"; +export type { LensClient, LensClientOptions, paths } from "./client.js"; +export type { components, operations, $defs } from "./schema.js"; diff --git a/clients/typescript/src/schema.d.ts b/clients/typescript/src/schema.d.ts new file mode 100644 index 00000000..ec48f136 --- /dev/null +++ b/clients/typescript/src/schema.d.ts @@ -0,0 +1,587 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/status": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get API status and indexer state */ + get: operations["getStatus"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/price/{assetA}/{assetB}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get aggregated price for a pair */ + get: operations["getPrice"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/price/{assetA}/{assetB}/route": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get best swap route for a pair and amount */ + get: operations["getBestRoute"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/price/{assetA}/{assetB}/history": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get price history buckets */ + get: operations["getPriceHistory"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/pools": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get AMM liquidity pool reserves and spot prices */ + get: operations["getPools"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/candles/{assetA}/{assetB}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get OHLCV candle data */ + get: operations["getCandles"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/prices/history": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get historical 1-minute price snapshots + * @description Returns minute-resolution price snapshots for a pair over [from, to], optionally aggregated into 5m or 1h buckets. Snapshots are retained for ~30 days. Gated by x402 (matches the /price prefix). + */ + get: operations["getPriceSnapshots"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/pairs": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List all active trading pairs */ + get: operations["listPairs"]; + put?: never; + /** Add a new trading pair (Admin only) */ + post: operations["addPair"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + Asset: { + code?: string; + issuer?: string | null; + }; + AggregatedPrice: { + assetA?: string; + assetB?: string; + pairKey?: string; + price?: number; + sdexPrice?: number; + ammPrice?: number; + /** @enum {string} */ + bestRoute?: "SDEX" | "AMM" | "SPLIT" | "UNKNOWN"; + volume24h?: number; + sdexVolume24h?: number; + ammVolume24h?: number; + vwap1m?: number; + vwap5m?: number; + vwap1h?: number; + vwap24h?: number; + priceChange24h?: number; + /** Format: date-time */ + lastUpdated?: string; + sources?: number; + /** @enum {string} */ + confidence?: "high" | "medium" | "low" | "unknown"; + lastTradeAgeSeconds?: number | null; + }; + RouteInfo: { + /** @enum {string} */ + route?: "SDEX" | "AMM" | "SPLIT" | "UNKNOWN"; + sdexPrice?: number; + ammPrice?: number; + estimatedOutput?: number; + slippagePct?: number; + recommendation?: string; + }; + PriceBucket: { + /** Format: date-time */ + bucket?: string; + window?: string; + vwap?: number; + sdexVwap?: number | null; + ammVwap?: number | null; + volume?: number; + tradeCount?: number; + open?: number | null; + close?: number | null; + high?: number | null; + low?: number | null; + }; + }; + responses: { + /** @description Payment Required */ + PaymentRequired: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + x402Version?: number; + accepts?: { + scheme?: string; + price?: string; + network?: string; + payTo?: string; + }[]; + error?: string; + description?: string; + }; + }; + }; + /** @description Rate limit exceeded */ + TooManyRequests: { + headers: { + /** @description Seconds to wait before retrying */ + "Retry-After"?: number; + /** @description Remaining requests in current window */ + "X-RateLimit-Remaining"?: number; + [name: string]: unknown; + }; + content: { + "application/json": { + statusCode?: number; + error?: string; + message?: string; + retryAfter?: string; + }; + }; + }; + }; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + getStatus: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description API status */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ok?: boolean; + watchedPairs?: string[]; + lastIndexedLedger?: number | null; + /** Format: date-time */ + lastProcessedAt?: string | null; + }; + }; + }; + 429: components["responses"]["TooManyRequests"]; + }; + }; + getPrice: { + parameters: { + query?: never; + header?: never; + path: { + /** @example XLM */ + assetA: string; + /** @example USDC */ + assetB: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Aggregated price data */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AggregatedPrice"]; + }; + }; + 402: components["responses"]["PaymentRequired"]; + /** @description Pair not watched */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 429: components["responses"]["TooManyRequests"]; + }; + }; + getBestRoute: { + parameters: { + query?: { + amount?: number; + }; + header?: never; + path: { + assetA: string; + assetB: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Best route information */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["RouteInfo"]; + }; + }; + 402: components["responses"]["PaymentRequired"]; + 429: components["responses"]["TooManyRequests"]; + }; + }; + getPriceHistory: { + parameters: { + query?: { + window?: "1m" | "5m" | "1h" | "24h"; + limit?: number; + }; + header?: never; + path: { + assetA: string; + assetB: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Price history */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + pairKey?: string; + window?: string; + buckets?: components["schemas"]["PriceBucket"][]; + }; + }; + }; + /** @description Invalid parameters */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 429: components["responses"]["TooManyRequests"]; + }; + }; + getPools: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List of pools */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + pools?: { + pool_id?: string; + asset_a?: string; + asset_b?: string; + reserve_a?: number; + reserve_b?: number; + spot_price?: number; + fee_bp?: number; + /** Format: date-time */ + timestamp?: string; + }[]; + }; + }; + }; + 402: components["responses"]["PaymentRequired"]; + 429: components["responses"]["TooManyRequests"]; + }; + }; + getCandles: { + parameters: { + query?: { + interval?: "1m" | "5m" | "15m" | "1h" | "4h" | "1d"; + from?: string; + to?: string; + }; + header?: never; + path: { + assetA: string; + assetB: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Candle data */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + pairKey?: string; + interval?: string; + from?: string; + to?: string; + candles?: { + /** Format: date-time */ + time?: string; + open?: number; + high?: number; + low?: number; + close?: number; + volume?: number; + }[]; + }; + }; + }; + /** @description Invalid parameters */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 402: components["responses"]["PaymentRequired"]; + 429: components["responses"]["TooManyRequests"]; + }; + }; + getPriceSnapshots: { + parameters: { + query: { + /** @example XLM/USDC */ + pair: string; + interval?: "1m" | "5m" | "1h"; + from?: string; + to?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Historical price snapshots */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + pair?: string; + interval?: string; + /** Format: date-time */ + from?: string; + /** Format: date-time */ + to?: string; + count?: number; + points?: { + /** Format: date-time */ + ts?: string; + price?: number; + volume?: number; + }[]; + }; + }; + }; + /** @description Invalid parameters */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 402: components["responses"]["PaymentRequired"]; + 429: components["responses"]["TooManyRequests"]; + }; + }; + listPairs: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List of pairs */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + pairs?: { + pairKey?: string; + assetA?: components["schemas"]["Asset"]; + assetB?: components["schemas"]["Asset"]; + latestPrice?: number | null; + /** Format: date-time */ + lastUpdated?: string | null; + }[]; + }; + }; + }; + 429: components["responses"]["TooManyRequests"]; + }; + }; + addPair: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @example XLM */ + assetA?: string; + /** @example USDC:GBBD47... */ + assetB?: string; + }; + }; + }; + responses: { + /** @description Pair added */ + 201: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Invalid input */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Pair already exists */ + 409: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 429: components["responses"]["TooManyRequests"]; + }; + }; +} diff --git a/clients/typescript/tsconfig.json b/clients/typescript/tsconfig.json new file mode 100644 index 00000000..b055bc44 --- /dev/null +++ b/clients/typescript/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Bundler", + "rootDir": "./src", + "outDir": "./dist", + "declaration": false, + "declarationMap": false, + "sourceMap": false, + "noEmit": true + }, + "include": ["src/**/*"] +} diff --git a/openapi.yaml b/openapi.yaml index 81a995ee..76069d8a 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -280,7 +280,7 @@ paths: /prices/history: get: - operationId: getPriceHistory + operationId: getPriceSnapshots summary: Get historical 1-minute price snapshots description: >- Returns minute-resolution price snapshots for a pair over [from, to], diff --git a/package-lock.json b/package-lock.json index 59c3e589..e7274006 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,12 +39,36 @@ "@vitest/coverage-v8": "^4.1.5", "fast-check": "^3.0.0", "js-yaml": "^4.2.0", + "openapi-fetch": "^0.17.0", + "openapi-typescript": "^7.13.0", "prisma": "^5.0.0", "tsx": "^4.0.0", "typescript": "^5.0.0", "vitest": "^4.1.5" } }, + "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/code-frame/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/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -56,9 +80,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "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": { @@ -1732,6 +1756,106 @@ "@prisma/debug": "5.22.0" } }, + "node_modules/@redocly/ajv": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.2.tgz", + "integrity": "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js-replace": "^1.0.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@redocly/config": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.22.0.tgz", + "integrity": "sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@redocly/openapi-core": { + "version": "1.34.21", + "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.21.tgz", + "integrity": "sha512-yXMiBFYJbP5JOaxPzx2tJhK+v8h2Wu2uhad1Ro6GzVT8E3F4GcQ7sbLPJ9w4zvGjfxroD0bIh5B0pDtnLV/qUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@redocly/ajv": "8.11.2", + "@redocly/config": "0.22.0", + "colorette": "1.4.0", + "https-proxy-agent": "7.0.6", + "js-levenshtein": "1.1.6", + "js-yaml": "4.3.2", + "minimatch": "5.1.9", + "pluralize": "8.0.0", + "yaml-ast-parser": "0.0.43" + }, + "engines": { + "node": ">=18.17.0", + "npm": ">=9.5.0" + } + }, + "node_modules/@redocly/openapi-core/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/@redocly/openapi-core/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@redocly/openapi-core/node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@redocly/openapi-core/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@redocly/openapi-core/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.0.0-rc.16", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.16.tgz", @@ -2912,6 +3036,13 @@ "node": ">=18" } }, + "node_modules/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "dev": true, + "license": "MIT" + }, "node_modules/chardet": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", @@ -2937,6 +3068,13 @@ "node": ">=0.10.0" } }, + "node_modules/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "dev": true, + "license": "MIT" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -4268,6 +4406,19 @@ "node": ">=8" } }, + "node_modules/index-to-position": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -4483,6 +4634,16 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/js-levenshtein": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/js-tokens": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", @@ -4491,9 +4652,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "dev": true, "funding": [ { @@ -5320,6 +5481,57 @@ "wrappy": "1" } }, + "node_modules/openapi-fetch": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/openapi-fetch/-/openapi-fetch-0.17.0.tgz", + "integrity": "sha512-PsbZR1wAPcG91eEthKhN+Zn92FMHxv+/faECIwjXdxfTODGSGegYv0sc1Olz+HYPvKOuoXfp+0pA2XVt2cI0Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "openapi-typescript-helpers": "^0.1.0" + } + }, + "node_modules/openapi-typescript": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-7.13.0.tgz", + "integrity": "sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@redocly/openapi-core": "^1.34.6", + "ansi-colors": "^4.1.3", + "change-case": "^5.4.4", + "parse-json": "^8.3.0", + "supports-color": "^10.2.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "openapi-typescript": "bin/cli.js" + }, + "peerDependencies": { + "typescript": "^5.x" + } + }, + "node_modules/openapi-typescript-helpers": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/openapi-typescript-helpers/-/openapi-typescript-helpers-0.1.0.tgz", + "integrity": "sha512-OKTGPthhivLw/fHz6c3OPtg72vi86qaMlqbJuVJ23qOvQ+53uw1n7HdmkJFibloF7QEjDrDkzJiOJuockM/ljw==", + "dev": true, + "license": "MIT" + }, + "node_modules/openapi-typescript/node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/outdent": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/outdent/-/outdent-0.5.0.tgz", @@ -5414,6 +5626,24 @@ "quansync": "^0.2.7" } }, + "node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -5709,6 +5939,16 @@ "node": ">=16.20.0" } }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -6968,6 +7208,19 @@ "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", "license": "Unlicense" }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/type-is": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", @@ -7081,6 +7334,13 @@ "node": ">= 0.8" } }, + "node_modules/uri-js-replace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uri-js-replace/-/uri-js-replace-1.0.1.tgz", + "integrity": "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==", + "dev": true, + "license": "MIT" + }, "node_modules/urijs": { "version": "1.19.11", "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz", @@ -7372,6 +7632,23 @@ "node": ">=0.4" } }, + "node_modules/yaml-ast-parser": { + "version": "0.0.43", + "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz", + "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/zod": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/zod/-/zod-4.5.4.tgz", diff --git a/package.json b/package.json index d31b331c..ffac5fde 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,8 @@ "start": "node dist/index.js", "test": "vitest run", "openapi:gen": "tsx scripts/generate-openapi.ts", + "client:gen": "openapi-typescript openapi.yaml -o clients/typescript/src/schema.d.ts", + "client:typecheck": "tsc -p clients/typescript/tsconfig.json --noEmit", "changeset": "changeset", "version-packages": "changeset version", "release": "changeset tag", @@ -52,6 +54,8 @@ "@vitest/coverage-v8": "^4.1.5", "fast-check": "^3.0.0", "js-yaml": "^4.2.0", + "openapi-fetch": "^0.17.0", + "openapi-typescript": "^7.13.0", "prisma": "^5.0.0", "tsx": "^4.0.0", "typescript": "^5.0.0",