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

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,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. Browsers need no polyfill, and Node needs an `EventSource` polyfill only for streaming — 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()`) uses the browser's native `EventSource`, 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 `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
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/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
154
 
113
155
  ## Development & Testing
114
156
 
@@ -124,13 +166,12 @@ npm run test:coverage # coverage report
124
166
 
125
167
  ### E2E Integration Tests
126
168
 
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).
169
+ 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
170
 
129
171
  ## API Reference
130
172
 
131
- See the full [SDK API Reference](../../docs/sdk.md) for detailed documentation of every method, type, and option.
173
+ See the full [SDK API Reference](https://wavehouse.dev/sdk) for detailed documentation of every method, type, and option.
132
174
 
133
175
  ## License
134
176
 
135
177
  Apache-2.0 © Wave RF — see [LICENSE](./LICENSE).
136
-
@@ -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,29 +72,64 @@ 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
- "Content-Type": "application/json",
109
+ "Content-Type": opts.contentType ?? "application/json",
80
110
  Accept: "application/json"
81
111
  };
112
+ const requestBody = opts.rawBody !== void 0 ? opts.rawBody : opts.body !== void 0 ? JSON.stringify(opts.body) : void 0;
82
113
  if (ctx.auth) {
83
114
  const token = await ctx.auth();
84
115
  if (token) {
85
116
  headers.Authorization = `Bearer ${token}`;
86
117
  }
87
118
  }
119
+ const finalHeaders = mergeHeaders(headers, ctx.options.headers);
88
120
  let lastError = null;
89
121
  const maxAttempts = ctx.options.maxRetries + 1;
