@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/dist/index.js CHANGED
@@ -35,29 +35,64 @@ function err(error) {
35
35
  return { ok: false, data: null, error };
36
36
  }
37
37
 
38
+ // src/url.ts
39
+ function resolveURL(base, path, params) {
40
+ const root = new URL(base);
41
+ root.search = "";
42
+ root.hash = "";
43
+ if (!root.pathname.endsWith("/")) root.pathname += "/";
44
+ const url = new URL(path.replace(/^\/+/, ""), root);
45
+ if (params) {
46
+ for (const [k, v] of Object.entries(params)) {
47
+ url.searchParams.set(k, v);
48
+ }
49
+ }
50
+ return url;
51
+ }
52
+
38
53
  // src/http.ts
54
+ function mergeHeaders(base, extra) {
55
+ if (!extra) return base;
56
+ const sdkNames = new Set(Object.keys(base).map((k) => k.toLowerCase()));
57
+ const merged = { ...base };
58
+ const configured = /* @__PURE__ */ new Map();
59
+ for (const [name, value] of Object.entries(extra)) {
60
+ const lower = name.toLowerCase();
61
+ if (sdkNames.has(lower)) continue;
62
+ const prior = configured.get(lower);
63
+ if (prior !== void 0) delete merged[prior];
64
+ merged[name] = value;
65
+ configured.set(lower, name);
66
+ }
67
+ return merged;
68
+ }
39
69
  async function request(ctx, opts) {
40
- const url = buildURL(ctx.baseURL, opts.path, opts.params);
70
+ const url = resolveURL(ctx.baseURL, opts.path, opts.params).toString();
41
71
  const headers = {
42
- "Content-Type": "application/json",
72
+ "Content-Type": opts.contentType ?? "application/json",
43
73
  Accept: "application/json"
44
74
  };
75
+ const requestBody = opts.rawBody !== void 0 ? opts.rawBody : opts.body !== void 0 ? JSON.stringify(opts.body) : void 0;
45
76
  if (ctx.auth) {
46
77
  const token = await ctx.auth();
47
78
  if (token) {
48
79
  headers.Authorization = `Bearer ${token}`;
49
80
  }
50
81
  }
82
+ const finalHeaders = mergeHeaders(headers, ctx.options.headers);
51
83
  let lastError = null;
52
84
  const maxAttempts = ctx.options.maxRetries + 1;
53
85
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
54
86
  try {
55
- const res = await fetch(url, {
87
+ const init = {
88
+ ...ctx.options.fetchOptions,
56
89
  method: opts.method,
57
- headers,
58
- body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0,
90
+ headers: finalHeaders,
91
+ body: requestBody,
59
92
  signal: opts.signal
60
- });
93
+ };
94
+ const doFetch = ctx.options.fetch;
95
+ const res = doFetch ? await doFetch(url, init) : await fetch(url, init);
61
96
  if (res.ok) {
62
97
  const text = await res.text();
63
98
  const data = text ? JSON.parse(text) : void 0;
@@ -95,15 +130,6 @@ async function request(ctx, opts) {
95
130
  }
96
131
  return { data: null, error: lastError, headers: new Headers() };
97
132
  }
98
- function buildURL(base, path, params) {
99
- const url = new URL(path, base.endsWith("/") ? base : `${base}/`);
100
- if (params) {
101
- for (const [k, v] of Object.entries(params)) {
102
- url.searchParams.set(k, v);
103
- }
104
- }
105
- return url.toString();
106
- }
107
133
  function backoff(attempt) {
108
134
  return Math.min(1e3 * 2 ** attempt, 3e4);
109
135
  }
@@ -172,7 +198,14 @@ var PipeRef = class {
172
198
  this._params = params;
173
199
  this._createStream = createStream;
174
200
  }
175
- /** Execute the pipe and return results. */
201
+ /**
202
+ * Execute the pipe and return results.
203
+ *
204
+ * Takes only `signal` — deliberately narrower than the `RequestOptions` the
205
+ * query builder accepts. The pipes endpoint binds the body as the pipe's
206
+ * parameters, so there is no row cap to forward; give the pipe a `{{limit}}`
207
+ * parameter in its SQL and pass it via `wh.pipe(name, { limit })`.
208
+ */
176
209
  async fetch(opts) {
177
210
  const { data, error } = await request(this._ctx, {
178
211
  method: "POST",
@@ -520,7 +553,7 @@ var SSETransport = class {
520
553
  this.onStatus?.("closed");
521
554
  }
522
555
  async _doConnect() {
523
- const url = new URL("/v1/stream", this._opts.baseURL);
556
+ const url = resolveURL(this._opts.baseURL, "/v1/stream");
524
557
  url.searchParams.set("table", this._opts.table);
525
558
  if (this._opts.since) {
526
559
  url.searchParams.set("since", this._opts.since);
@@ -690,6 +723,15 @@ var QueryBuilder = class _QueryBuilder {
690
723
  select(...columns) {
691
724
  return this._clone({ columns: [...this._state.columns, ...columns] });
692
725
  }
726
+ /**
727
+ * Select every column the caller's role is allowed to read (the all-columns
728
+ * wildcard). Use this instead of `.select(...)` when you want a full-row read;
729
+ * a bare `.fetch()` with no `.select()` does this implicitly. Mutually
730
+ * exclusive with `.select(...)`.
731
+ */
732
+ selectAll() {
733
+ return this._clone({ selectAll: true });
734
+ }
693
735
  where(column, op, value) {
694
736
  const filter = { column, op: OP_MAP[op], value };
695
737
  return this._clone({ filters: [...this._state.filters, filter] });
@@ -731,7 +773,7 @@ var QueryBuilder = class _QueryBuilder {
731
773
  return this._clone({ cacheTTL: seconds });
732
774
  }
733
775
  // --- Execution ---
734
- /** Default row limit when none is specified. Matches backend DefaultMaxRows. */
776
+ /** Default row limit when none is specified — deliberately tighter than the backend's DefaultMaxRows (10000) safety cap. */
735
777
  static DEFAULT_LIMIT = 1e3;
736
778
  async fetch(opts) {
737
779
  const effectiveLimit = opts?.limit ?? this._state.limit ?? _QueryBuilder.DEFAULT_LIMIT;
@@ -745,11 +787,11 @@ var QueryBuilder = class _QueryBuilder {
745
787
  if (error) return err(error);
746
788
  const rows = data;
747
789
  const hasMore = effectiveLimit != null && rows.length >= effectiveLimit;
748
- if (hasMore) {
790
+ if (hasMore && this._state.orderBy.length > 0) {
749
791
  const nextFn = () => this._fetchNext(rows, effectiveLimit, opts);
750
792
  return okPage(rows, true, nextFn);
751
793
  }
752
- return okPage(rows, false);
794
+ return okPage(rows, hasMore);
753
795
  }
754
796
  stream(opts) {
755
797
  const raw = this._createStream(this._state.table, opts);
@@ -788,31 +830,35 @@ var QueryBuilder = class _QueryBuilder {
788
830
  }
789
831
  _buildAST(effectiveLimit) {
790
832
  const ast = {};
791
- if (this._state.columns.length > 0) ast.columns = this._state.columns;
792
- if (this._state.aggregations.length > 0) ast.aggregations = this._state.aggregations;
833
+ const hasColumns = this._state.columns.length > 0;
834
+ const hasAggs = this._state.aggregations.length > 0;
835
+ if (this._state.selectAll) {
836
+ ast.select_all = true;
837
+ } else if (hasColumns) {
838
+ ast.columns = this._state.columns;
839
+ } else if (!hasAggs) {
840
+ ast.select_all = true;
841
+ }
842
+ if (hasAggs) ast.aggregations = this._state.aggregations;
793
843
  if (this._state.filters.length > 0) ast.filters = this._state.filters;
794
844
  if (this._state.groupBy.length > 0) ast.group_by = this._state.groupBy;
795
- if (this._state.orderBy.length > 0) {
796
- ast.order_by = this._state.orderBy;
797
- } else if (effectiveLimit != null && this._state.aggregations.length === 0) {
798
- ast.order_by = [{ column: "received_timestamp", dir: "desc" }];
799
- }
845
+ if (this._state.orderBy.length > 0) ast.order_by = this._state.orderBy;
800
846
  if (effectiveLimit != null) ast.limit = effectiveLimit;
801
847
  if (this._state.timeRange) ast.time_range = this._state.timeRange;
802
848
  return ast;
803
849
  }
804
850
  async _fetchNext(prevRows, _limit, opts) {
805
- const orderCol = this._state.orderBy[0]?.column ?? "received_timestamp";
806
- const orderDir = this._state.orderBy[0]?.dir ?? "desc";
851
+ const cursor = this._state.orderBy[0];
852
+ if (cursor == null) return okPage([], false);
853
+ const { column: orderCol, dir: orderDir } = cursor;
807
854
  const lastRow = prevRows[prevRows.length - 1];
808
855
  const lastValue = lastRow?.[orderCol];
809
856
  if (lastValue === void 0) return okPage([], false);
810
857
  const cursorOp = orderDir === "desc" ? "lt" : "gt";
811
858
  const cursorFilter = { column: orderCol, op: cursorOp, value: lastValue };
812
- const orderBy = this._state.orderBy.length > 0 ? this._state.orderBy : [{ column: orderCol, dir: orderDir }];
813
859
  const nextBuilder = this._clone({
814
860
  filters: [...this._state.filters, cursorFilter],
815
- orderBy
861
+ orderBy: this._state.orderBy
816
862
  });
817
863
  return nextBuilder.fetch(opts);
818
864
  }
@@ -903,6 +949,12 @@ function projectColumns(row, columns) {
903
949
  }
904
950
 
905
951
  // src/table.ts
952
+ var NDJSON_CONTENT_TYPE = "application/x-ndjson";
953
+ async function ndjsonSourceToString(source) {
954
+ if (typeof source === "string") return source;
955
+ if (source instanceof Uint8Array) return new TextDecoder().decode(source);
956
+ return new Response(source).text();
957
+ }
906
958
  var TableRef = class {
907
959
  _ctx;
908
960
  _table;
@@ -931,22 +983,34 @@ var TableRef = class {
931
983
  this._createStream
932
984
  );
933
985
  }
934
- /** Insert one or more rows into this table. */
986
+ /**
987
+ * Start a query that selects every column the caller's role is allowed to read
988
+ * (the all-columns wildcard). Use instead of `.select(...)` for an explicit
989
+ * full-row read; `.fetch()` does this implicitly.
990
+ */
991
+ selectAll() {
992
+ return this.select().selectAll();
993
+ }
994
+ /**
995
+ * Insert one or more rows into this table.
996
+ *
997
+ * A single object is sent as a JSON `POST /v1/ingest`. An **array** is
998
+ * serialized to NDJSON (one record per line) and sent as a single
999
+ * `application/x-ndjson` request: a bad record no longer fails or hides the
1000
+ * rest of the batch — per-record outcomes come back in the result
1001
+ * (`failed` / `results`), and `ok` is true only when every record succeeded.
1002
+ *
1003
+ * The array path sends one request regardless of size; bounded-concurrency
1004
+ * chunking of very large arrays is tracked separately (#196). For NDJSON you
1005
+ * already have (a file or stream), use {@link insertNDJSON}.
1006
+ */
935
1007
  async insert(data, opts) {
936
1008
  if (Array.isArray(data)) {
937
- const promises = data.map(
938
- (row) => request(this._ctx, {
939
- method: "POST",
940
- path: `/v1/ingest?table=${encodeURIComponent(this._table)}`,
941
- body: row,
942
- signal: opts?.signal
943
- })
944
- );
945
- const results = await Promise.all(promises);
946
- for (const res2 of results) {
947
- if (res2.error) return err(res2.error);
1009
+ if (data.length === 0) {
1010
+ return ok({ ok: true, total: 0, succeeded: 0, failed: 0, duplicates: 0, results: [] });
948
1011
  }
949
- return ok({ ok: true });
1012
+ const ndjson = data.map((row) => JSON.stringify(row)).join("\n");
1013
+ return this._sendNDJSON(ndjson, opts);
950
1014
  }
951
1015
  const { data: res, error } = await request(this._ctx, {
952
1016
  method: "POST",
@@ -959,6 +1023,46 @@ var TableRef = class {
959
1023
  if (res?.duplicate != null) result.duplicate = res.duplicate;
960
1024
  return ok(result);
961
1025
  }
1026
+ /**
1027
+ * Insert pre-formatted NDJSON (newline-delimited JSON, one record per line)
1028
+ * from a string, raw bytes, a Blob/File, or a byte stream — e.g. a `.ndjson`
1029
+ * file or a stream produced by another system. For in-memory rows, prefer
1030
+ * {@link insert}.
1031
+ *
1032
+ * Non-string sources are read fully into memory before sending (the server
1033
+ * streams the parse). Returns the same per-record batch summary as an array
1034
+ * `insert`.
1035
+ */
1036
+ async insertNDJSON(source, opts) {
1037
+ const ndjson = await ndjsonSourceToString(source);
1038
+ return this._sendNDJSON(ndjson, opts);
1039
+ }
1040
+ /**
1041
+ * @internal Send an NDJSON body and map the server's batch response onto an
1042
+ * {@link InsertResult}. A transport / whole-request failure surfaces as the
1043
+ * Result error arm; a processed batch (even with rejected records) surfaces
1044
+ * as the success arm with `ok = failed === 0`.
1045
+ */
1046
+ async _sendNDJSON(ndjson, opts) {
1047
+ const { data, error } = await request(this._ctx, {
1048
+ method: "POST",
1049
+ path: `/v1/ingest?table=${encodeURIComponent(this._table)}`,
1050
+ rawBody: ndjson,
1051
+ contentType: NDJSON_CONTENT_TYPE,
1052
+ signal: opts?.signal
1053
+ });
1054
+ if (error) return err(error);
1055
+ const r = data ?? { total: 0, succeeded: 0, failed: 0, duplicates: 0 };
1056
+ const result = {
1057
+ ok: r.failed === 0,
1058
+ total: r.total,
1059
+ succeeded: r.succeeded,
1060
+ failed: r.failed,
1061
+ duplicates: r.duplicates
1062
+ };
1063
+ if (r.results && r.results.length > 0) result.results = r.results;
1064
+ return ok(result);
1065
+ }
962
1066
  /** Fetch the schema for this table. */
963
1067
  async schema(opts) {
964
1068
  const { data, error } = await request(this._ctx, {
@@ -994,7 +1098,10 @@ var WaveHouseClient = class {
994
1098
  baseURL: config.baseURL.replace(/\/+$/, ""),
995
1099
  auth: config.auth,
996
1100
  options: {
997
- maxRetries: config.options?.maxRetries ?? 2
1101
+ maxRetries: config.options?.maxRetries ?? 2,
1102
+ fetch: config.options?.fetch,
1103
+ headers: config.options?.headers,
1104
+ fetchOptions: config.options?.fetchOptions
998
1105
  }
999
1106
  };
1000
1107
  this.schema = new SchemaNamespace(this._ctx);