# SDK Reference & CLI

> **Section:** [TypeScript SDK](https://wavehouse.dev/sdk.md)
> **Related:** [SDK Admin & System](https://wavehouse.dev/sdk/admin.md) · [SDK Pipes](https://wavehouse.dev/sdk/pipes.md) · [SDK Queries](https://wavehouse.dev/sdk/queries.md) · [SDK Streaming & Live Queries](https://wavehouse.dev/sdk/streaming.md)
> **Also:** [HTML version](https://wavehouse.dev/sdk/reference) · [Docs index](https://wavehouse.dev/llms.txt)

---
Cross-cutting reference for `@wavehouse/sdk`: cancellation, the error model
behind every [`Result<T>`](/sdk#result-type), the complete API tree at a
glance, and the tooling that ships in the package.

## AbortController Support

All async operations accept an `AbortSignal` for cancellation:

```ts
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000); // 5s timeout

const { data, error } = await wh.from('clicks').fetch({ signal: controller.signal });
if (error?.code === 'ABORTED') {
  console.log('Request timed out');
}
```

---

## Error Handling

The SDK **never throws**. All errors are returned in `Result.error`.

| Status | Code | Retryable | Description |
|--------|------|-----------|-------------|
| 400 | `HTTP_400` | No | Bad request (validation, missing fields) |
| 401 | `HTTP_401` | No | Missing or invalid JWT |
| 403 | `HTTP_403` | No | Insufficient permissions |
| 404 | `HTTP_404` | No | Table or pipe not found |
| 500 | `HTTP_500` | Yes | Server error (retried per `maxRetries`) |
| 503 | `HTTP_503` | Yes | Service unavailable (auto-retries with `Retry-After`) |
| 0 | `NETWORK_ERROR` | Yes | Network failure (retried with exponential backoff) |
| 0 | `ABORTED` | No | Request canceled via `AbortSignal` |

---

## Full API Tree

```text
createClient<DB>(config) → WaveHouseClient
├── .from(table) → TableRef (NOT thenable)
│   ├── .fetch(opts?) → Promise<Result<Row[]>>
│   ├── .select(...cols?) → QueryBuilder (PromiseLike)
│   │   ├── .select() .selectAll() .where() .count() .sum() .avg() .min() .max()
│   │   │   .countDistinct() .aggregate() .groupBy() .orderBy()
│   │   │   .limit() .timeRange() .cacheTTL()
│   │   ├── .fetch(opts?) → Promise<Result<Row[]>>
│   │   ├── .stream(opts?) → StreamController
│   │   └── .liveQuery(subscriber, opts?) → LiveQuery
│   ├── .selectAll() → QueryBuilder (PromiseLike)
│   ├── .insert(data) → Promise<Result<InsertResult>>
│   ├── .insertNDJSON(source) → Promise<Result<InsertResult>>
│   ├── .schema() → Promise<Result<TableSchema>>
│   └── .stream(opts?) → StreamController
├── .pipe(name, params?) → PipeRef (PromiseLike)
│   ├── .fetch(opts?) → Promise<Result<Row[]>>
│   └── .stream(opts?) → StreamController
├── .pipes (admin)
│   ├── .list() → Promise<Result<Pipe[]>>
│   ├── .get(name) → Promise<Result<Pipe>>
│   ├── .set(name, def) → Promise<Result<void>>
│   └── .delete(name) → Promise<Result<void>>
├── .sql(query, opts?) → Promise<Result<Row[]>>
├── .schema
│   ├── .list() → Promise<Result<Schemas>>
│   └── .refresh() → Promise<Result<void>>
├── .policy (admin)
│   ├── .get() → Promise<Result<Policy>>
│   ├── .set(policy) → Promise<Result<void>>
│   └── .validate(policy) → Promise<Result<ValidationResult>>
├── .dlq
│   ├── .list() → Promise<Result<DLQStats>>
│   ├── .table(name) → Promise<Result<DLQStats>>
│   └── .stream() → StreamController  // not yet functional server-side — #197
└── .sys
    └── .health() → Promise<Result<void>>

StreamController (NOT thenable)
├── .subscribe({ next, status?, error? }) → unsubscribe()
├── .close()
├── .status → StreamStatus
└── [Symbol.asyncIterator]() → AsyncIterableIterator<StreamEvent>
```

## Codegen CLI

Generate TypeScript types from a running WaveHouse instance. The package ships a `wavehouse-codegen` bin, so after installing `@wavehouse/sdk` you can run it with `npx`:

```bash
npx wavehouse-codegen --url http://localhost:8080 --out ./src/db.d.ts

# Or, working inside this repo (clients/ts/):
pnpm codegen --url http://localhost:8080 --out ./src/db.d.ts
```

Codegen reads `/v1/schema`, which is **admin-only**. Against a non-dev server, pass an admin-role token with `--auth <jwt>` or the request is denied with `403`.

**Options:**

| Flag | Description | Default |
|------|-------------|---------|
| `--url`, `-u` | WaveHouse base URL | `http://localhost:8080` |
| `--out`, `-o` | Output .d.ts file path | `./wavehouse.d.ts` |
| `--auth`, `-a` | Bearer token (if auth required) | — |

**Example output:**

```ts
// Auto-generated by @wavehouse/sdk codegen
export interface Database {
  clicks: ClicksRow;
  events: EventsRow;
}

export interface ClicksRow {
  event_id: string;
  page: string;
  user_id: string;
  duration_ms: number;
  received_timestamp: string;
}
```

**ClickHouse → TypeScript type mapping:**

| ClickHouse Type | TypeScript Type |
|----------------|-----------------|
| `String`, `FixedString`, `UUID`, `DateTime*`, `Date*`, `Enum*`, `IPv4/6` | `string` |
| `UInt*`, `Int*`, `Float*`, `Decimal*` | `number` |
| `Bool` | `boolean` |
| `Nullable(T)` | `T \| null` |
| `Array(T)` | `T[]` |
| `Map(K, V)` | `Record<K, V>` |
| `LowCardinality(T)` | same as `T` |

## E2E Testing

The SDK doubles as the E2E integration test harness. Tests in `tests/e2e/sdk/` exercise the full pipeline (ingest → ClickHouse → query) through the SDK, validating both the backend and the client library in one pass.

```bash
# Run all E2E tests: the orchestrator boots a ClickHouse testcontainer +
# the wavehouse-cov binary, then runs the SDK suite
make test-e2e
```

Test files live in `tests/e2e/sdk/`: `admin`, `auth`, `batching`, `cache`, `dlq`, `ingest`, `ndjson`, `query`, `streaming`, `stress` (each `*.test.ts`).

See [Development Guide — E2E Tests via SDK](/development#e2e-tests-via-sdk) for architecture details and workflow tips.