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