@wavehouse/sdk 0.0.0-dev.0f8826c → 0.0.0-dev.h514be5522d89

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,40 @@ 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
+ ## Use without a build step (CDN)
14
+
15
+ 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.
16
+
17
+ **ES module (recommended).** Modern browsers run `type="module"` natively:
18
+
19
+ ```html
20
+ <script type="module">
21
+ import { createClient } from 'https://esm.sh/@wavehouse/sdk';
22
+
23
+ const wh = createClient({ baseURL: 'https://your-wavehouse.example.com' });
24
+ const { data, error } = await wh.from('clicks').select('page').limit(10);
25
+ console.log(data ?? error);
26
+ </script>
27
+ ```
28
+
29
+ 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.
30
+
31
+ **Classic global (`<script src>`).** For pages that can't use ES modules, the bundled IIFE build attaches everything to a `WaveHouse` global:
32
+
33
+ ```html
34
+ <script src="https://cdn.jsdelivr.net/npm/@wavehouse/sdk"></script>
35
+ <script>
36
+ const wh = WaveHouse.createClient({ baseURL: 'https://your-wavehouse.example.com' });
37
+ wh.from('clicks').select('page').limit(10).then(({ data }) => console.log(data));
38
+ </script>
39
+ ```
40
+
41
+ **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.
42
+
43
+ Streaming (`.stream()`) uses the browser's native `EventSource`, so it works in both forms with no polyfill.
44
+
11
45
  ## Quick Start
12
46
 
13
47
  ```ts
@@ -102,13 +136,13 @@ if (error) {
102
136
 
103
137
  ## Codegen
104
138
 
105
- Generate TypeScript types from your live WaveHouse schema:
139
+ 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
140
 
107
141
  ```bash
