@wavehouse/sdk 0.0.0-dev.hab0797ca5a74 → 0.0.0-dev.hb0c3a577b2f8

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
@@ -8,6 +8,42 @@ Zero-dependency TypeScript client for [WaveHouse](https://github.com/Wave-RF/Wav
8
8
  npm install @wavehouse/sdk
9
9
  ```
10
10
 
11
+ 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
+
13
+ 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.
14
+
15
+ ## Use without a build step (CDN)
16
+
17
+ 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.
18
+
19
+ **ES module (recommended).** Modern browsers run `type="module"` natively:
20
+
21
+ ```html
22
+ <script type="module">
23
+ import { createClient } from 'https://esm.sh/@wavehouse/sdk';
24
+
25
+ const wh = createClient({ baseURL: 'https://your-wavehouse.example.com' });
26
+ const { data, error } = await wh.from('clicks').select('page').limit(10);
27
+ console.log(data ?? error);
28
+ </script>
29
+ ```
30
+
31
+ 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.
32
+
33
+ **Classic global (`<script src>`).** For pages that can't use ES modules, the bundled IIFE build attaches everything to a `WaveHouse` global:
34
+
35
+ ```html
36
+ <script src="https://cdn.jsdelivr.net/npm/@wavehouse/sdk"></script>
37
+ <script>
38
+ const wh = WaveHouse.createClient({ baseURL: 'https://your-wavehouse.example.com' });
39
+ wh.from('clicks').select('page').limit(10).then(({ data }) => console.log(data));
40
+ </script>
41
+ ```
42
+
43
+ **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
+
45
+ Streaming (`.stream()`) uses the browser's native `EventSource`, so it works in both forms with no polyfill.
46
+
11
47
  ## Quick Start
12
48
 
13
49
  ```ts
@@ -19,6 +55,8 @@ const wh = createClient({
19
55
  });
20
56
  ```
21
57
 
58
+ `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).
59
+
22
60
  ### Query Data
23
61
 
24
62
  ```ts
@@ -89,7 +127,9 @@ const { data } = await wh.from('clicks').select('page').limit(10);
89
127
 
90
128
  ## Error Handling
91
129
 
92
- All operations return `{ data, error }` tuples — the SDK never throws.
130
+ 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.
131
+
132
+ The SDK does throw on caller and environment errors: a non-absolute `baseURL` (REST calls reject with a `TypeError`; streams report `SSE_CONNECT_ERROR`, and despite that error's `retryable: true` the SDK never re-dials the stream itself), `.stream()` / `.liveQuery()` in a runtime with no `EventSource`, and an `auth` callback that rejects — a token-refresh failure propagates out of the REST call.
93
133
 
94
134
  ```ts
95
135
  const { data, error } = await wh.from('clicks').fetch();
