@wavehouse/sdk 0.0.0-dev.h825a5d7fa405 → 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,44 @@ 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
+
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'`.
14
+
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.
16
+
17
+ ## Use without a build step (CDN)
18
+
19
+ No bundler, no `npm`, no framework required — drop the SDK straight into an HTML file you deploy over FTP, object storage, or any static host.
20
+
21
+ **ES module (recommended).** Modern browsers run `type="module"` natively:
22
+
23
+ ```html
24
+ <script type="module">
25
+ import { createClient } from 'https://esm.sh/@wavehouse/sdk';
26
+
27
+ const wh = createClient({ baseURL: 'https://your-wavehouse.example.com' });
28
+ const { data, error } = await wh.from('clicks').select('page').limit(10);
29
+ console.log(data ?? error);
30
+ </script>
31
+ ```
32
+
33
+ Pin a version for production (`https://esm.sh/@wavehouse/sdk@0.1.0`). jsDelivr (`https://cdn.jsdelivr.net/npm/@wavehouse/sdk/+esm`) and unpkg (`https://unpkg.com/@wavehouse/sdk?module`) serve the same ES module.
34
+
35
+ **Classic global (`<script src>`).** For pages that can't use ES modules, the bundled IIFE build attaches everything to a `WaveHouse` global:
36
+
37
+ ```html
38
+ <script src="https://cdn.jsdelivr.net/npm/@wavehouse/sdk"></script>
39
+ <script>
40
+ const wh = WaveHouse.createClient({ baseURL: 'https://your-wavehouse.example.com' });
41
+ wh.from('clicks').select('page').limit(10).then(({ data }) => console.log(data));
42
+ </script>
43
+ ```
44
+
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.
46
+
47
+ Streaming (`.stream()`) runs on `fetch`, so it works in both forms with no polyfill.
48
+
11
49
  ## Quick Start
12
50
 
13
51
  ```ts
@@ -19,6 +57,8 @@ const wh = createClient({
19
57
  });
20
58
  ```
21
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
+
22
62
  ### Query Data
23
63
 
24
64
  ```ts
@@ -89,7 +129,9 @@ const { data } = await wh.from('clicks').select('page').limit(10);
89
129
 
90
130
  ## Error Handling
91
131
 
92
- 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).
93
135
 
94
136
  ```ts
95
137
  const { data, error } = await wh.from('clicks').fetch();
