@vaultgradient/pipequery-cli 0.6.1 → 0.10.0

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.
Files changed (40) hide show
  1. package/README.md +98 -13
  2. package/__tests__/clickhouse-pushdown-compile.test.ts +121 -0
  3. package/__tests__/event-log.test.ts +267 -0
  4. package/__tests__/mongo-pushdown-compile.test.ts +160 -0
  5. package/__tests__/mysql-pushdown-compile.test.ts +122 -0
  6. package/__tests__/mysql-pushdown.smoke.test.ts +77 -0
  7. package/__tests__/postgres-pushdown-compile.test.ts +240 -0
  8. package/__tests__/postgres-pushdown.smoke.test.ts +158 -0
  9. package/__tests__/pushdown-routing.test.ts +144 -0
  10. package/__tests__/rest-source.test.ts +191 -0
  11. package/__tests__/snowflake-pushdown-compile.test.ts +74 -0
  12. package/__tests__/websocket-source.test.ts +187 -0
  13. package/dist/index.js +3570 -2044
  14. package/package.json +11 -2
  15. package/src/commands/telegram.ts +10 -0
  16. package/src/config/schema.ts +90 -1
  17. package/src/index.ts +2 -1
  18. package/src/mcp/provider.ts +4 -7
  19. package/src/server/routes/control.ts +1 -3
  20. package/src/server/routes/dynamic.ts +2 -4
  21. package/src/server/sources/clickhouse.ts +153 -0
  22. package/src/server/sources/kafka.ts +4 -16
  23. package/src/server/sources/manager.ts +71 -1
  24. package/src/server/sources/mongodb.ts +165 -0
  25. package/src/server/sources/mysql.ts +46 -14
  26. package/src/server/sources/postgres.ts +52 -20
  27. package/src/server/sources/pushdown/clickhouse.ts +17 -0
  28. package/src/server/sources/pushdown/compile.ts +328 -0
  29. package/src/server/sources/pushdown/dialect.ts +88 -0
  30. package/src/server/sources/pushdown/mongodb.ts +450 -0
  31. package/src/server/sources/pushdown/mysql.ts +17 -0
  32. package/src/server/sources/pushdown/postgres.ts +19 -0
  33. package/src/server/sources/pushdown/snowflake.ts +17 -0
  34. package/src/server/sources/rest.ts +33 -5
  35. package/src/server/sources/snowflake.ts +158 -0
  36. package/src/server/sources/types.ts +27 -0
  37. package/src/server/sources/websocket.ts +73 -9
  38. package/src/telegram/bot.ts +198 -12
  39. package/src/telegram/event-log.ts +234 -0
  40. package/src/utils/expandEnv.ts +23 -0