90
122
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
91
123
  try {
92
- const res = await fetch(url, {
124
+ const init = {
125
+ ...ctx.options.fetchOptions,
93
126
  method: opts.method,
94
- headers,
95
- body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0,
127
+ headers: finalHeaders,
128
+ body: requestBody,
96
129
  signal: opts.signal
97
- });
130
+ };
131
+ const doFetch = ctx.options.fetch;
132
+ const res = doFetch ? await doFetch(url, init) : await fetch(url, init);
98
133
  if (res.ok) {
99
134
  const text = await res.text();
100
135
  const data = text ? JSON.parse(text) : void 0;
@@ -132,15 +167,6 @@ async function request(ctx, opts) {
132
167
  }
133
168
  return { data: null, error: lastError, headers: new Headers() };
134
169
  }
135
- function buildURL(base, path, params) {
136
- const url = new URL(path, base.endsWith("/") ? base : `${base}/`);
137
- if (params) {
138
- for (const [k, v] of Object.entries(params)) {
139
- url.searchParams.set(k, v);
140
- }
141
- }
142
- return url.toString();
143
- }
144
170
  function backoff(attempt) {
145
171
  return Math.min(1e3 * 2 ** attempt, 3e4);
146
172
  }
@@ -209,7 +235,14 @@ var PipeRef = class {
209
235
  this._params = params;
210
236
  this._createStream = createStream;
211
237
  }
212
- /** 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
+ */
213
246
  async fetch(opts) {
214
247
  const { data, error } = await request(this._ctx, {
215
248
  method: "POST",
@@ -557,7 +590,7 @@ var SSETransport = class {
557
590
  this.onStatus?.("closed");
558
591
  }
559
592
  async _doConnect() {
560
- const url = new URL("/v1/stream", this._opts.baseURL);
593
+ const url = resolveURL(this._opts.baseURL, "/v1/stream");
561
594
  url.searchParams.set("table", this._opts.table);
562
595
  if (this._opts.since) {
563
596
  url.searchParams.set("since", this._opts.since);
@@ -727,6 +760,15 @@ var QueryBuilder = class _QueryBuilder {
727
760
  select(...columns) {
728
761
  return this._clone({ columns: [...this._state.columns, ...columns] });
729
762
  }
763
+ /**
764
+ * Select every column the caller's role is allowed to read (the all-columns
765
+ * wildcard). Use this instead of `.select(...)` when you want a full-row read;
766
+ * a bare `.fetch()` with no `.select()` does this implicitly. Mutually
767
+ * exclusive with `.select(...)`.
768
+ */
769
+ selectAll() {
770
+ return this._clone({ selectAll: true });
771
+ }
730
772
  where(column, op, value) {
731
773
  const filter = { column, op: OP_MAP[op], value };
732
774
  return this._clone({ filters: [...this._state.filters, filter] });
@@ -768,7 +810,7 @@ var QueryBuilder = class _QueryBuilder {
768
810
  return this._clone({ cacheTTL: seconds });
769
811
  }
770
812
  // --- Execution ---
771
- /** Default row limit when none is specified. Matches backend DefaultMaxRows. */
813
+ /** Default row limit when none is specified — deliberately tighter than the backend's DefaultMaxRows (10000) safety cap. */
772
814
  static DEFAULT_LIMIT = 1e3;
773
815
  async fetch(opts) {
774
816
  const effectiveLimit = opts?.limit ?? this._state.limit ?? _QueryBuilder.DEFAULT_LIMIT;
@@ -782,11 +824,11 @@ var QueryBuilder = class _QueryBuilder {
782
824
  if (error) return err(error);
783
825
  const rows = data;
784
826
  const hasMore = effectiveLimit != null && rows.length >= effectiveLimit;
785
- if (hasMore) {
827
+ if (hasMore && this._state.orderBy.length > 0) {
786
828
  const nextFn = () => this._fetchNext(rows, effectiveLimit, opts);
787
829
  return okPage(rows, true, nextFn);
788
830
  }
789
- return okPage(rows, false);
831
+ return okPage(rows, hasMore);
790
832
  }
791
833
  stream(opts) {
792
834
  const raw = this._createStream(this._state.table, opts);
@@ -825,31 +867,35 @@ var QueryBuilder = class _QueryBuilder {
825
867
  }
826
868
  _buildAST(effectiveLimit) {
827
869
  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;
870
+ const hasColumns = this._state.columns.length > 0;
871
+ const hasAggs = this._state.aggregations.length > 0;
872
+ if (this._state.selectAll) {
873
+ ast.select_all = true;
874
+ } else if (hasColumns) {
875
+ ast.columns = this._state.columns;
876
+ } else if (!hasAggs) {
877
+ ast.select_all = true;
878
+ }
879
+ if (hasAggs) ast.aggregations = this._state.aggregations;
830
880
  if (this._state.filters.length > 0) ast.filters = this._state.filters;
831
881
  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
- }
882
+ if (this._state.orderBy.length > 0) ast.order_by = this._state.orderBy;
837
883
  if (effectiveLimit != null) ast.limit = effectiveLimit;
838
884
  if (this._state.timeRange) ast.time_range = this._state.timeRange;
839
885
  return ast;
840
886
  }
841
887
  async _fetchNext(prevRows, _limit, opts) {
842
- const orderCol = this._state.orderBy[0]?.column ?? "received_timestamp";
843
- const orderDir = this._state.orderBy[0]?.dir ?? "desc";
888
+ const cursor = this._state.orderBy[0];
889
+ if (cursor == null) return okPage([], false);
890
+ const { column: orderCol, dir: orderDir } = cursor;
844
891
  const lastRow = prevRows[prevRows.length - 1];
845
892
  const lastValue = lastRow?.[orderCol];
846
893
  if (lastValue === void 0) return okPage([], false);
847
894
  const cursorOp = orderDir === "desc" ? "lt" : "gt";
848
895
  const cursorFilter = { column: orderCol, op: cursorOp, value: lastValue };
849
- const orderBy = this._state.orderBy.length > 0 ? this._state.orderBy : [{ column: orderCol, dir: orderDir }];
850
896
  const nextBuilder = this._clone({
851
897
  filters: [...this._state.filters, cursorFilter],
852
- orderBy
898
+ orderBy: this._state.orderBy
853
899
  });
854
900
  return nextBuilder.fetch(opts);
855
901
  }
@@ -940,6 +986,12 @@ function projectColumns(row, columns) {
940
986
  }
941
987
 
942
988
  // src/table.ts
989
+ var NDJSON_CONTENT_TYPE = "application/x-ndjson";
990
+ async function ndjsonSourceToString(source) {
991
+ if (typeof source === "string") return source;
992
+ if (source instanceof Uint8Array) return new TextDecoder().decode(source);
993
+ return new Response(source).text();
994
+ }
943
995
  var TableRef = class {
944
996
  _ctx;
945
997
  _table;
@@ -968,22 +1020,34 @@ var TableRef = class {
968
1020
  this._createStream
969
1021
  );
970
1022
  }
971
- /** Insert one or more rows into this table. */
1023
+ /**
1024
+ * Start a query that selects every column the caller's role is allowed to read
1025
+ * (the all-columns wildcard). Use instead of `.select(...)` for an explicit
1026
+ * full-row read; `.fetch()` does this implicitly.
1027
+ */
1028
+ selectAll() {
1029
+ return this.select().selectAll();
1030
+ }
1031
+ /**
1032
+ * Insert one or more rows into this table.
1033
+ *
1034
+ * A single object is sent as a JSON `POST /v1/ingest`. An **array** is
1035
+ * serialized to NDJSON (one record per line) and sent as a single
1036
+ * `application/x-ndjson` request: a bad record no longer fails or hides the
1037
+ * rest of the batch — per-record outcomes come back in the result
1038
+ * (`failed` / `results`), and `ok` is true only when every record succeeded.
1039
+ *
1040
+ * The array path sends one request regardless of size; bounded-concurrency
1041
+ * chunking of very large arrays is tracked separately (#196). For NDJSON you
1042
+ * already have (a file or stream), use {@link insertNDJSON}.
1043
+ */
972
1044
  async insert(data, opts) {
973
1045
  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);
1046
+ if (data.length === 0) {
1047
+ return ok({ ok: true, total: 0, succeeded: 0, failed: 0, duplicates: 0, results: [] });
985
1048
  }
986
- return ok({ ok: true });
1049
+ const ndjson = data.map((row) => JSON.stringify(row)).join("\n");
1050
+ return this._sendNDJSON(ndjson, opts);
987
1051
  }
988
1052
  const { data: res, error } = await request(this._ctx, {
989
1053
  method: "POST",
@@ -996,6 +1060,46 @@ var TableRef = class {
996
1060
  if (res?.duplicate != null) result.duplicate = res.duplicate;
997
1061
  return ok(result);
998
1062
  }
1063
+ /**
1064
+ * Insert pre-formatted NDJSON (newline-delimited JSON, one record per line)
1065
+ * from a string, raw bytes, a Blob/File, or a byte stream — e.g. a `.ndjson`
1066
+ * file or a stream produced by another system. For in-memory rows, prefer
1067
+ * {@link insert}.
1068
+ *
1069
+ * Non-string sources are read fully into memory before sending (the server
1070
+ * streams the parse). Returns the same per-record batch summary as an array
1071
+ * `insert`.
1072
+ */
1073
+ async insertNDJSON(source, opts) {
1074
+ const ndjson = await ndjsonSourceToString(source);
1075
+ return this._sendNDJSON(ndjson, opts);
1076
+ }
1077
+ /**
1078
+ * @internal Send an NDJSON body and map the server's batch response onto an
1079
+ * {@link InsertResult}. A transport / whole-request failure surfaces as the
1080
+ * Result error arm; a processed batch (even with rejected records) surfaces
1081
+ * as the success arm with `ok = failed === 0`.
1082
+ */
1083
+ async _sendNDJSON(ndjson, opts) {
1084
+ const { data, error } = await request(this._ctx, {
1085
+ method: "POST",
1086
+ path: `/v1/ingest?table=${encodeURIComponent(this._table)}`,
1087
+ rawBody: ndjson,
1088
+ contentType: NDJSON_CONTENT_TYPE,
1089
+ signal: opts?.signal
1090
+ });
1091
+ if (error) return err(error);
1092
+ const r = data ?? { total: 0, succeeded: 0, failed: 0, duplicates: 0 };
1093
+ const result = {
1094
+ ok: r.failed === 0,
1095
+ total: r.total,
1096
+ succeeded: r.succeeded,
1097
+ failed: r.failed,
1098
+ duplicates: r.duplicates
1099
+ };
1100
+ if (r.results && r.results.length > 0) result.results = r.results;
1101
+ return ok(result);
1102
+ }
999
1103
  /** Fetch the schema for this table. */
1000
1104
  async schema(opts) {
1001
1105
  const { data, error } = await request(this._ctx, {
@@ -1031,7 +1135,10 @@ var WaveHouseClient = class {
1031
1135
  baseURL: config.baseURL.replace(/\/+$/, ""),
1032
1136
  auth: config.auth,
1033
1137
  options: {
1034
- 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
1035
1142
  }
1036
1143
  };
1037
1144
  this.schema = new SchemaNamespace(this._ctx);