@@ -102,13 +142,13 @@ if (error) {
102
142
 
103
143
  ## Codegen
104
144
 
105
- Generate TypeScript types from your live WaveHouse schema:
145
+ 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
146
 
107
147
  ```bash
108
- npm run codegen -- --url http://localhost:8080 --out ./src/db.d.ts
148
+ npx wavehouse-codegen --url http://localhost:8080 --out ./src/db.d.ts
109
149
  ```
110
150
 
111
- This introspects `/v1/schema`, maps ClickHouse column types to TypeScript, and outputs a `Database` interface you can pass to `createClient<Database>()`.
151
+ 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.
112
152
 
113
153
  ## Development & Testing
114
154
 
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/url.ts
4
+ function resolveURL(base, path, params) {
5
+ const root = new URL(base);
6
+ root.search = "";
7
+ root.hash = "";
8
+ if (!root.pathname.endsWith("/")) root.pathname += "/";
9
+ const url = new URL(path.replace(/^\/+/, ""), root);
10
+ if (params) {
11
+ for (const [k, v] of Object.entries(params)) {
12
+ url.searchParams.set(k, v);
13
+ }
14
+ }
15
+ return url;
16
+ }
17
+
18
+ // src/cli/codegen.ts
19
+ function parseArgs(argv) {
20
+ const args = { url: "http://localhost:8080", out: "./wavehouse.d.ts" };
21
+ for (let i = 2; i < argv.length; i++) {
22
+ switch (argv[i]) {
23
+ case "--url":
24
+ case "-u":
25
+ args.url = argv[++i];
26
+ break;
27
+ case "--out":
28
+ case "-o":
29
+ args.out = argv[++i];
30
+ break;
31
+ case "--auth":
32
+ case "-a":
33
+ args.auth = argv[++i];
34
+ break;
35
+ case "--help":
36
+ case "-h":
37
+ console.log(`wavehouse codegen \u2014 Generate TypeScript types from WaveHouse schema
38
+
39
+ Options:
40
+ --url, -u WaveHouse base URL (default: http://localhost:8080)
41
+ --out, -o Output file path (default: ./wavehouse.d.ts)
42
+ --auth, -a Bearer token for authenticated endpoints
43
+ --help, -h Show this help`);
44
+ process.exit(0);
45
+ }
46
+ }
47
+ return args;
48
+ }
49
+ function chTypeToTS(chType) {
50
+ if (chType.startsWith("Nullable(") && chType.endsWith(")")) {
51
+ const inner = chType.slice(9, -1);
52
+ return `${chTypeToTS(inner)} | null`;
53
+ }
54
+ if (chType.startsWith("LowCardinality(") && chType.endsWith(")")) {
55
+ return chTypeToTS(chType.slice(15, -1));
56
+ }
57
+ if (chType === "String" || chType.startsWith("FixedString(") || chType === "UUID" || chType.startsWith("DateTime") || chType.startsWith("Date") || chType.startsWith("Enum8(") || chType.startsWith("Enum16(") || chType === "IPv4" || chType === "IPv6") {
58
+ return "string";
59
+ }
60
+ if (chType === "Bool") return "boolean";
61
+ if (isNumeric(chType)) return "number";
62
+ if (chType.startsWith("Array(") && chType.endsWith(")")) {
63
+ const inner = chType.slice(6, -1);
64
+ return `${chTypeToTS(inner)}[]`;
65
+ }
66
+ if (chType.startsWith("Map(") && chType.endsWith(")")) {
67
+ const inner = chType.slice(4, -1);
68
+ const comma = findTopLevelComma(inner);
69
+ if (comma !== -1) {
70
+ const keyType = chTypeToTS(inner.slice(0, comma).trim());
71
+ const valType = chTypeToTS(inner.slice(comma + 1).trim());
72
+ return `Record<${keyType}, ${valType}>`;
73
+ }
74
+ return "Record<string, unknown>";
75
+ }
76
+ if (chType.startsWith("Tuple(")) {
77
+ return "unknown[]";
78
+ }
79
+ return "unknown";
80
+ }
81
+ function isNumeric(t) {
82
+ const numericPrefixes = [
83
+ "UInt8",
84
+ "UInt16",
85
+ "UInt32",
86
+ "UInt64",
87
+ "UInt128",
88
+ "UInt256",
89
+ "Int8",
90
+ "Int16",
91
+ "Int32",
92
+ "Int64",
93
+ "Int128",
94
+ "Int256",
95
+ "Float32",
96
+ "Float64",
97
+ "Decimal"
98
+ ];
99
+ return numericPrefixes.some((p) => t === p || t.startsWith(`${p}(`));
100
+ }
101
+ function findTopLevelComma(s) {
102
+ let depth = 0;
103
+ for (let i = 0; i < s.length; i++) {
104
+ if (s[i] === "(") depth++;
105
+ else if (s[i] === ")") depth--;
106
+ else if (s[i] === "," && depth === 0) return i;
107
+ }
108
+ return -1;
109
+ }
110
+ async function fetchSchemas(url, auth) {
111
+ const headers = {};
112
+ if (auth) headers.Authorization = `Bearer ${auth}`;
113
+ const res = await fetch(resolveURL(url, "/v1/schema").toString(), { headers });
114
+ if (!res.ok) {
115
+ const text = await res.text();
116
+ throw new Error(`Schema fetch failed (${res.status}): ${text}`);
117
+ }
118
+ return await res.json();
119
+ }
120
+ function generateTypes(schemas) {
121
+ const lines = [
122
+ "// Auto-generated by @wavehouse/sdk codegen",
123
+ `// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
124
+ "// Do not edit manually \u2014 re-run: npm run codegen",
125
+ "",
126
+ "export interface Database {"
127
+ ];
128
+ const tableNames = Object.keys(schemas).sort();
129
+ for (const tableName of tableNames) {
130
+ const _schema = schemas[tableName];
131
+ const rowType = `${pascalCase(tableName)}Row`;
132
+ lines.push(` ${tableName}: ${rowType};`);
133
+ }
134
+ lines.push("}");
135
+ lines.push("");
136
+ for (const tableName of tableNames) {
137
+ const schema = schemas[tableName];
138
+ const rowType = `${pascalCase(tableName)}Row`;
139
+ lines.push(`export interface ${rowType} {`);
140
+ for (const col of schema.columns) {
141
+ const tsType = chTypeToTS(col.type);
142
+ const optional = col.has_default ? "?" : "";
143
+ lines.push(` ${col.name}${optional}: ${tsType};`);
144
+ }
145
+ lines.push("}");
146
+ lines.push("");
147
+ }
148
+ return lines.join("\n");
149
+ }
150
+ function pascalCase(s) {
151
+ return s.split(/[_\-\s]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
152
+ }
153
+ async function main() {
154
+ const args = parseArgs(process.argv);
155
+ console.log(`Fetching schema from ${args.url}...`);
156
+ const schemas = await fetchSchemas(args.url, args.auth);
157
+ const tableCount = Object.keys(schemas).length;
158
+ if (tableCount === 0) {
159
+ console.warn("No tables found. Is WaveHouse running with tables in ClickHouse?");
160
+ process.exit(1);
161
+ }
162
+ console.log(`Found ${tableCount} table(s): ${Object.keys(schemas).join(", ")}`);
163
+ const output = generateTypes(schemas);
164
+ const { writeFile } = await import("fs/promises");
165
+ const { resolve } = await import("path");
166
+ const outPath = resolve(args.out);
167
+ await writeFile(outPath, output, "utf-8");
168
+ console.log(`\u2713 Types written to ${outPath}`);
169
+ }
170
+ main().catch((err) => {
171
+ console.error("Codegen failed:", err.message);
172
+ process.exit(1);
173
+ });
package/dist/index.cjs CHANGED
@@ -72,9 +72,24 @@ function err(error) {
72
72
  return { ok: false, data: null, error };
73
73
  }
74
74
 
75
+ // src/url.ts
76
+ function resolveURL(base, path, params) {
77
+ const root = new URL(base);
78
+ root.search = "";
79
+ root.hash = "";
80
+ if (!root.pathname.endsWith("/")) root.pathname += "/";
81
+ const url = new URL(path.replace(/^\/+/, ""), root);
82
+ if (params) {
83
+ for (const [k, v] of Object.entries(params)) {
84
+ url.searchParams.set(k, v);
85
+ }
86
+ }
87
+ return url;
88
+ }
89
+
75
90
  // src/http.ts
76
91
  async function request(ctx, opts) {
77
- const url = buildURL(ctx.baseURL, opts.path, opts.params);
92
+ const url = resolveURL(ctx.baseURL, opts.path, opts.params).toString();
78
93
  const headers = {
79
94
  "Content-Type": opts.contentType ?? "application/json",
80
95
  Accept: "application/json"
@@ -133,15 +148,6 @@ async function request(ctx, opts) {
133
148
  }
134
149
  return { data: null, error: lastError, headers: new Headers() };
135
150
  }
136
- function buildURL(base, path, params) {
137
- const url = new URL(path, base.endsWith("/") ? base : `${base}/`);
138
- if (params) {
139
- for (const [k, v] of Object.entries(params)) {
140
- url.searchParams.set(k, v);
141
- }
142
- }
143
- return url.toString();
144
- }
145
151
  function backoff(attempt) {
146
152
  return Math.min(1e3 * 2 ** attempt, 3e4);
147
153
  }
@@ -558,7 +564,7 @@ var SSETransport = class {
558
564
  this.onStatus?.("closed");
559
565
  }
560
566
  async _doConnect() {
561
- const url = new URL("/v1/stream", this._opts.baseURL);
567
+ const url = resolveURL(this._opts.baseURL, "/v1/stream");
562
568
  url.searchParams.set("table", this._opts.table);
563
569
  if (this._opts.since) {
564
570
  url.searchParams.set("since", this._opts.since);
@@ -778,7 +784,7 @@ var QueryBuilder = class _QueryBuilder {
778
784
  return this._clone({ cacheTTL: seconds });
779
785
  }
780
786
  // --- Execution ---
781
- /** Default row limit when none is specified. Matches backend DefaultMaxRows. */
787
+ /** Default row limit when none is specified — deliberately tighter than the backend's DefaultMaxRows (10000) safety cap. */
782
788
  static DEFAULT_LIMIT = 1e3;
783
789
  async fetch(opts) {
784
790
  const effectiveLimit = opts?.limit ?? this._state.limit ?? _QueryBuilder.DEFAULT_LIMIT;