Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/typed-typescript-client.md
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,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.
Expand Down
23 changes: 23 additions & 0 deletions clients/typescript/src/client.ts
Original file line number Diff line number Diff line change
@@ -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<paths>;

export type LensClientOptions = Omit<ClientOptions, "baseUrl" | "fetch"> & {
baseUrl?: string;
fetch?: typeof globalThis.fetch;
};

export const DEFAULT_BASE_URL = "http://localhost:3002";

export function createLensClient(
options: LensClientOptions = {},
): LensClient {
return createClient<paths>({
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
fetch: options.fetch,
headers: options.headers,
});
}
50 changes: 50 additions & 0 deletions clients/typescript/src/example.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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();
3 changes: 3 additions & 0 deletions clients/typescript/src/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Loading
Loading