@wavehouse/sdk 0.0.0-dev.h8ea15a023aa2 → 0.0.0-dev.h9539004107f0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @wavehouse/sdk
2
2
 
3
- Zero-dependency TypeScript client for [WaveHouse](https://github.com/Wave-RF/WaveHouse) — schema-aware real-time API gateway for ClickHouse.
3
+ TypeScript client for [WaveHouse](https://github.com/Wave-RF/WaveHouse) — schema-aware real-time API gateway for ClickHouse. One runtime dependency (`eventsource-parser`, ~1.4 KB gzipped, itself dependency-free).
4
4
 
5
5
  ## Installation
6
6
 
@@ -8,6 +8,8 @@ Zero-dependency TypeScript client for [WaveHouse](https://github.com/Wave-RF/Wav
8
8
  npm install @wavehouse/sdk
9
9
  ```
10
10
 
11
+ Requires Node 22 or newer — the only line this SDK is tested against; Node 18 and 20 are past upstream end-of-life. Neither browsers nor Node need a polyfill: streaming runs on `fetch`, like the rest of the SDK — see [Runtime support](https://wavehouse.dev/sdk/#runtime-support).
12
+
11
13
  This works in any framework that uses a bundler — React, Vue, Svelte, Angular, Astro, SolidJS, or plain Vite — with `import { createClient } from '@wavehouse/sdk'`.
12
14
 
13
15
  The package is published with [npm provenance](https://docs.npmjs.com/generating-provenance-statements) — npm shows a verified provenance badge on the [package page](https://www.npmjs.com/package/@wavehouse/sdk), and `npm audit signatures` checks the attestation against this repository's signed build.
@@ -42,7 +44,7 @@ Pin a version for production (`https://esm.sh/@wavehouse/sdk@0.1.0`). jsDelivr (
42
44
 
43
45
  **Versioning.** A bare CDN URL serves the latest published **release**; pin for production (`@wavehouse/sdk@0.1.0`) or float on a range (`@0` for the newest 0.x, `@0.1` for 0.1.x). Builds from `main` are published under the `dev` tag — `@wavehouse/sdk@dev` — for trying unreleased changes.
44
46
 
45
- Streaming (`.stream()`) uses the browser's native `EventSource`, so it works in both forms with no polyfill.
47
+ Streaming (`.stream()`) runs on `fetch`, so it works in both forms with no polyfill.
46
48
 
47
49
  ## Quick Start
48
50
 
@@ -55,6 +57,8 @@ const wh = createClient({
55
57
  });
56
58
  ```
57
59
 
60
+ `baseURL` may include a path prefix (`https://app.example.com/api/warehouse`) when WaveHouse is served under one — see [Serving under a path prefix](https://wavehouse.dev/sdk#serving-under-a-path-prefix).
61
+
58
62
  ### Query Data
59
63
 
60
64
  ```ts
@@ -125,7 +129,9 @@ const { data } = await wh.from('clicks').select('page').limit(10);
125
129
 
126
130
  ## Error Handling
127
131
 
128
- All operations return `{ data, error }` tuples — the SDK never throws.
132
+ Async request methods return a `Result<T>` object — destructure it as `{ data, error }` — and never throw for anything the server returns. `.stream()` and `.liveQuery()` return controllers instead, reporting failures through the subscriber's `error` callback.
133
+
134
+ The SDK does throw on caller and environment errors: a non-absolute `baseURL` (REST calls reject with a `TypeError`; streams report a terminal `SSE_CONNECT_ERROR`), `.stream()` / `.liveQuery()` in a runtime with no global `fetch` and no `options.fetch`, and an `auth` callback that rejects — a token-refresh failure propagates out of the REST call, and on a stream is reported as a retryable `SSE_AUTH_ERROR`. A dropped stream *is* re-dialed by the SDK, on a jittered backoff; only non-retryable failures end it. One more exception can escape an SDK call, though it is yours rather than ours: a subscriber's `status` handler throwing on the first, synchronous `.subscribe()` / `.liveQuery()` call — see [Error Handling](https://wavehouse.dev/sdk/reference#error-handling).
129
135
 
130
136
  ```ts
131
137
  const { data, error } = await wh.from('clicks').fetch();
@@ -144,7 +150,7 @@ Generate TypeScript types from your live WaveHouse schema. The package ships a `
144
150
  npx wavehouse-codegen --url http://localhost:8080 --out ./src/db.d.ts
145
151
  ```
146
152
 
147
- This introspects `/v1/schema`, maps ClickHouse column types to TypeScript, and outputs a `Database` interface you can pass to `createClient<Database>()`. `/v1/schema` is **admin-only** — pass an admin-role token with `--auth <jwt>` (or `-a`) against any non-dev policy.
153
+ This introspects `/v1/ops/schema`, maps ClickHouse column types to TypeScript, and outputs a `Database` interface you can pass to `createClient<Database>()`. `/v1/ops/schema` is **admin-only** — pass an admin-role token with `--auth <jwt>` (or `-a`) against any non-dev policy.
148
154
 
149
155
  ## Development & Testing
150
156
 
@@ -1,5 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // src/cli/codegen.ts
4
+ import { realpathSync } from "fs";
5
+ import { pathToFileURL } from "url";
6
+
7
+ // src/url.ts
8
+ function resolveURL(base, path, params) {
9
+ const root = new URL(base);
10
+ root.search = "";
11
+ root.hash = "";
12
+ if (!root.pathname.endsWith("/")) root.pathname += "/";
13
+ const url = new URL(path.replace(/^\/+/, ""), root);
14
+ if (params) {
15
+ for (const [k, v] of Object.entries(params)) {
16
+ url.searchParams.set(k, v);
17
+ }
18
+ }
19
+ return url;
20
+ }
21
+
3
22
  // src/cli/codegen.ts
4
23
  function parseArgs(argv) {
5
24
  const args = { url: "http://localhost:8080", out: "./wavehouse.d.ts" };
@@ -92,15 +111,29 @@ function findTopLevelComma(s) {
92
111
  }
93
112
  return -1;
94
113
  }
114
+ function isColumn(value) {
115
+ if (typeof value !== "object" || value === null) return false;
116
+ const col = value;
117
+ return typeof col.name === "string" && typeof col.type === "string";
118
+ }
119
+ function isTableSchema(value) {
120
+ if (typeof value !== "object" || value === null) return false;
121
+ const table = value;
122
+ return typeof table.name === "string" && Array.isArray(table.columns) && table.columns.every(isColumn);
123
+ }
95
124
  async function fetchSchemas(url, auth) {
96
125
  const headers = {};
97
126
  if (auth) headers.Authorization = `Bearer ${auth}`;
98
- const res = await fetch(`${url.replace(/\/+$/, "")}/v1/schema`, { headers });
127
+ const res = await fetch(resolveURL(url, "/v1/ops/schema").toString(), { headers });
99
128
  if (!res.ok) {
100
129
  const text = await res.text();
101
130
  throw new Error(`Schema fetch failed (${res.status}): ${text}`);
102
131
  }
103
- return await res.json();
132
+ const body = await res.json();
133
+ if (!Array.isArray(body) || !body.every(isTableSchema)) {
134
+ throw new Error("Unexpected /v1/ops/schema response: expected a JSON array of table schemas");
135
+ }
136
+ return body;
104
137
  }
105
138
  function generateTypes(schemas) {
106
139
  const lines = [
@@ -110,19 +143,16 @@ function generateTypes(schemas) {
110
143
  "",
111
144
  "export interface Database {"
112
145
  ];
113
- const tableNames = Object.keys(schemas).sort();
114
- for (const tableName of tableNames) {
115
- const _schema = schemas[tableName];
116
- const rowType = `${pascalCase(tableName)}Row`;
117
- lines.push(` ${tableName}: ${rowType};`);
146
+ const tables = [...schemas].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
147
+ for (const table of tables) {
148
+ lines.push(` ${table.name}: ${pascalCase(table.name)}Row;`);
118
149
  }
119
150
  lines.push("}");
120
151
  lines.push("");
121
- for (const tableName of tableNames) {
122
- const schema = schemas[tableName];
123
- const rowType = `${pascalCase(tableName)}Row`;
152
+ for (const table of tables) {
153
+ const rowType = `${pascalCase(table.name)}Row`;
124
154
  lines.push(`export interface ${rowType} {`);
125
- for (const col of schema.columns) {
155
+ for (const col of table.columns) {
126
156
  const tsType = chTypeToTS(col.type);
127
157
  const optional = col.has_default ? "?" : "";
128
158
  lines.push(` ${col.name}${optional}: ${tsType};`);
@@ -139,12 +169,11 @@ async function main() {
139
169
  const args = parseArgs(process.argv);
140
170
  console.log(`Fetching schema from ${args.url}...`);
141
171
  const schemas = await fetchSchemas(args.url, args.auth);
142
- const tableCount = Object.keys(schemas).length;
143
- if (tableCount === 0) {
172
+ if (schemas.length === 0) {
144
173
  console.warn("No tables found. Is WaveHouse running with tables in ClickHouse?");
145
174
  process.exit(1);
146
175
  }
147
- console.log(`Found ${tableCount} table(s): ${Object.keys(schemas).join(", ")}`);
176
+ console.log(`Found ${schemas.length} table(s): ${schemas.map((t) => t.name).join(", ")}`);
148
177
  const output = generateTypes(schemas);
149
178
  const { writeFile } = await import("fs/promises");
150
179
  const { resolve } = await import("path");
@@ -152,7 +181,22 @@ async function main() {
152
181
  await writeFile(outPath, output, "utf-8");
153
182
  console.log(`\u2713 Types written to ${outPath}`);
154
183
  }
155
- main().catch((err) => {
156
- console.error("Codegen failed:", err.message);
157
- process.exit(1);
158
- });
184
+ function isDirectInvocation() {
185
+ if (!process.argv[1]) return false;
186
+ try {
187
+ return import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href;
188
+ } catch {
189
+ return false;
190
+ }
191
+ }
192
+ if (isDirectInvocation()) {
193
+ main().catch((err) => {
194
+ console.error("Codegen failed:", err.message);
195
+ process.exit(1);
196
+ });
197
+ }
198
+ export {
199
+ chTypeToTS,
200
+ fetchSchemas,
201
+ generateTypes
202
+ };