@wavehouse/sdk 0.0.0-dev.h514be5522d89 → 0.0.0-dev.h70dbf5d8f5ed

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,8 +8,12 @@ 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. Browsers need no polyfill, and Node needs an `EventSource` polyfill only for streaming — 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
 
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
+
13
17
  ## Use without a build step (CDN)
14
18
 
15
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.
@@ -53,6 +57,8 @@ const wh = createClient({
53
57
  });
54
58
  ```
55
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
+
56
62
  ### Query Data
57
63
 
58
64
  ```ts
@@ -123,7 +129,9 @@ const { data } = await wh.from('clicks').select('page').limit(10);
123
129
 
124
130
  ## Error Handling
125
131
 
126
- 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 `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.
127
135
 
128
136
  ```ts
129
137
  const { data, error } = await wh.from('clicks').fetch();
@@ -142,7 +150,7 @@ Generate TypeScript types from your live WaveHouse schema. The package ships a `
142
150
  npx wavehouse-codegen --url http://localhost:8080 --out ./src/db.d.ts
143
151
  ```
144
152
 
145
- 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.
146
154
 
147
155
  ## Development & Testing
148
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
+ };
package/dist/index.cjs CHANGED
@@ -72,9 +72,39 @@ 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
91
+ function mergeHeaders(base, extra) {
92
+ if (!extra) return base;
93
+ const sdkNames = new Set(Object.keys(base).map((k) => k.toLowerCase()));
94
+ const merged = { ...base };
95
+ const configured = /* @__PURE__ */ new Map();
96
+ for (const [name, value] of Object.entries(extra)) {
97
+ const lower = name.toLowerCase();
98
+ if (sdkNames.has(lower)) continue;
99
+ const prior = configured.get(lower);
100
+ if (prior !== void 0) delete merged[prior];
101
+ merged[name] = value;
102
+ configured.set(lower, name);
103
+ }
104
+ return merged;
105
+ }
76
106
  async function request(ctx, opts) {
77
- const url = buildURL(ctx.baseURL, opts.path, opts.params);
107
+ const url = resolveURL(ctx.baseURL, opts.path, opts.params).toString();
78
108
  const headers = {
79
109
  "Content-Type": opts.contentType ?? "application/json",
80
110
  Accept: "application/json"
@@ -86,16 +116,20 @@ async function request(ctx, opts) {
86
116
  headers.Authorization = `Bearer ${token}`;
87
117
  }
88
118
  }
119
+ const finalHeaders = mergeHeaders(headers, ctx.options.headers);
89
120
  let lastError = null;
90
121
  const maxAttempts = ctx.options.maxRetries + 1;
91
122
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
92
123
  try {
93
- const res = await fetch(url, {
124
+ const init = {
125
+ ...ctx.options.fetchOptions,
94
126
  method: opts.method,
95
- headers,
127
+ headers: finalHeaders,
96
128
  body: requestBody,
97
129
  signal: opts.signal
98
- });
130
+ };
131
+ const doFetch = ctx.options.fetch;
132
+ const res = doFetch ? await doFetch(url, init) : await fetch(url, init);
99
133
  if (res.ok) {
100
134
  const text = await res.text();
101
135
  const data = text ? JSON.parse(text) : void 0;
@@ -133,15 +167,6 @@ async function request(ctx, opts) {
133
167
  }
134
168
  return { data: null, error: lastError, headers: new Headers() };
135
169
  }
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
170
  function backoff(attempt) {
146
171
  return Math.min(1e3 * 2 ** attempt, 3e4);
147
172
  }
@@ -175,7 +200,7 @@ var DLQNamespace = class {
175
200
  async list(opts) {
176
201
  const { data, error } = await request(this._ctx, {
177
202
  method: "GET",
178
- path: "/v1/dlq/stats",
203
+ path: "/v1/ops/dlq/stats",
179
204
  signal: opts?.signal
180
205
  });
181
206
  if (error) return err(error);
@@ -185,7 +210,7 @@ var DLQNamespace = class {
185
210
  async table(name, opts) {
186
211
  const { data, error } = await request(this._ctx, {
187
212
  method: "GET",
188
- path: "/v1/dlq/stats",
213
+ path: "/v1/ops/dlq/stats",
189
214
  params: { table: name },
190
215
  signal: opts?.signal
191
216
  });
@@ -210,7 +235,14 @@ var PipeRef = class {
210
235
  this._params = params;
211
236
  this._createStream = createStream;
212
237
  }
213
- /** Execute the pipe and return results. */
238
+ /**
239
+ * Execute the pipe and return results.
240
+ *
241
+ * Takes only `signal` — deliberately narrower than the `RequestOptions` the
242
+ * query builder accepts. The pipes endpoint binds the body as the pipe's
243
+ * parameters, so there is no row cap to forward; give the pipe a `{{limit}}`
244
+ * parameter in its SQL and pass it via `wh.pipe(name, { limit })`.
245
+ */
214
246
  async fetch(opts) {
215
247
  const { data, error } = await request(this._ctx, {
216
248
  method: "POST",
@@ -238,7 +270,7 @@ var PipesNamespace = class {
238
270
  async list(opts) {
239
271
  const { data, error } = await request(this._ctx, {
240
272
  method: "GET",
241
- path: "/v1/admin/pipes",
273
+ path: "/v1/ops/pipes",
242
274
  signal: opts?.signal
243
275
  });
244
276
  if (error) return err(error);
@@ -248,7 +280,7 @@ var PipesNamespace = class {
248
280
  async get(name, opts) {
249
281
  const { data, error } = await request(this._ctx, {
250
282
  method: "GET",
251
- path: `/v1/admin/pipes/${encodeURIComponent(name)}`,
283
+ path: `/v1/ops/pipes/${encodeURIComponent(name)}`,
252
284
  signal: opts?.signal
253
285
  });
254
286
  if (error) return err(error);
@@ -258,7 +290,7 @@ var PipesNamespace = class {
258
290
  async set(name, def, opts) {
259
291
  const { error } = await request(this._ctx, {
260
292
  method: "PUT",
261
- path: `/v1/admin/pipes/${encodeURIComponent(name)}`,
293
+ path: `/v1/ops/pipes/${encodeURIComponent(name)}`,
262
294
  body: def,
263
295
  signal: opts?.signal
264
296
  });
@@ -269,7 +301,7 @@ var PipesNamespace = class {
269
301
  async delete(name, opts) {
270
302
  const { error } = await request(this._ctx, {
271
303
  method: "DELETE",
272
- path: `/v1/admin/pipes/${encodeURIComponent(name)}`,
304
+ path: `/v1/ops/pipes/${encodeURIComponent(name)}`,
273
305
  signal: opts?.signal
274
306
  });
275
307
  if (error) return err(error);
@@ -287,7 +319,7 @@ var PolicyNamespace = class {
287
319
  async get(opts) {
288
320
  const { data, error } = await request(this._ctx, {
289
321
  method: "GET",
290
- path: "/v1/admin/policy",
322
+ path: "/v1/ops/policy",
291
323
  signal: opts?.signal
292
324
  });
293
325
  if (error) return err(error);
@@ -297,7 +329,7 @@ var PolicyNamespace = class {
297
329
  async set(policy, opts) {
298
330
  const { error } = await request(this._ctx, {
299
331
  method: "PUT",
300
- path: "/v1/admin/policy",
332
+ path: "/v1/ops/policy",
301
333
  body: policy,
302
334
  signal: opts?.signal
303
335
  });
@@ -308,7 +340,7 @@ var PolicyNamespace = class {
308
340
  async validate(policy, opts) {
309
341
  const { data, error } = await request(this._ctx, {
310
342
  method: "POST",
311
- path: "/v1/admin/policy/validate",
343
+ path: "/v1/ops/policy/validate",
312
344
  body: policy,
313
345
  signal: opts?.signal
314
346
  });
@@ -327,7 +359,7 @@ var SchemaNamespace = class {
327
359
  async list(opts) {
328
360
  const { data, error } = await request(this._ctx, {
329
361
  method: "GET",
330
- path: "/v1/schema",
362
+ path: "/v1/ops/schema",
331
363
  signal: opts?.signal
332
364
  });
333
365
  if (error) return err(error);
@@ -348,7 +380,7 @@ var SchemaNamespace = class {
348
380
  async refresh(opts) {
349
381
  const { error } = await request(this._ctx, {
350
382
  method: "POST",
351
- path: "/v1/schema/refresh",
383
+ path: "/v1/ops/schema/refresh",
352
384
  signal: opts?.signal
353
385
  });
354
386
  if (error) return err(error);
@@ -360,7 +392,7 @@ var SchemaNamespace = class {
360
392
  async function sql(ctx, query, opts) {
361
393
  const { data, error } = await request(ctx, {
362
394
  method: "POST",
363
- path: "/v1/admin/query",
395
+ path: "/v1/ops/query",
364
396
  body: { sql: query },
365
397
  signal: opts?.signal
366
398
  });
@@ -558,7 +590,7 @@ var SSETransport = class {
558
590
  this.onStatus?.("closed");
559
591
  }
560
592
  async _doConnect() {
561
- const url = new URL("/v1/stream", this._opts.baseURL);
593
+ const url = resolveURL(this._opts.baseURL, "/v1/stream");
562
594
  url.searchParams.set("table", this._opts.table);
563
595
  if (this._opts.since) {
564
596
  url.searchParams.set("since", this._opts.since);
@@ -1072,7 +1104,7 @@ var TableRef = class {
1072
1104
  async schema(opts) {
1073
1105
  const { data, error } = await request(this._ctx, {
1074
1106
  method: "GET",
1075
- path: `/v1/schema?table=${encodeURIComponent(this._table)}`,
1107
+ path: `/v1/ops/schema?table=${encodeURIComponent(this._table)}`,
1076
1108
  signal: opts?.signal
1077
1109
  });
1078
1110
  if (error) return err(error);
@@ -1103,7 +1135,10 @@ var WaveHouseClient = class {
1103
1135
  baseURL: config.baseURL.replace(/\/+$/, ""),
1104
1136
  auth: config.auth,
1105
1137
  options: {
1106
- maxRetries: config.options?.maxRetries ?? 2
1138
+ maxRetries: config.options?.maxRetries ?? 2,
1139
+ fetch: config.options?.fetch,
1140
+ headers: config.options?.headers,
1141
+ fetchOptions: config.options?.fetchOptions
1107
1142
  }
1108
1143
  };
1109
1144
  this.schema = new SchemaNamespace(this._ctx);
@@ -1135,7 +1170,7 @@ var WaveHouseClient = class {
1135
1170
  sql(query, opts) {
1136
1171
  if (Array.isArray(opts)) {
1137
1172
  throw new Error(
1138
- "[WaveHouse SDK] client.sql(sql, params) was removed. The /v1/admin/query endpoint does not accept positional `?` params. Inline literals into the SQL, or use the structured query builder (wh.from(table)\u2026) for safe binding from user input."
1173
+ "[WaveHouse SDK] client.sql(sql, params) was removed. The /v1/ops/query endpoint does not accept positional `?` params. Inline literals into the SQL, or use the structured query builder (wh.from(table)\u2026) for safe binding from user input."
1139
1174
  );
1140
1175
  }
1141
1176
  return sql(this._ctx, query, opts);