@@ -102,13 +144,13 @@ if (error) {
102
144
 
103
145
  ## Codegen
104
146
 
105
- Generate TypeScript types from your live WaveHouse schema:
147
+ Generate TypeScript types from your live WaveHouse schema. The package ships a `wavehouse-codegen` bin, so after installing you can run it with `npx`:
106
148
 
107
149
  ```bash
108
- npm run codegen -- --url http://localhost:8080 --out ./src/db.d.ts
150
+ npx wavehouse-codegen --url http://localhost:8080 --out ./src/db.d.ts
109
151
  ```
110
152
 
111
- This introspects `/v1/schema`, maps ClickHouse column types to TypeScript, and outputs a `Database` interface you can pass to `createClient<Database>()`.
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.
112
154
 
113
155
  ## Development & Testing
114
156
 
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env node
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
+
22
+ // src/cli/codegen.ts
23
+ function parseArgs(argv) {
24
+ const args = { url: "http://localhost:8080", out: "./wavehouse.d.ts" };
25
+ for (let i = 2; i < argv.length; i++) {
26
+ switch (argv[i]) {
27
+ case "--url":
28
+ case "-u":
29
+ args.url = argv[++i];
30
+ break;
31
+ case "--out":
32
+ case "-o":
33
+ args.out = argv[++i];
34
+ break;
35
+ case "--auth":
36
+ case "-a":
37
+ args.auth = argv[++i];
38
+ break;
39
+ case "--help":
40
+ case "-h":
41
+ console.log(`wavehouse codegen \u2014 Generate TypeScript types from WaveHouse schema
42
+
43
+ Options:
44
+ --url, -u WaveHouse base URL (default: http://localhost:8080)
45
+ --out, -o Output file path (default: ./wavehouse.d.ts)
46
+ --auth, -a Bearer token for authenticated endpoints
47
+ --help, -h Show this help`);
48
+ process.exit(0);
49
+ }
50
+ }
51
+ return args;
52
+ }
53
+ function chTypeToTS(chType) {
54
+ if (chType.startsWith("Nullable(") && chType.endsWith(")")) {
55
+ const inner = chType.slice(9, -1);
56
+ return `${chTypeToTS(inner)} | null`;
57
+ }
58
+ if (chType.startsWith("LowCardinality(") && chType.endsWith(")")) {
59
+ return chTypeToTS(chType.slice(15, -1));
60
+ }
61
+ if (chType === "String" || chType.startsWith("FixedString(") || chType === "UUID" || chType.startsWith("DateTime") || chType.startsWith("Date") || chType.startsWith("Enum8(") || chType.startsWith("Enum16(") || chType === "IPv4" || chType === "IPv6") {
62
+ return "string";
63
+ }
64
+ if (chType === "Bool") return "boolean";
65
+ if (isNumeric(chType)) return "number";
66
+ if (chType.startsWith("Array(") && chType.endsWith(")")) {
67
+ const inner = chType.slice(6, -1);
68
+ return `${chTypeToTS(inner)}[]`;
69
+ }
70
+ if (chType.startsWith("Map(") && chType.endsWith(")")) {
71
+ const inner = chType.slice(4, -1);
72
+ const comma = findTopLevelComma(inner);
73
+ if (comma !== -1) {
74
+ const keyType = chTypeToTS(inner.slice(0, comma).trim());
75
+ const valType = chTypeToTS(inner.slice(comma + 1).trim());
76
+ return `Record<${keyType}, ${valType}>`;
77
+ }
78
+ return "Record<string, unknown>";
79
+ }
80
+ if (chType.startsWith("Tuple(")) {
81
+ return "unknown[]";
82
+ }
83
+ return "unknown";
84
+ }
85
+ function isNumeric(t) {
86
+ const numericPrefixes = [
87
+ "UInt8",
88
+ "UInt16",
89
+ "UInt32",
90
+ "UInt64",
91
+ "UInt128",
92
+ "UInt256",
93
+ "Int8",
94
+ "Int16",
95
+ "Int32",
96
+ "Int64",
97
+ "Int128",
98
+ "Int256",
99
+ "Float32",
100
+ "Float64",
101
+ "Decimal"
102
+ ];
103
+ return numericPrefixes.some((p) => t === p || t.startsWith(`${p}(`));
104
+ }
105
+ function findTopLevelComma(s) {
106
+ let depth = 0;
107
+ for (let i = 0; i < s.length; i++) {
108
+ if (s[i] === "(") depth++;
109
+ else if (s[i] === ")") depth--;
110
+ else if (s[i] === "," && depth === 0) return i;
111
+ }
112
+ return -1;
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
+ }
124
+ async function fetchSchemas(url, auth) {
125
+ const headers = {};
126
+ if (auth) headers.Authorization = `Bearer ${auth}`;
127
+ const res = await fetch(resolveURL(url, "/v1/ops/schema").toString(), { headers });
128
+ if (!res.ok) {
129
+ const text = await res.text();
130
+ throw new Error(`Schema fetch failed (${res.status}): ${text}`);
131
+ }
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;
137
+ }
138
+ function generateTypes(schemas) {
139
+ const lines = [
140
+ "// Auto-generated by @wavehouse/sdk codegen",
141
+ `// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
142
+ "// Do not edit manually \u2014 re-run: npm run codegen",
143
+ "",
144
+ "export interface Database {"
145
+ ];
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;`);
149
+ }
150
+ lines.push("}");
151
+ lines.push("");
152
+ for (const table of tables) {
153
+ const rowType = `${pascalCase(table.name)}Row`;
154
+ lines.push(`export interface ${rowType} {`);
155
+ for (const col of table.columns) {
156
+ const tsType = chTypeToTS(col.type);
157
+ const optional = col.has_default ? "?" : "";
158
+ lines.push(` ${col.name}${optional}: ${tsType};`);
159
+ }
160
+ lines.push("}");
161
+ lines.push("");
162
+ }
163
+ return lines.join("\n");
164
+ }
165
+ function pascalCase(s) {
166
+ return s.split(/[_\-\s]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
167
+ }
168
+ async function main() {
169
+ const args = parseArgs(process.argv);
170
+ console.log(`Fetching schema from ${args.url}...`);
171
+ const schemas = await fetchSchemas(args.url, args.auth);
172
+ if (schemas.length === 0) {
173
+ console.warn("No tables found. Is WaveHouse running with tables in ClickHouse?");
174
+ process.exit(1);
175
+ }
176
+ console.log(`Found ${schemas.length} table(s): ${schemas.map((t) => t.name).join(", ")}`);
177
+ const output = generateTypes(schemas);
178
+ const { writeFile } = await import("fs/promises");
179
+ const { resolve } = await import("path");
180
+ const outPath = resolve(args.out);
181
+ await writeFile(outPath, output, "utf-8");
182
+ console.log(`\u2713 Types written to ${outPath}`);
183
+ }
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
+ };