@wavehouse/sdk 0.0.0-dev.0f8826c

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.cjs ADDED
@@ -0,0 +1,1107 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ DLQNamespace: () => DLQNamespace,
24
+ LiveQuery: () => LiveQuery,
25
+ PipeRef: () => PipeRef,
26
+ PipesNamespace: () => PipesNamespace,
27
+ PolicyNamespace: () => PolicyNamespace,
28
+ QueryBuilder: () => QueryBuilder,
29
+ SchemaNamespace: () => SchemaNamespace,
30
+ StreamController: () => StreamController,
31
+ SysNamespace: () => SysNamespace,
32
+ TableRef: () => TableRef,
33
+ WaveHouseClient: () => WaveHouseClient,
34
+ createClient: () => createClient
35
+ });
36
+ module.exports = __toCommonJS(src_exports);
37
+
38
+ // src/errors.ts
39
+ async function parseErrorResponse(res) {
40
+ let body;
41
+ try {
42
+ body = await res.json();
43
+ } catch {
44
+ body = void 0;
45
+ }
46
+ const message = typeof body?.error === "string" ? body.error : typeof body?.message === "string" ? body.message : res.statusText;
47
+ const retryable = res.status === 503 || res.status >= 500;
48
+ return {
49
+ status: res.status,
50
+ code: `HTTP_${res.status}`,
51
+ message,
52
+ details: body,
53
+ retryable
54
+ };
55
+ }
56
+ function networkError(cause) {
57
+ const message = cause instanceof Error ? cause.message : String(cause);
58
+ return {
59
+ status: 0,
60
+ code: "NETWORK_ERROR",
61
+ message,
62
+ retryable: true
63
+ };
64
+ }
65
+ function ok(data) {
66
+ return { ok: true, data, error: null };
67
+ }
68
+ function okPage(data, hasMore, next) {
69
+ return { ok: true, data, error: null, hasMore, next };
70
+ }
71
+ function err(error) {
72
+ return { ok: false, data: null, error };
73
+ }
74
+
75
+ // src/http.ts
76
+ async function request(ctx, opts) {
77
+ const url = buildURL(ctx.baseURL, opts.path, opts.params);
78
+ const headers = {
79
+ "Content-Type": "application/json",
80
+ Accept: "application/json"
81
+ };
82
+ if (ctx.auth) {
83
+ const token = await ctx.auth();
84
+ if (token) {
85
+ headers.Authorization = `Bearer ${token}`;
86
+ }
87
+ }
88
+ let lastError = null;
89
+ const maxAttempts = ctx.options.maxRetries + 1;
90
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
91
+ try {
92
+ const res = await fetch(url, {
93
+ method: opts.method,
94
+ headers,
95
+ body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0,
96
+ signal: opts.signal
97
+ });
98
+ if (res.ok) {
99
+ const text = await res.text();
100
+ const data = text ? JSON.parse(text) : void 0;
101
+ return { data, error: null, headers: res.headers };
102
+ }
103
+ const error = await parseErrorResponse(res);
104
+ if (res.status === 503) {
105
+ const retryAfter = res.headers.get("Retry-After");
106
+ if (retryAfter && attempt < maxAttempts - 1) {
107
+ const delay = parseInt(retryAfter, 10) * 1e3 || 3e4;
108
+ await sleep(delay, opts.signal);
109
+ lastError = error;
110
+ continue;
111
+ }
112
+ }
113
+ if (error.retryable && attempt < maxAttempts - 1) {
114
+ await sleep(backoff(attempt), opts.signal);
115
+ lastError = error;
116
+ continue;
117
+ }
118
+ return { data: null, error, headers: res.headers };
119
+ } catch (e) {
120
+ if (e instanceof DOMException && e.name === "AbortError") {
121
+ return {
122
+ data: null,
123
+ error: { status: 0, code: "ABORTED", message: "Request aborted", retryable: false },
124
+ headers: new Headers()
125
+ };
126
+ }
127
+ lastError = networkError(e);
128
+ if (attempt < maxAttempts - 1) {
129
+ await sleep(backoff(attempt), opts.signal);
130
+ }
131
+ }
132
+ }
133
+ return { data: null, error: lastError, headers: new Headers() };
134
+ }
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
+ function backoff(attempt) {
145
+ return Math.min(1e3 * 2 ** attempt, 3e4);
146
+ }
147
+ function sleep(ms, signal) {
148
+ return new Promise((resolve, reject) => {
149
+ if (signal?.aborted) {
150
+ reject(new DOMException("Aborted", "AbortError"));
151
+ return;
152
+ }
153
+ const timer = setTimeout(resolve, ms);
154
+ signal?.addEventListener(
155
+ "abort",
156
+ () => {
157
+ clearTimeout(timer);
158
+ reject(new DOMException("Aborted", "AbortError"));
159
+ },
160
+ { once: true }
161
+ );
162
+ });
163
+ }
164
+
165
+ // src/dlq.ts
166
+ var DLQNamespace = class {
167
+ _ctx;
168
+ _createStream;
169
+ constructor(ctx, createStream) {
170
+ this._ctx = ctx;
171
+ this._createStream = createStream;
172
+ }
173
+ /** Get DLQ statistics (message counts per table). */
174
+ async list(opts) {
175
+ const { data, error } = await request(this._ctx, {
176
+ method: "GET",
177
+ path: "/v1/dlq/stats",
178
+ signal: opts?.signal
179
+ });
180
+ if (error) return err(error);
181
+ return ok(data);
182
+ }
183
+ /** Get DLQ stats filtered by table name. */
184
+ async table(name, opts) {
185
+ const { data, error } = await request(this._ctx, {
186
+ method: "GET",
187
+ path: "/v1/dlq/stats",
188
+ params: { table: name },
189
+ signal: opts?.signal
190
+ });
191
+ if (error) return err(error);
192
+ return ok(data);
193
+ }
194
+ /** Subscribe to live DLQ events. */
195
+ stream(opts) {
196
+ return this._createStream("dlq", opts);
197
+ }
198
+ };
199
+
200
+ // src/pipes.ts
201
+ var PipeRef = class {
202
+ _ctx;
203
+ _name;
204
+ _params;
205
+ _createStream;
206
+ constructor(ctx, name, params, createStream) {
207
+ this._ctx = ctx;
208
+ this._name = name;
209
+ this._params = params;
210
+ this._createStream = createStream;
211
+ }
212
+ /** Execute the pipe and return results. */
213
+ async fetch(opts) {
214
+ const { data, error } = await request(this._ctx, {
215
+ method: "POST",
216
+ path: `/v1/pipes/${encodeURIComponent(this._name)}`,
217
+ body: this._params ?? {},
218
+ signal: opts?.signal
219
+ });
220
+ if (error) return err(error);
221
+ return ok(data);
222
+ }
223
+ /** Subscribe to live events from the pipe's underlying query. */
224
+ stream(opts) {
225
+ return this._createStream(this._name, opts);
226
+ }
227
+ then(onfulfilled, onrejected) {
228
+ return this.fetch().then(onfulfilled, onrejected);
229
+ }
230
+ };
231
+ var PipesNamespace = class {
232
+ _ctx;
233
+ constructor(ctx) {
234
+ this._ctx = ctx;
235
+ }
236
+ /** List all registered pipes. */
237
+ async list(opts) {
238
+ const { data, error } = await request(this._ctx, {
239
+ method: "GET",
240
+ path: "/v1/admin/pipes",
241
+ signal: opts?.signal
242
+ });
243
+ if (error) return err(error);
244
+ return ok(data);
245
+ }
246
+ /** Get a single pipe definition by name. */
247
+ async get(name, opts) {
248
+ const { data, error } = await request(this._ctx, {
249
+ method: "GET",
250
+ path: `/v1/admin/pipes/${encodeURIComponent(name)}`,
251
+ signal: opts?.signal
252
+ });
253
+ if (error) return err(error);
254
+ return ok(data);
255
+ }
256
+ /** Create or update a pipe. */
257
+ async set(name, def, opts) {
258
+ const { error } = await request(this._ctx, {
259
+ method: "PUT",
260
+ path: `/v1/admin/pipes/${encodeURIComponent(name)}`,
261
+ body: def,
262
+ signal: opts?.signal
263
+ });
264
+ if (error) return err(error);
265
+ return ok(void 0);
266
+ }
267
+ /** Delete a pipe by name. */
268
+ async delete(name, opts) {
269
+ const { error } = await request(this._ctx, {
270
+ method: "DELETE",
271
+ path: `/v1/admin/pipes/${encodeURIComponent(name)}`,
272
+ signal: opts?.signal
273
+ });
274
+ if (error) return err(error);
275
+ return ok(void 0);
276
+ }
277
+ };
278
+
279
+ // src/policy.ts
280
+ var PolicyNamespace = class {
281
+ _ctx;
282
+ constructor(ctx) {
283
+ this._ctx = ctx;
284
+ }
285
+ /** Get the current access control policy. */
286
+ async get(opts) {
287
+ const { data, error } = await request(this._ctx, {
288
+ method: "GET",
289
+ path: "/v1/admin/policy",
290
+ signal: opts?.signal
291
+ });
292
+ if (error) return err(error);
293
+ return ok(data);
294
+ }
295
+ /** Replace the entire access control policy. */
296
+ async set(policy, opts) {
297
+ const { error } = await request(this._ctx, {
298
+ method: "PUT",
299
+ path: "/v1/admin/policy",
300
+ body: policy,
301
+ signal: opts?.signal
302
+ });
303
+ if (error) return err(error);
304
+ return ok(void 0);
305
+ }
306
+ /** Validate a policy without applying it (dry run). */
307
+ async validate(policy, opts) {
308
+ const { data, error } = await request(this._ctx, {
309
+ method: "POST",
310
+ path: "/v1/admin/policy/validate",
311
+ body: policy,
312
+ signal: opts?.signal
313
+ });
314
+ if (error) return err(error);
315
+ return ok(data);
316
+ }
317
+ };
318
+
319
+ // src/schema.ts
320
+ var SchemaNamespace = class {
321
+ _ctx;
322
+ constructor(ctx) {
323
+ this._ctx = ctx;
324
+ }
325
+ /** List all table schemas discovered from ClickHouse. */
326
+ async list(opts) {
327
+ const { data, error } = await request(this._ctx, {
328
+ method: "GET",
329
+ path: "/v1/schema",
330
+ signal: opts?.signal
331
+ });
332
+ if (error) return err(error);
333
+ let schemas;
334
+ if (Array.isArray(data)) {
335
+ schemas = {};
336
+ for (const table of data) {
337
+ if (table && typeof table === "object" && "name" in table) {
338
+ schemas[table.name] = table;
339
+ }
340
+ }
341
+ } else {
342
+ schemas = data;
343
+ }
344
+ return ok(schemas);
345
+ }
346
+ /** Force a schema refresh from ClickHouse system.columns. */
347
+ async refresh(opts) {
348
+ const { error } = await request(this._ctx, {
349
+ method: "POST",
350
+ path: "/v1/schema/refresh",
351
+ signal: opts?.signal
352
+ });
353
+ if (error) return err(error);
354
+ return ok(void 0);
355
+ }
356
+ };
357
+
358
+ // src/sql.ts
359
+ async function sql(ctx, query, opts) {
360
+ const { data, error } = await request(ctx, {
361
+ method: "POST",
362
+ path: "/v1/admin/query",
363
+ body: { sql: query },
364
+ signal: opts?.signal
365
+ });
366
+ if (error) return err(error);
367
+ return ok(data);
368
+ }
369
+
370
+ // src/stream/controller.ts
371
+ var StreamController = class {
372
+ _transport;
373
+ _subscribers = /* @__PURE__ */ new Set();
374
+ _status = "connecting";
375
+ _buffer = [];
376
+ _waiters = [];
377
+ _done = false;
378
+ constructor(transport) {
379
+ this._transport = transport;
380
+ this._transport.onEvent = (event) => {
381
+ for (const sub of this._subscribers) {
382
+ sub.next(event);
383
+ }
384
+ const waiter = this._waiters.shift();
385
+ if (waiter) {
386
+ waiter.resolve({ value: event, done: false });
387
+ } else {
388
+ this._buffer.push(event);
389
+ }
390
+ };
391
+ this._transport.onStatus = (status) => {
392
+ if (status === this._status) return;
393
+ this._status = status;
394
+ for (const sub of this._subscribers) {
395
+ sub.status?.(status);
396
+ }
397
+ if (status === "closed") {
398
+ this._done = true;
399
+ for (const w of this._waiters) {
400
+ w.resolve({ value: void 0, done: true });
401
+ }
402
+ this._waiters = [];
403
+ }
404
+ };
405
+ this._transport.onError = (error) => {
406
+ for (const sub of this._subscribers) {
407
+ sub.error?.(error);
408
+ }
409
+ };
410
+ this._transport.connect();
411
+ }
412
+ /** Current connection status. */
413
+ get status() {
414
+ return this._status;
415
+ }
416
+ /**
417
+ * Returns a promise that resolves when the stream status reaches `'live'`,
418
+ * rejects immediately if the stream is already `'closed'`, or rejects after
419
+ * `timeoutMs` milliseconds (default: 5 000) if it never connects.
420
+ *
421
+ * Safe to call before `.subscribe()` — does not trigger auto-close when
422
+ * the internal waiter is removed.
423
+ *
424
+ * `@example`
425
+ * const stream = client.from('events').stream();
426
+ * const unsub = stream.subscribe({ next: (e) => console.log(e) });
427
+ * await stream.connected(); // waits until the transport is live
428
+ * await client.from('events').insert({ ... });
429
+ */
430
+ connected(timeoutMs = 5e3) {
431
+ if (this._status === "live") return Promise.resolve();
432
+ if (this._status === "closed") return Promise.reject(new Error("Stream is closed"));
433
+ if (this._done) return Promise.reject(new Error("Stream closed before connecting"));
434
+ return new Promise((resolve, reject) => {
435
+ let settled = false;
436
+ const timer = setTimeout(() => {
437
+ if (settled) return;
438
+ settled = true;
439
+ this._subscribers.delete(watcher);
440
+ reject(new Error(`Stream did not connect within ${timeoutMs}ms`));
441
+ }, timeoutMs);
442
+ const watcher = {
443
+ next: () => {
444
+ },
445
+ status: (s) => {
446
+ if (s === "live") {
447
+ if (settled) return;
448
+ settled = true;
449
+ clearTimeout(timer);
450
+ this._subscribers.delete(watcher);
451
+ resolve();
452
+ } else if (s === "closed") {
453
+ if (settled) return;
454
+ settled = true;
455
+ clearTimeout(timer);
456
+ this._subscribers.delete(watcher);
457
+ reject(new Error("Stream closed before connecting"));
458
+ }
459
+ }
460
+ };
461
+ this._subscribers.add(watcher);
462
+ });
463
+ }
464
+ /** Subscribe to stream events via callbacks. Returns an unsubscribe function. */
465
+ subscribe(subscriber) {
466
+ this._subscribers.add(subscriber);
467
+ subscriber.status?.(this._status);
468
+ return () => {
469
+ this._subscribers.delete(subscriber);
470
+ if (this._subscribers.size === 0 && this._waiters.length === 0) {
471
+ this.close();
472
+ }
473
+ };
474
+ }
475
+ /** Attach an AbortSignal — when aborted, the stream is closed. */
476
+ attachSignal(signal) {
477
+ if (signal.aborted) {
478
+ this.close();
479
+ return;
480
+ }
481
+ signal.addEventListener("abort", () => this.close(), { once: true });
482
+ }
483
+ /** Close the stream and release resources. */
484
+ close() {
485
+ this._transport.disconnect();
486
+ if (this._status !== "closed") {
487
+ this._status = "closed";
488
+ for (const sub of this._subscribers) {
489
+ sub.status?.("closed");
490
+ }
491
+ }
492
+ this._done = true;
493
+ for (const w of this._waiters) {
494
+ w.resolve({ value: void 0, done: true });
495
+ }
496
+ this._waiters = [];
497
+ }
498
+ /** Async iterator protocol — enables `for await (const event of stream)`. */
499
+ [Symbol.asyncIterator]() {
500
+ const self = this;
501
+ return {
502
+ next() {
503
+ if (self._buffer.length > 0) {
504
+ return Promise.resolve({ value: self._buffer.shift(), done: false });
505
+ }
506
+ if (self._done) {
507
+ return Promise.resolve({ value: void 0, done: true });
508
+ }
509
+ return new Promise((resolve) => {
510
+ self._waiters.push({ resolve });
511
+ });
512
+ },
513
+ return() {
514
+ self.close();
515
+ return Promise.resolve({ value: void 0, done: true });
516
+ },
517
+ [Symbol.asyncIterator]() {
518
+ return this;
519
+ }
520
+ };
521
+ }
522
+ };
523
+
524
+ // src/stream/sse.ts
525
+ var activeSSEConnections = 0;
526
+ var SSE_WARN_THRESHOLD = 5;
527
+ var SSETransport = class {
528
+ _opts;
529
+ _es = null;
530
+ onEvent = null;
531
+ onStatus = null;
532
+ onError = null;
533
+ constructor(opts) {
534
+ this._opts = opts;
535
+ }
536
+ connect() {
537
+ if (typeof EventSource === "undefined") {
538
+ throw new Error(
539
+ "[wavehouse] EventSource is not available in this environment. Please provide a global polyfill (e.g., `globalThis.EventSource = require('eventsource')`)."
540
+ );
541
+ }
542
+ this._doConnect().catch((err2) => {
543
+ this.onError?.({
544
+ status: 0,
545
+ code: "SSE_CONNECT_ERROR",
546
+ message: err2 instanceof Error ? err2.message : String(err2),
547
+ retryable: true
548
+ });
549
+ });
550
+ }
551
+ disconnect() {
552
+ if (this._es) {
553
+ this._es.close();
554
+ this._es = null;
555
+ activeSSEConnections = Math.max(0, activeSSEConnections - 1);
556
+ }
557
+ this.onStatus?.("closed");
558
+ }
559
+ async _doConnect() {
560
+ const url = new URL("/v1/stream", this._opts.baseURL);
561
+ url.searchParams.set("table", this._opts.table);
562
+ if (this._opts.since) {
563
+ url.searchParams.set("since", this._opts.since);
564
+ }
565
+ if (this._opts.auth) {
566
+ const token = await this._opts.auth();
567
+ if (token) {
568
+ url.searchParams.set("token", token);
569
+ }
570
+ }
571
+ activeSSEConnections++;
572
+ if (activeSSEConnections > SSE_WARN_THRESHOLD) {
573
+ console.warn(
574
+ `[wavehouse] ${activeSSEConnections} SSE connections open. Browsers limit HTTP/1.1 to 6 connections per domain.`
575
+ );
576
+ }
577
+ this._es = new EventSource(url.toString());
578
+ this._es.onopen = () => {
579
+ this.onStatus?.("live");
580
+ };
581
+ this._es.onmessage = (e) => {
582
+ try {
583
+ const msg = JSON.parse(e.data);
584
+ const event = {
585
+ table: msg.table_name,
586
+ timestamp: msg.received_timestamp,
587
+ data: msg.data
588
+ };
589
+ this.onEvent?.(event);
590
+ } catch {
591
+ console.warn("[wavehouse] SSE received malformed message:", e.data);
592
+ }
593
+ };
594
+ this._es.onerror = () => {
595
+ if (this._es?.readyState === EventSource.CONNECTING) {
596
+ this.onStatus?.("reconnecting");
597
+ } else if (this._es?.readyState === EventSource.CLOSED) {
598
+ this.onStatus?.("closed");
599
+ } else if (this._es?.readyState === EventSource.OPEN) {
600
+ this.onStatus?.("live");
601
+ } else {
602
+ this.onError?.({
603
+ status: 0,
604
+ code: "SSE_ERROR",
605
+ message: "SSE connection error",
606
+ retryable: true
607
+ });
608
+ }
609
+ };
610
+ }
611
+ };
612
+
613
+ // src/sys.ts
614
+ var SysNamespace = class {
615
+ _ctx;
616
+ constructor(ctx) {
617
+ this._ctx = ctx;
618
+ }
619
+ /**
620
+ * Liveness ping — resolves with no error when the server is reachable and
621
+ * past boot. Hits the public, content-free `/v1/health` route (200/503, no
622
+ * body), kept intentionally distinct from the `/livez` Kubernetes probe so
623
+ * it stays reachable even in deployments that filter probe paths at the
624
+ * reverse proxy. Use it to check a server is online before sending data, or
625
+ * to pick among servers in a distributed setup.
626
+ */
627
+ async health(opts) {
628
+ const { error } = await request(this._ctx, {
629
+ method: "GET",
630
+ path: "/v1/health",
631
+ signal: opts?.signal
632
+ });
633
+ if (error) return err(error);
634
+ return ok(void 0);
635
+ }
636
+ };
637
+
638
+ // src/stream/live-query.ts
639
+ var LiveQuery = class {
640
+ _stream;
641
+ _subscriber;
642
+ _buffer = [];
643
+ _buffering = true;
644
+ _unsubStream = null;
645
+ _closed = false;
646
+ constructor(stream, fetchFn, subscriber, _filters) {
647
+ this._stream = stream;
648
+ this._subscriber = subscriber;
649
+ this._unsubStream = stream.subscribe({
650
+ next: (event) => {
651
+ if (this._closed) return;
652
+ if (this._buffering) {
653
+ this._buffer.push(event);
654
+ } else {
655
+ subscriber.next(event);
656
+ }
657
+ },
658
+ status: (s) => subscriber.status?.(s),
659
+ error: (e) => subscriber.error?.(e)
660
+ });
661
+ this._runBackfill(fetchFn);
662
+ }
663
+ async _runBackfill(fetchFn) {
664
+ try {
665
+ const result = await fetchFn();
666
+ if (this._closed) return;
667
+ this._subscriber.initial?.(result);
668
+ if (result.error) {
669
+ this._buffering = false;
670
+ return;
671
+ }
672
+ const rows = result.data;
673
+ let lastTimestamp;
674
+ if (rows.length > 0) {
675
+ const lastRow = rows[rows.length - 1];
676
+ lastTimestamp = lastRow?.received_timestamp;
677
+ }
678
+ this._buffering = false;
679
+ for (const event of this._buffer) {
680
+ if (this._closed) break;
681
+ if (lastTimestamp && event.timestamp <= lastTimestamp) {
682
+ continue;
683
+ }
684
+ this._subscriber.next(event);
685
+ }
686
+ this._buffer = [];
687
+ } catch {
688
+ this._buffering = false;
689
+ this._buffer = [];
690
+ }
691
+ }
692
+ /** Close the live query and the underlying stream. */
693
+ close() {
694
+ this._closed = true;
695
+ this._buffering = false;
696
+ this._buffer = [];
697
+ this._unsubStream?.();
698
+ this._stream.close();
699
+ }
700
+ };
701
+
702
+ // src/query-builder.ts
703
+ var OP_MAP = {
704
+ "=": "eq",
705
+ "!=": "neq",
706
+ ">": "gt",
707
+ ">=": "gte",
708
+ "<": "lt",
709
+ "<=": "lte",
710
+ in: "in",
711
+ like: "like",
712
+ not_like: "not_like"
713
+ };
714
+ var QueryBuilder = class _QueryBuilder {
715
+ /** @internal */
716
+ _state;
717
+ /** @internal */
718
+ _ctx;
719
+ /** @internal */
720
+ _createStream;
721
+ constructor(ctx, state, createStream) {
722
+ this._ctx = ctx;
723
+ this._state = Object.freeze({ ...state });
724
+ this._createStream = createStream;
725
+ }
726
+ // --- Builder methods (each returns a new QueryBuilder) ---
727
+ select(...columns) {
728
+ return this._clone({ columns: [...this._state.columns, ...columns] });
729
+ }
730
+ where(column, op, value) {
731
+ const filter = { column, op: OP_MAP[op], value };
732
+ return this._clone({ filters: [...this._state.filters, filter] });
733
+ }
734
+ count(column = "*", alias = "count") {
735
+ return this._addAgg("count", column, alias);
736
+ }
737
+ sum(column, alias = `sum_${column}`) {
738
+ return this._addAgg("sum", column, alias);
739
+ }
740
+ avg(column, alias = `avg_${column}`) {
741
+ return this._addAgg("avg", column, alias);
742
+ }
743
+ min(column, alias = `min_${column}`) {
744
+ return this._addAgg("min", column, alias);
745
+ }
746
+ max(column, alias = `max_${column}`) {
747
+ return this._addAgg("max", column, alias);
748
+ }
749
+ countDistinct(column, alias = `count_distinct_${column}`) {
750
+ return this._addAgg("countDistinct", column, alias);
751
+ }
752
+ aggregate(fn, column, alias) {
753
+ return this._addAgg(fn, column, alias);
754
+ }
755
+ groupBy(...columns) {
756
+ return this._clone({ groupBy: [...this._state.groupBy, ...columns] });
757
+ }
758
+ orderBy(column, dir = "asc") {
759
+ return this._clone({ orderBy: [...this._state.orderBy, { column, dir }] });
760
+ }
761
+ limit(n) {
762
+ return this._clone({ limit: n });
763
+ }
764
+ timeRange(column, since, until) {
765
+ return this._clone({ timeRange: { column, since, until } });
766
+ }
767
+ cacheTTL(seconds) {
768
+ return this._clone({ cacheTTL: seconds });
769
+ }
770
+ // --- Execution ---
771
+ /** Default row limit when none is specified. Matches backend DefaultMaxRows. */
772
+ static DEFAULT_LIMIT = 1e3;
773
+ async fetch(opts) {
774
+ const effectiveLimit = opts?.limit ?? this._state.limit ?? _QueryBuilder.DEFAULT_LIMIT;
775
+ const ast = this._buildAST(effectiveLimit);
776
+ const { data, error } = await request(this._ctx, {
777
+ method: "POST",
778
+ path: `/v1/query?table=${encodeURIComponent(this._state.table)}`,
779
+ body: ast,
780
+ signal: opts?.signal
781
+ });
782
+ if (error) return err(error);
783
+ const rows = data;
784
+ const hasMore = effectiveLimit != null && rows.length >= effectiveLimit;
785
+ if (hasMore) {
786
+ const nextFn = () => this._fetchNext(rows, effectiveLimit, opts);
787
+ return okPage(rows, true, nextFn);
788
+ }
789
+ return okPage(rows, false);
790
+ }
791
+ stream(opts) {
792
+ const raw = this._createStream(this._state.table, opts);
793
+ const filters = this._state.filters;
794
+ const columns = this._state.columns;
795
+ if (filters.length === 0 && columns.length === 0) {
796
+ return raw;
797
+ }
798
+ return new FilteredStreamController(raw, filters, columns);
799
+ }
800
+ /**
801
+ * Start a live query: fetches historical data, then streams live updates.
802
+ *
803
+ * The subscriber's `initial()` is called once with the fetch result, then
804
+ * `next()` fires for each live event. Events that arrived during the fetch
805
+ * are deduplicated and flushed automatically.
806
+ *
807
+ * Returns a LiveQuery handle with a `.close()` method.
808
+ */
809
+ liveQuery(subscriber, opts) {
810
+ const stream = this.stream(opts);
811
+ const fetchFn = () => this.fetch();
812
+ return new LiveQuery(stream, fetchFn, subscriber, this._state.filters);
813
+ }
814
+ // --- PromiseLike implementation ---
815
+ then(onfulfilled, onrejected) {
816
+ return this.fetch().then(onfulfilled, onrejected);
817
+ }
818
+ // --- Private helpers ---
819
+ _clone(overrides) {
820
+ return new _QueryBuilder(this._ctx, { ...this._state, ...overrides }, this._createStream);
821
+ }
822
+ _addAgg(fn, column, alias) {
823
+ const agg = { fn, column, alias };
824
+ return this._clone({ aggregations: [...this._state.aggregations, agg] });
825
+ }
826
+ _buildAST(effectiveLimit) {
827
+ 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;
830
+ if (this._state.filters.length > 0) ast.filters = this._state.filters;
831
+ 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
+ }
837
+ if (effectiveLimit != null) ast.limit = effectiveLimit;
838
+ if (this._state.timeRange) ast.time_range = this._state.timeRange;
839
+ return ast;
840
+ }
841
+ async _fetchNext(prevRows, _limit, opts) {
842
+ const orderCol = this._state.orderBy[0]?.column ?? "received_timestamp";
843
+ const orderDir = this._state.orderBy[0]?.dir ?? "desc";
844
+ const lastRow = prevRows[prevRows.length - 1];
845
+ const lastValue = lastRow?.[orderCol];
846
+ if (lastValue === void 0) return okPage([], false);
847
+ const cursorOp = orderDir === "desc" ? "lt" : "gt";
848
+ const cursorFilter = { column: orderCol, op: cursorOp, value: lastValue };
849
+ const orderBy = this._state.orderBy.length > 0 ? this._state.orderBy : [{ column: orderCol, dir: orderDir }];
850
+ const nextBuilder = this._clone({
851
+ filters: [...this._state.filters, cursorFilter],
852
+ orderBy
853
+ });
854
+ return nextBuilder.fetch(opts);
855
+ }
856
+ };
857
+ var FilteredStreamController = class extends StreamController {
858
+ constructor(inner, filters, columns) {
859
+ const transport = {
860
+ onEvent: null,
861
+ onStatus: null,
862
+ onError: null,
863
+ connect() {
864
+ inner.subscribe({
865
+ next: (event) => {
866
+ if (!matchesFilters(event.data, filters)) {
867
+ return;
868
+ }
869
+ const projected = columns.length > 0 ? {
870
+ ...event,
871
+ data: projectColumns(event.data, columns)
872
+ } : event;
873
+ this.onEvent?.(projected);
874
+ },
875
+ status: (s) => this.onStatus?.(s),
876
+ error: (e) => this.onError?.(e)
877
+ });
878
+ },
879
+ disconnect() {
880
+ inner.close();
881
+ }
882
+ };
883
+ super(transport);
884
+ }
885
+ };
886
+ function matchesFilters(row, filters) {
887
+ for (const f of filters) {
888
+ const val = row[f.column];
889
+ if (!evaluateFilter(val, f.op, f.value)) return false;
890
+ }
891
+ return true;
892
+ }
893
+ function evaluateFilter(actual, op, expected) {
894
+ switch (op) {
895
+ case "eq":
896
+ return actual === expected;
897
+ case "neq":
898
+ return actual !== expected;
899
+ case "gt":
900
+ return compareOrdered(actual, expected, (a, b) => a > b);
901
+ case "gte":
902
+ return compareOrdered(actual, expected, (a, b) => a >= b);
903
+ case "lt":
904
+ return compareOrdered(actual, expected, (a, b) => a < b);
905
+ case "lte":
906
+ return compareOrdered(actual, expected, (a, b) => a <= b);
907
+ case "in":
908
+ return Array.isArray(expected) && expected.includes(actual);
909
+ case "like": {
910
+ if (typeof actual !== "string" || typeof expected !== "string") return false;
911
+ const escaped = expected.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
912
+ const pattern = escaped.replace(/%/g, ".*").replace(/_/g, ".");
913
+ return new RegExp(`^${pattern}$`, "i").test(actual);
914
+ }
915
+ case "not_like": {
916
+ if (typeof actual !== "string" || typeof expected !== "string") return false;
917
+ const escaped = expected.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
918
+ const pattern = escaped.replace(/%/g, ".*").replace(/_/g, ".");
919
+ return !new RegExp(`^${pattern}$`, "i").test(actual);
920
+ }
921
+ default:
922
+ return true;
923
+ }
924
+ }
925
+ function compareOrdered(actual, expected, cmp) {
926
+ if (typeof actual === "number" && typeof expected === "number") {
927
+ return cmp(actual, expected);
928
+ }
929
+ if (typeof actual === "string" && typeof expected === "string") {
930
+ return cmp(actual, expected);
931
+ }
932
+ return false;
933
+ }
934
+ function projectColumns(row, columns) {
935
+ const result = {};
936
+ for (const col of columns) {
937
+ if (col in row) result[col] = row[col];
938
+ }
939
+ return result;
940
+ }
941
+
942
+ // src/table.ts
943
+ var TableRef = class {
944
+ _ctx;
945
+ _table;
946
+ _createStream;
947
+ constructor(ctx, table, createStream) {
948
+ this._ctx = ctx;
949
+ this._table = table;
950
+ this._createStream = createStream;
951
+ }
952
+ /** SELECT * shortcut — fetches rows with optional pagination. */
953
+ async fetch(opts) {
954
+ return this.select().limit(opts?.limit ?? 1e3).fetch(opts);
955
+ }
956
+ /** Start building a typed query. Returns an immutable, PromiseLike QueryBuilder. */
957
+ select(...columns) {
958
+ return new QueryBuilder(
959
+ this._ctx,
960
+ {
961
+ table: this._table,
962
+ columns,
963
+ aggregations: [],
964
+ filters: [],
965
+ groupBy: [],
966
+ orderBy: []
967
+ },
968
+ this._createStream
969
+ );
970
+ }
971
+ /** Insert one or more rows into this table. */
972
+ async insert(data, opts) {
973
+ 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);
985
+ }
986
+ return ok({ ok: true });
987
+ }
988
+ const { data: res, error } = await request(this._ctx, {
989
+ method: "POST",
990
+ path: `/v1/ingest?table=${encodeURIComponent(this._table)}`,
991
+ body: data,
992
+ signal: opts?.signal
993
+ });
994
+ if (error) return err(error);
995
+ const result = { ok: res?.ok ?? true };
996
+ if (res?.duplicate != null) result.duplicate = res.duplicate;
997
+ return ok(result);
998
+ }
999
+ /** Fetch the schema for this table. */
1000
+ async schema(opts) {
1001
+ const { data, error } = await request(this._ctx, {
1002
+ method: "GET",
1003
+ path: `/v1/schema?table=${encodeURIComponent(this._table)}`,
1004
+ signal: opts?.signal
1005
+ });
1006
+ if (error) return err(error);
1007
+ return ok(data);
1008
+ }
1009
+ /** Subscribe to live events for this table. */
1010
+ stream(opts) {
1011
+ return this._createStream(this._table, opts);
1012
+ }
1013
+ };
1014
+
1015
+ // src/client.ts
1016
+ var WaveHouseClient = class {
1017
+ /** @internal */
1018
+ _ctx;
1019
+ /** Schema introspection namespace. */
1020
+ schema;
1021
+ /** Access control policy namespace (admin). */
1022
+ policy;
1023
+ /** Dead Letter Queue namespace. */
1024
+ dlq;
1025
+ /** System health/readiness namespace. */
1026
+ sys;
1027
+ /** Named query pipes admin namespace. */
1028
+ pipes;
1029
+ constructor(config) {
1030
+ this._ctx = {
1031
+ baseURL: config.baseURL.replace(/\/+$/, ""),
1032
+ auth: config.auth,
1033
+ options: {
1034
+ maxRetries: config.options?.maxRetries ?? 2
1035
+ }
1036
+ };
1037
+ this.schema = new SchemaNamespace(this._ctx);
1038
+ this.policy = new PolicyNamespace(this._ctx);
1039
+ this.dlq = new DLQNamespace(this._ctx, (table, opts) => this._createStream(table, opts));
1040
+ this.sys = new SysNamespace(this._ctx);
1041
+ this.pipes = new PipesNamespace(this._ctx);
1042
+ }
1043
+ /** Get a table reference for building queries, inserts, and streams. */
1044
+ from(table) {
1045
+ return new TableRef(
1046
+ this._ctx,
1047
+ table,
1048
+ (t, opts) => this._createStream(t, opts)
1049
+ );
1050
+ }
1051
+ /** Get a reference to a named query pipe. PromiseLike — `await` it to execute. */
1052
+ pipe(name, params) {
1053
+ return new PipeRef(this._ctx, name, params, (t, opts) => this._createStream(t, opts));
1054
+ }
1055
+ /**
1056
+ * Execute a raw SQL query against ClickHouse. Requires the admin role (the
1057
+ * configured `admin_role`, `"admin"` by default; there is no separate
1058
+ * `service` role). The endpoint proxies straight to ClickHouse's HTTP
1059
+ * interface so any ClickHouse-accepted SQL works; positional `?` param
1060
+ * binding is NOT supported — inline literals or use the structured query
1061
+ * builder for safe binding. See sql.ts for details.
1062
+ */
1063
+ sql(query, opts) {
1064
+ if (Array.isArray(opts)) {
1065
+ throw new Error(
1066
+ "[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."
1067
+ );
1068
+ }
1069
+ return sql(this._ctx, query, opts);
1070
+ }
1071
+ /** @internal Create a stream for the given table. */
1072
+ _createStream(table, opts) {
1073
+ if (typeof EventSource === "undefined") {
1074
+ throw new Error(
1075
+ "[WaveHouse SDK] Native EventSource is not available in this environment. Please provide a global polyfill (e.g., `globalThis.EventSource = require('eventsource')`)."
1076
+ );
1077
+ }
1078
+ const transport = new SSETransport({
1079
+ baseURL: this._ctx.baseURL,
1080
+ table,
1081
+ since: opts?.since,
1082
+ auth: this._ctx.auth
1083
+ });
1084
+ const controller = new StreamController(transport);
1085
+ if (opts?.signal) controller.attachSignal(opts.signal);
1086
+ return controller;
1087
+ }
1088
+ };
1089
+ function createClient(config) {
1090
+ return new WaveHouseClient(config);
1091
+ }
1092
+ // Annotate the CommonJS export names for ESM import in node:
1093
+ 0 && (module.exports = {
1094
+ DLQNamespace,
1095
+ LiveQuery,
1096
+ PipeRef,
1097
+ PipesNamespace,
1098
+ PolicyNamespace,
1099
+ QueryBuilder,
1100
+ SchemaNamespace,
1101
+ StreamController,
1102
+ SysNamespace,
1103
+ TableRef,
1104
+ WaveHouseClient,
1105
+ createClient
1106
+ });
1107
+ //# sourceMappingURL=index.cjs.map