@@ -0,0 +1,187 @@
1
+ /**
2
+ * WebSocket source tests — covers the new subscribe-on-open + heartbeat
3
+ * behaviour from issue #35. Spins up a real `ws` server in-process so
4
+ * we exercise the actual JSON round-trip rather than mocking the socket.
5
+ */
6
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
7
+ import { WebSocketServer, type WebSocket as WSocket } from 'ws';
8
+ import type { AddressInfo } from 'node:net';
9
+ import { WebSocketSourceAdapter } from '../src/server/sources/websocket.js';
10
+
11
+ interface Harness {
12
+ url: string;
13
+ /** Messages received by the server, parsed as JSON when possible. */
14
+ received: unknown[];
15
+ /** First connected client — used to push messages back at the adapter. */
16
+ clientPromise: Promise<WSocket>;
17
+ /** Force-close the active client connection (simulates a server restart). */
18
+ forceCloseClient: () => void;
19
+ close: () => Promise<void>;
20
+ }
21
+
22
+ async function makeServer(): Promise<Harness> {
23
+ const wss = new WebSocketServer({ port: 0 });
24
+ const received: unknown[] = [];
25
+ let resolveClient!: (ws: WSocket) => void;
26
+ const clientPromise = new Promise<WSocket>((res) => {
27
+ resolveClient = res;
28
+ });
29
+ let activeClient: WSocket | null = null;
30
+
31
+ wss.on('connection', (ws) => {
32
+ activeClient = ws;
33
+ ws.on('message', (raw) => {
34
+ try {
35
+ received.push(JSON.parse(raw.toString()));
36
+ } catch {
37
+ received.push(raw.toString());
38
+ }
39
+ });
40
+ resolveClient(ws);
41
+ });
42
+
43
+ await new Promise<void>((res) => wss.once('listening', res));
44
+ const port = (wss.address() as AddressInfo).port;
45
+ return {
46
+ url: `ws://127.0.0.1:${port}`,
47
+ received,
48
+ clientPromise,
49
+ forceCloseClient: () => activeClient?.close(),
50
+ close: () =>
51
+ new Promise<void>((res) => {
52
+ for (const c of wss.clients) c.terminate();
53
+ wss.close(() => res());
54
+ }),
55
+ };
56
+ }
57
+
58
+ describe('WebSocketSourceAdapter — subscribe + heartbeat', () => {
59
+ let server: Harness;
60
+ beforeEach(async () => {
61
+ server = await makeServer();
62
+ });
63
+ afterEach(async () => {
64
+ await server.close();
65
+ });
66
+
67
+ it('sends a single subscribe payload immediately after open', async () => {
68
+ const adapter = new WebSocketSourceAdapter({
69
+ type: 'websocket',
70
+ url: server.url,
71
+ subscribe: { method: 'SUBSCRIBE', params: ['btcusdt@ticker'], id: 1 },
72
+ });
73
+ await adapter.start();
74
+ await server.clientPromise;
75
+ // Give the server a tick to flush the message into `received`.
76
+ await new Promise((r) => setTimeout(r, 50));
77
+ expect(server.received).toEqual([
78
+ { method: 'SUBSCRIBE', params: ['btcusdt@ticker'], id: 1 },
79
+ ]);
80
+ adapter.stop();
81
+ });
82
+
83
+ it('sends an array of subscribe payloads in order', async () => {
84
+ const adapter = new WebSocketSourceAdapter({
85
+ type: 'websocket',
86
+ url: server.url,
87
+ subscribe: [
88
+ { method: 'SUBSCRIBE', params: ['btc@ticker'], id: 1 },
89
+ { method: 'SUBSCRIBE', params: ['eth@ticker'], id: 2 },
90
+ { method: 'SUBSCRIBE', params: ['sol@ticker'], id: 3 },
91
+ ],
92
+ });
93
+ await adapter.start();
94
+ await server.clientPromise;
95
+ await new Promise((r) => setTimeout(r, 50));
96
+ expect(server.received).toHaveLength(3);
97
+ expect((server.received[0] as { id: number }).id).toBe(1);
98
+ expect((server.received[1] as { id: number }).id).toBe(2);
99
+ expect((server.received[2] as { id: number }).id).toBe(3);
100
+ adapter.stop();
101
+ });
102
+
103
+ it('sends nothing when subscribe is omitted (back-compat)', async () => {
104
+ const adapter = new WebSocketSourceAdapter({
105
+ type: 'websocket',
106
+ url: server.url,
107
+ });
108
+ await adapter.start();
109
+ await server.clientPromise;
110
+ await new Promise((r) => setTimeout(r, 50));
111
+ expect(server.received).toEqual([]);
112
+ adapter.stop();
113
+ });
114
+
115
+ it('emits heartbeat payloads on the configured second-resolution interval', async () => {
116
+ const adapter = new WebSocketSourceAdapter({
117
+ type: 'websocket',
118
+ url: server.url,
119
+ heartbeat: { payload: { method: 'PING' }, interval: '1s' },
120
+ });
121
+ await adapter.start();
122
+ await server.clientPromise;
123
+ // Wait long enough for at least 2 ticks plus the open-time slack.
124
+ await new Promise((r) => setTimeout(r, 2300));
125
+ const pings = server.received.filter(
126
+ (m) => typeof m === 'object' && m !== null && (m as { method?: string }).method === 'PING',
127
+ );
128
+ expect(pings.length).toBeGreaterThanOrEqual(2);
129
+ adapter.stop();
130
+ }, 5000);
131
+
132
+ it('re-sends subscribe payloads on reconnect', async () => {
133
+ const adapter = new WebSocketSourceAdapter({
134
+ type: 'websocket',
135
+ url: server.url,
136
+ subscribe: { method: 'SUBSCRIBE', params: ['btc@ticker'], id: 1 },
137
+ });
138
+ await adapter.start();
139
+ await server.clientPromise;
140
+ await new Promise((r) => setTimeout(r, 50));
141
+ expect(server.received).toHaveLength(1);
142
+
143
+ // Server force-closes the client; adapter's internal reconnect (5s
144
+ // delay) should fire and re-run start(), which re-sends subscribe.
145
+ server.forceCloseClient();
146
+ await new Promise((r) => setTimeout(r, 5500));
147
+ expect(server.received.length).toBeGreaterThanOrEqual(2);
148
+ expect((server.received[server.received.length - 1] as { method: string }).method).toBe(
149
+ 'SUBSCRIBE',
150
+ );
151
+ adapter.stop();
152
+ }, 10_000);
153
+
154
+ it('stops heartbeat timer and does not reconnect after stop()', async () => {
155
+ const adapter = new WebSocketSourceAdapter({
156
+ type: 'websocket',
157
+ url: server.url,
158
+ subscribe: { method: 'SUBSCRIBE', params: ['x'], id: 1 },
159
+ heartbeat: { payload: { method: 'PING' }, interval: '1s' },
160
+ });
161
+ await adapter.start();
162
+ await server.clientPromise;
163
+ await new Promise((r) => setTimeout(r, 100));
164
+ const beforeStop = server.received.length;
165
+
166
+ adapter.stop();
167
+ // Wait through one would-be reconnect window + a few heartbeat ticks.
168
+ await new Promise((r) => setTimeout(r, 6000));
169
+ // No new SUBSCRIBE re-sent (would indicate reconnect happened) and
170
+ // no new PING ticks (heartbeat would have kept firing).
171
+ expect(server.received.length).toBe(beforeStop);
172
+ }, 10_000);
173
+
174
+ it('still ingests messages from the server (no regression to base behaviour)', async () => {
175
+ const adapter = new WebSocketSourceAdapter({
176
+ type: 'websocket',
177
+ url: server.url,
178
+ });
179
+ await adapter.start();
180
+ const client = await server.clientPromise;
181
+ client.send(JSON.stringify({ price: 100 }));
182
+ client.send(JSON.stringify({ price: 101 }));
183
+ await new Promise((r) => setTimeout(r, 50));
184
+ expect(adapter.getData()).toEqual([{ price: 100 }, { price: 101 }]);
185
+ adapter.stop();
186
+ });
187
+ });