108
- npm run codegen -- --url http://localhost:8080 --out ./src/db.d.ts
142
+ npx wavehouse-codegen --url http://localhost:8080 --out ./src/db.d.ts
109
143
  ```
110
144
 
111
- This introspects `/v1/schema`, maps ClickHouse column types to TypeScript, and outputs a `Database` interface you can pass to `createClient<Database>()`.
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.
112
146
 
113
147
  ## Development & Testing
114
148
 
@@ -124,13 +158,12 @@ npm run test:coverage # coverage report
124
158
 
125
159
  ### E2E Integration Tests
126
160
 
127
- E2E tests live in `tests/sdk/` (repo root) and exercise the full pipeline through the SDK. See `make test-e2e` in the [Development Guide](../../docs/development.md#e2e-tests-via-sdk).
161
+ E2E tests live in `tests/e2e/sdk/` (repo root) and exercise the full pipeline through the SDK. See `make test-e2e` in the [Development Guide](https://wavehouse.dev/development#e2e-tests-via-sdk).
128
162
 
129
163
  ## API Reference
130
164
 
131
- See the full [SDK API Reference](../../docs/sdk.md) for detailed documentation of every method, type, and option.
165
+ See the full [SDK API Reference](https://wavehouse.dev/sdk) for detailed documentation of every method, type, and option.
132
166
 
133
167
  ## License
134
168
 
135
169
  Apache-2.0 © Wave RF — see [LICENSE](./LICENSE).
136
-
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/codegen.ts
4
+ function parseArgs(argv) {
5
+ const args = { url: "http://localhost:8080", out: "./wavehouse.d.ts" };
6
+ for (let i = 2; i < argv.length; i++) {
7
+ switch (argv[i]) {
8
+ case "--url":
9
+ case "-u":
10
+ args.url = argv[++i];
11
+ break;
12
+ case "--out":
13
+ case "-o":
14
+ args.out = argv[++i];
15
+ break;
16
+ case "--auth":
17
+ case "-a":
18
+ args.auth = argv[++i];
19
+ break;
20
+ case "--help":
21
+ case "-h":
22
+ console.log(`wavehouse codegen \u2014 Generate TypeScript types from WaveHouse schema
23
+
24
+ Options:
25
+ --url, -u WaveHouse base URL (default: http://localhost:8080)
26
+ --out, -o Output file path (default: ./wavehouse.d.ts)
27
+ --auth, -a Bearer token for authenticated endpoints
28
+ --help, -h Show this help`);
29
+ process.exit(0);
30
+ }
31
+ }
32
+ return args;
33
+ }
34
+ function chTypeToTS(chType) {
35
+ if (chType.startsWith("Nullable(") && chType.endsWith(")")) {
36
+ const inner = chType.slice(9, -1);
37
+ return `${chTypeToTS(inner)} | null`;
38
+ }
39
+ if (chType.startsWith("LowCardinality(") && chType.endsWith(")")) {
40
+ return chTypeToTS(chType.slice(15, -1));
41
+ }
42
+ if (chType === "String" || chType.startsWith("FixedString(") || chType === "UUID" || chType.startsWith("DateTime") || chType.startsWith("Date") || chType.startsWith("Enum8(") || chType.startsWith("Enum16(") || chType === "IPv4" || chType === "IPv6") {
43
+ return "string";
44
+ }
45
+ if (chType === "Bool") return "boolean";
46
+ if (isNumeric(chType)) return "number";
47
+ if (chType.startsWith("Array(") && chType.endsWith(")")) {
48
+ const inner = chType.slice(6, -1);
49
+ return `${chTypeToTS(inner)}[]`;
50
+ }
51
+ if (chType.startsWith("Map(") && chType.endsWith(")")) {
52
+ const inner = chType.slice(4, -1);
53
+ const comma = findTopLevelComma(inner);
54
+ if (comma !== -1) {
55
+ const keyType = chTypeToTS(inner.slice(0, comma).trim());
56
+ const valType = chTypeToTS(inner.slice(comma + 1).trim());
57
+ return `Record<${keyType}, ${valType}>`;
58
+ }
59
+ return "Record<string, unknown>";
60
+ }
61
+ if (chType.startsWith("Tuple(")) {
62
+ return "unknown[]";
63
+ }
64
+ return "unknown";
65
+ }
66
+ function isNumeric(t) {
67
+ const numericPrefixes = [
68
+ "UInt8",
69
+ "UInt16",
70
+ "UInt32",
71
+ "UInt64",
72
+ "UInt128",
73
+ "UInt256",
74
+ "Int8",
75
+ "Int16",
76
+ "Int32",
77
+ "Int64",
78
+ "Int128",
79
+ "Int256",
80
+ "Float32",
81
+ "Float64",
82
+ "Decimal"
83
+ ];
84
+ return numericPrefixes.some((p) => t === p || t.startsWith(`${p}(`));
85
+ }
86
+ function findTopLevelComma(s) {
87
+ let depth = 0;
88
+ for (let i = 0; i < s.length; i++) {
89
+ if (s[i] === "(") depth++;
90
+ else if (s[i] === ")") depth--;
91
+ else if (s[i] === "," && depth === 0) return i;
92
+ }
93
+ return -1;
94
+ }
95
+ async function fetchSchemas(url, auth) {
96
+ const headers = {};
97
+ if (auth) headers.Authorization = `Bearer ${auth}`;
98
+ const res = await fetch(`${url.replace(/\/+$/, "")}/v1/schema`, { headers });
99
+ if (!res.ok) {
100
+ const text = await res.text();
101
+ throw new Error(`Schema fetch failed (${res.status}): ${text}`);
102
+ }
103
+ return await res.json();
104
+ }
105
+ function generateTypes(schemas) {
106
+ const lines = [
107
+ "// Auto-generated by @wavehouse/sdk codegen",
108
+ `// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
109
+ "// Do not edit manually \u2014 re-run: npm run codegen",
110
+ "",
111
+ "export interface Database {"
112
+ ];
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};`);
118
+ }
119
+ lines.push("}");
120
+ lines.push("");
121
+ for (const tableName of tableNames) {
122
+ const schema = schemas[tableName];
123
+ const rowType = `${pascalCase(tableName)}Row`;
124
+ lines.push(`export interface ${rowType} {`);
125
+ for (const col of schema.columns) {
126
+ const tsType = chTypeToTS(col.type);
127
+ const optional = col.has_default ? "?" : "";
128
+ lines.push(` ${col.name}${optional}: ${tsType};`);
129
+ }
130
+ lines.push("}");
131
+ lines.push("");
132
+ }
133
+ return lines.join("\n");
134
+ }
135
+ function pascalCase(s) {
136
+ return s.split(/[_\-\s]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
137
+ }
138
+ async function main() {
139
+ const args = parseArgs(process.argv);
140
+ console.log(`Fetching schema from ${args.url}...`);
141
+ const schemas = await fetchSchemas(args.url, args.auth);
142
+ const tableCount = Object.keys(schemas).length;
143
+ if (tableCount === 0) {
144
+ console.warn("No tables found. Is WaveHouse running with tables in ClickHouse?");
145
+ process.exit(1);
146
+ }
147
+ console.log(`Found ${tableCount} table(s): ${Object.keys(schemas).join(", ")}`);
148
+ const output = generateTypes(schemas);
149
+ const { writeFile } = await import("fs/promises");
150
+ const { resolve } = await import("path");
151
+ const outPath = resolve(args.out);
152
+ await writeFile(outPath, output, "utf-8");
153
+ console.log(`\u2713 Types written to ${outPath}`);
154
+ }
155
+ main().catch((err) => {
156
+ console.error("Codegen failed:", err.message);
157
+ process.exit(1);
158
+ });
package/dist/index.cjs CHANGED
@@ -76,9 +76,10 @@ function err(error) {
76
76
  async function request(ctx, opts) {
77
77
  const url = buildURL(ctx.baseURL, opts.path, opts.params);
78
78
  const headers = {
79
- "Content-Type": "application/json",
79
+ "Content-Type": opts.contentType ?? "application/json",
80
80
  Accept: "application/json"
81
81
  };
82
+ const requestBody = opts.rawBody !== void 0 ? opts.rawBody : opts.body !== void 0 ? JSON.stringify(opts.body) : void 0;
82
83
  if (ctx.auth) {
83
84
  const token = await ctx.auth();
84
85
  if (token) {
@@ -92,7 +93,7 @@ async function request(ctx, opts) {
92
93
  const res = await fetch(url, {
93
94
  method: opts.method,
94
95
  headers,
95
- body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0,
96
+ body: requestBody,
96
97
  signal: opts.signal
97
98
  });
98
99
  if (res.ok) {
@@ -727,6 +728,15 @@ var QueryBuilder = class _QueryBuilder {
727
728
  select(...columns) {
728
729
  return this._clone({ columns: [...this._state.columns, ...columns] });
729
730
  }
731
+ /**
732
+ * Select every column the caller's role is allowed to read (the all-columns
733
+ * wildcard). Use this instead of `.select(...)` when you want a full-row read;
734
+ * a bare `.fetch()` with no `.select()` does this implicitly. Mutually
735
+ * exclusive with `.select(...)`.
736
+ */
737
+ selectAll() {
738
+ return this._clone({ selectAll: true });
739
+ }
730
740
  where(column, op, value) {
731
741
  const filter = { column, op: OP_MAP[op], value };
732
742
  return this._clone({ filters: [...this._state.filters, filter] });
@@ -768,7 +778,7 @@ var QueryBuilder = class _QueryBuilder {
768
778
  return this._clone({ cacheTTL: seconds });
769
779
  }
770
780
  // --- Execution ---
771
- /** Default row limit when none is specified. Matches backend DefaultMaxRows. */
781
+ /** Default row limit when none is specified deliberately tighter than the backend's DefaultMaxRows (10000) safety cap. */
772
782
  static DEFAULT_LIMIT = 1e3;
773
783
  async fetch(opts) {
774
784
  const effectiveLimit = opts?.limit ?? this._state.limit ?? _QueryBuilder.DEFAULT_LIMIT;
@@ -782,11 +792,11 @@ var QueryBuilder = class _QueryBuilder {
782
792
  if (error) return err(error);
783
793
  const rows = data;
784
794
  const hasMore = effectiveLimit != null && rows.length >= effectiveLimit;
785
- if (hasMore) {
795
+ if (hasMore && this._state.orderBy.length > 0) {
786
796
  const nextFn = () => this._fetchNext(rows, effectiveLimit, opts);
787
797
  return okPage(rows, true, nextFn);
788
798
  }
789
- return okPage(rows, false);
799
+ return okPage(rows, hasMore);
790
800
  }
791
801
  stream(opts) {
792
802
  const raw = this._createStream(this._state.table, opts);
@@ -825,31 +835,35 @@ var QueryBuilder = class _QueryBuilder {
825
835
  }
826
836
  _buildAST(effectiveLimit) {
827
837
  const ast = {};
828
- if (this._state.columns.length > 0) ast.columns = this._state.columns;
829
- if (this._state.aggregations.length > 0) ast.aggregations = this._state.aggregations;
838
+ const hasColumns = this._state.columns.length > 0;
839
+ const hasAggs = this._state.aggregations.length > 0;
840
+ if (this._state.selectAll) {
841
+ ast.select_all = true;
842
+ } else if (hasColumns) {
843
+ ast.columns = this._state.columns;
844
+ } else if (!hasAggs) {
845
+ ast.select_all = true;
846
+ }
847
+ if (hasAggs) ast.aggregations = this._state.aggregations;
830
848
  if (this._state.filters.length > 0) ast.filters = this._state.filters;
831
849
  if (this._state.groupBy.length > 0) ast.group_by = this._state.groupBy;
832
- if (this._state.orderBy.length > 0) {
833
- ast.order_by = this._state.orderBy;
834
- } else if (effectiveLimit != null && this._state.aggregations.length === 0) {
835
- ast.order_by = [{ column: "received_timestamp", dir: "desc" }];
836
- }
850
+ if (this._state.orderBy.length > 0) ast.order_by = this._state.orderBy;
837
851
  if (effectiveLimit != null) ast.limit = effectiveLimit;
838
852
  if (this._state.timeRange) ast.time_range = this._state.timeRange;
839
853
  return ast;
840
854
  }
841
855
  async _fetchNext(prevRows, _limit, opts) {
842
- const orderCol = this._state.orderBy[0]?.column ?? "received_timestamp";
843
- const orderDir = this._state.orderBy[0]?.dir ?? "desc";
856
+ const cursor = this._state.orderBy[0];
857
+ if (cursor == null) return okPage([], false);
858
+ const { column: orderCol, dir: orderDir } = cursor;
844
859
  const lastRow = prevRows[prevRows.length - 1];
845
860
  const lastValue = lastRow?.[orderCol];
846
861
  if (lastValue === void 0) return okPage([], false);
847
862
  const cursorOp = orderDir === "desc" ? "lt" : "gt";
848
863
  const cursorFilter = { column: orderCol, op: cursorOp, value: lastValue };
849
- const orderBy = this._state.orderBy.length > 0 ? this._state.orderBy : [{ column: orderCol, dir: orderDir }];
850
864
  const nextBuilder = this._clone({
851
865
  filters: [...this._state.filters, cursorFilter],
852
- orderBy
866
+ orderBy: this._state.orderBy
853
867
  });
854
868
  return nextBuilder.fetch(opts);
855
869
  }
@@ -940,6 +954,12 @@ function projectColumns(row, columns) {
940
954
  }
941
955
 
942
956
  // src/table.ts
957
+ var NDJSON_CONTENT_TYPE = "application/x-ndjson";
958
+ async function ndjsonSourceToString(source) {
959
+ if (typeof source === "string") return source;
960
+ if (source instanceof Uint8Array) return new TextDecoder().decode(source);
961
+ return new Response(source).text();
962
+ }
943
963
  var TableRef = class {
944
964
  _ctx;
945
965
  _table;
@@ -968,22 +988,34 @@ var TableRef = class {
968
988
  this._createStream
969
989
  );
970
990
  }
971
- /** Insert one or more rows into this table. */
991
+ /**
992
+ * Start a query that selects every column the caller's role is allowed to read
993
+ * (the all-columns wildcard). Use instead of `.select(...)` for an explicit
994
+ * full-row read; `.fetch()` does this implicitly.
995
+ */
996
+ selectAll() {
997
+ return this.select().selectAll();
998
+ }
999
+ /**
1000
+ * Insert one or more rows into this table.
1001
+ *
1002
+ * A single object is sent as a JSON `POST /v1/ingest`. An **array** is
1003
+ * serialized to NDJSON (one record per line) and sent as a single
1004
+ * `application/x-ndjson` request: a bad record no longer fails or hides the
1005
+ * rest of the batch — per-record outcomes come back in the result
1006
+ * (`failed` / `results`), and `ok` is true only when every record succeeded.
1007
+ *
1008
+ * The array path sends one request regardless of size; bounded-concurrency
1009
+ * chunking of very large arrays is tracked separately (#196). For NDJSON you
1010
+ * already have (a file or stream), use {@link insertNDJSON}.
1011
+ */
972
1012
  async insert(data, opts) {
973
1013
  if (Array.isArray(data)) {
974
- const promises = data.map(
975
- (row) => request(this._ctx, {
976
- method: "POST",
977
- path: `/v1/ingest?table=${encodeURIComponent(this._table)}`,
978
- body: row,
979
- signal: opts?.signal
980
- })
981
- );
982
- const results = await Promise.all(promises);
983
- for (const res2 of results) {
984
- if (res2.error) return err(res2.error);
1014
+ if (data.length === 0) {
1015
+ return ok({ ok: true, total: 0, succeeded: 0, failed: 0, duplicates: 0, results: [] });
985
1016
  }
986
- return ok({ ok: true });
1017
+ const ndjson = data.map((row) => JSON.stringify(row)).join("\n");
1018
+ return this._sendNDJSON(ndjson, opts);
987
1019
  }
988
1020
  const { data: res, error } = await request(this._ctx, {
989
1021
  method: "POST",
@@ -996,6 +1028,46 @@ var TableRef = class {
996
1028
  if (res?.duplicate != null) result.duplicate = res.duplicate;
997
1029
  return ok(result);
998
1030
  }
1031
+ /**
1032
+ * Insert pre-formatted NDJSON (newline-delimited JSON, one record per line)
1033
+ * from a string, raw bytes, a Blob/File, or a byte stream — e.g. a `.ndjson`
1034
+ * file or a stream produced by another system. For in-memory rows, prefer
1035
+ * {@link insert}.
1036
+ *
1037
+ * Non-string sources are read fully into memory before sending (the server
1038
+ * streams the parse). Returns the same per-record batch summary as an array
1039
+ * `insert`.
1040
+ */
1041
+ async insertNDJSON(source, opts) {
1042
+ const ndjson = await ndjsonSourceToString(source);
1043
+ return this._sendNDJSON(ndjson, opts);
1044
+ }
1045
+ /**
1046
+ * @internal Send an NDJSON body and map the server's batch response onto an
1047
+ * {@link InsertResult}. A transport / whole-request failure surfaces as the
1048
+ * Result error arm; a processed batch (even with rejected records) surfaces
1049
+ * as the success arm with `ok = failed === 0`.
1050
+ */
1051
+ async _sendNDJSON(ndjson, opts) {
1052
+ const { data, error } = await request(this._ctx, {
1053
+ method: "POST",
1054
+ path: `/v1/ingest?table=${encodeURIComponent(this._table)}`,
1055
+ rawBody: ndjson,
1056
+ contentType: NDJSON_CONTENT_TYPE,
1057
+ signal: opts?.signal
1058
+ });
1059
+ if (error) return err(error);
1060
+ const r = data ?? { total: 0, succeeded: 0, failed: 0, duplicates: 0 };
1061
+ const result = {
1062
+ ok: r.failed === 0,
1063
+ total: r.total,
1064
+ succeeded: r.succeeded,
1065
+ failed: r.failed,
1066
+ duplicates: r.duplicates
1067
+ };
1068
+ if (r.results && r.results.length > 0) result.results = r.results;
1069
+ return ok(result);
1070
+ }
999
1071
  /** Fetch the schema for this table. */
1000
1072
  async schema(opts) {
1001
1073
  const { data, error } = await request(this._ctx, {