@superatomai/sdk-node 0.0.32 → 0.0.33-dsp
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +942 -942
- package/dist/index.d.mts +2674 -190
- package/dist/index.d.ts +2674 -190
- package/dist/index.js +22243 -7575
- package/dist/index.mjs +22182 -7529
- package/dist/userResponse/scripts/script-bootstrap.d.mts +2 -0
- package/dist/userResponse/scripts/script-bootstrap.d.ts +2 -0
- package/dist/userResponse/scripts/script-bootstrap.js +305 -0
- package/dist/userResponse/scripts/script-bootstrap.mjs +303 -0
- package/package.json +51 -49
- package/dist/index.js.map +0 -1
- package/dist/index.mjs.map +0 -1
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// src/userResponse/scripts/script-bootstrap.ts
|
|
4
|
+
var import_url = require("url");
|
|
5
|
+
|
|
6
|
+
// src/userResponse/scripts/script-ipc.ts
|
|
7
|
+
function encodeMessage(msg) {
|
|
8
|
+
return JSON.stringify(msg) + "\n";
|
|
9
|
+
}
|
|
10
|
+
var LineOverflowError = class extends Error {
|
|
11
|
+
constructor(buffered, limit) {
|
|
12
|
+
super(`IPC message exceeded ${limit} bytes without a newline (${buffered} buffered) \u2014 aborting to avoid unbounded memory.`);
|
|
13
|
+
this.buffered = buffered;
|
|
14
|
+
this.limit = limit;
|
|
15
|
+
this.name = "LineOverflowError";
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
var DEFAULT_MAX_IPC_BYTES = (() => {
|
|
19
|
+
const envVal = Number(process.env.SCRIPT_MAX_IPC_BYTES);
|
|
20
|
+
return Number.isFinite(envVal) && envVal > 0 ? Math.floor(envVal) : 64 * 1024 * 1024;
|
|
21
|
+
})();
|
|
22
|
+
var LineSplitter = class {
|
|
23
|
+
constructor(maxBytes = DEFAULT_MAX_IPC_BYTES) {
|
|
24
|
+
this.maxBytes = maxBytes;
|
|
25
|
+
this.buffer = "";
|
|
26
|
+
}
|
|
27
|
+
push(chunk) {
|
|
28
|
+
this.buffer += chunk;
|
|
29
|
+
const lines = [];
|
|
30
|
+
let idx;
|
|
31
|
+
while ((idx = this.buffer.indexOf("\n")) !== -1) {
|
|
32
|
+
const line = this.buffer.slice(0, idx);
|
|
33
|
+
this.buffer = this.buffer.slice(idx + 1);
|
|
34
|
+
if (line.length > 0) lines.push(line);
|
|
35
|
+
}
|
|
36
|
+
if (this.buffer.length > this.maxBytes) {
|
|
37
|
+
const buffered = this.buffer.length;
|
|
38
|
+
this.buffer = "";
|
|
39
|
+
throw new LineOverflowError(buffered, this.maxBytes);
|
|
40
|
+
}
|
|
41
|
+
return lines;
|
|
42
|
+
}
|
|
43
|
+
/** Flush any remaining partial data (useful on stream close) */
|
|
44
|
+
flush() {
|
|
45
|
+
if (this.buffer.length === 0) return null;
|
|
46
|
+
const rest = this.buffer;
|
|
47
|
+
this.buffer = "";
|
|
48
|
+
return rest;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// src/userResponse/scripts/script-bootstrap.ts
|
|
53
|
+
var SCRIPT_ARG = process.argv[2];
|
|
54
|
+
if (!SCRIPT_ARG) {
|
|
55
|
+
sendError("Bootstrap was invoked without a script path.", "compile");
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
var BRIDGE_URL = process.env.SCRIPT_BRIDGE_URL;
|
|
59
|
+
var executedQueries = [];
|
|
60
|
+
var PREVIEW_MAX_ROWS = 10;
|
|
61
|
+
var PREVIEW_MAX_CHARS = 200;
|
|
62
|
+
var QUERY_ROW_CAP = 250;
|
|
63
|
+
function previewRows(data) {
|
|
64
|
+
return data.slice(0, PREVIEW_MAX_ROWS).map((row) => {
|
|
65
|
+
if (!row || typeof row !== "object") return row;
|
|
66
|
+
const out = {};
|
|
67
|
+
for (const [k, v] of Object.entries(row)) {
|
|
68
|
+
out[k] = typeof v === "string" && v.length > PREVIEW_MAX_CHARS ? v.slice(0, PREVIEW_MAX_CHARS) + "\u2026" : v;
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
var initResolve = null;
|
|
74
|
+
var initPromise = new Promise((resolve) => {
|
|
75
|
+
initResolve = resolve;
|
|
76
|
+
});
|
|
77
|
+
function buildCtx(init) {
|
|
78
|
+
return {
|
|
79
|
+
now: init.now,
|
|
80
|
+
async query(toolId, sql) {
|
|
81
|
+
if (!BRIDGE_URL) throw new Error("SCRIPT_BRIDGE_URL is not set \u2014 script bridge server is not running");
|
|
82
|
+
const startedAt = Date.now();
|
|
83
|
+
send({ type: "stream", chunk: `
|
|
84
|
+
\u{1F4DD} **Querying ${toolId}:**
|
|
85
|
+
\`\`\`sql
|
|
86
|
+
${sql}
|
|
87
|
+
\`\`\`
|
|
88
|
+
|
|
89
|
+
` });
|
|
90
|
+
send({ type: "stream", chunk: `__QUERY_TIMER_START_Executing query__` });
|
|
91
|
+
const res = await fetch(`${BRIDGE_URL}/query`, {
|
|
92
|
+
method: "POST",
|
|
93
|
+
headers: { "content-type": "application/json" },
|
|
94
|
+
body: JSON.stringify({ toolId, sql, limit: QUERY_ROW_CAP })
|
|
95
|
+
});
|
|
96
|
+
const json = await res.json();
|
|
97
|
+
const executionTimeMs = Date.now() - startedAt;
|
|
98
|
+
send({ type: "stream", chunk: `__QUERY_TIMER_DONE_${(executionTimeMs / 1e3).toFixed(1)}__
|
|
99
|
+
|
|
100
|
+
` });
|
|
101
|
+
if (!res.ok || !json.success) {
|
|
102
|
+
const errMsg = json.error || `Bridge query failed (${res.status})`;
|
|
103
|
+
send({ type: "stream", chunk: `\u274C **Query failed on ${toolId}:** ${errMsg}
|
|
104
|
+
|
|
105
|
+
` });
|
|
106
|
+
throw new Error(errMsg);
|
|
107
|
+
}
|
|
108
|
+
if (json.metadata?.truncated) {
|
|
109
|
+
const total = json.metadata.totalCount ?? "more";
|
|
110
|
+
const hasGroupBy = /\bGROUP\s+BY\b/i.test(sql);
|
|
111
|
+
if (hasGroupBy) {
|
|
112
|
+
send({ type: "stream", chunk: `\u2139\uFE0F Only the top ${QUERY_ROW_CAP} of ${total} grouped rows are returned (query is already aggregated via GROUP BY) \u2014 expected for a top-N request.
|
|
113
|
+
|
|
114
|
+
` });
|
|
115
|
+
} else {
|
|
116
|
+
const msg = `ctx.query truncated the result of this query to ${QUERY_ROW_CAP} rows (it matched ${total}). Do NOT compute over raw rows \u2014 AGGREGATE IN SQL (GROUP BY / mode() / mode(...) FILTER (WHERE \u2026) / COUNT(...) FILTER (\u2026) / CASE) so the query returns the final, already-reduced rows, then have getData return those.`;
|
|
117
|
+
send({ type: "stream", chunk: `\u274C **Result truncated on ${toolId}:** ${msg}
|
|
118
|
+
|
|
119
|
+
` });
|
|
120
|
+
throw new Error(msg);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const data = json.data ?? [];
|
|
124
|
+
const count = json.count ?? data.length;
|
|
125
|
+
send({ type: "stream", chunk: `\u2705 **${count} rows from ${toolId}**
|
|
126
|
+
|
|
127
|
+
` });
|
|
128
|
+
if (data.length > 0) {
|
|
129
|
+
send({ type: "stream", chunk: `<DataTable>${JSON.stringify(previewRows(data))}</DataTable>
|
|
130
|
+
|
|
131
|
+
` });
|
|
132
|
+
}
|
|
133
|
+
executedQueries.push({
|
|
134
|
+
sourceId: toolId,
|
|
135
|
+
sourceName: toolId,
|
|
136
|
+
// enriched by runner after result arrives
|
|
137
|
+
sql,
|
|
138
|
+
data,
|
|
139
|
+
count,
|
|
140
|
+
executionTimeMs
|
|
141
|
+
});
|
|
142
|
+
return { data, count };
|
|
143
|
+
},
|
|
144
|
+
/**
|
|
145
|
+
* Call a tool with structured params (typically a direct tool whose
|
|
146
|
+
* fn(params) does not expect a SQL string). Returns whatever the tool
|
|
147
|
+
* returned — shape is up to the tool.
|
|
148
|
+
*
|
|
149
|
+
* Use this when you need a pre-built business-logic tool (e.g.,
|
|
150
|
+
* "Calculate Shelf Life") that takes structured input rather than SQL.
|
|
151
|
+
* For SQL source tools, keep using ctx.query.
|
|
152
|
+
*
|
|
153
|
+
* runTool results do NOT auto-flow into executedQueries. If you want
|
|
154
|
+
* the result visualised, register it via ctx.emit() after extracting
|
|
155
|
+
* the array you want to chart.
|
|
156
|
+
*/
|
|
157
|
+
async runTool(toolId, params = {}) {
|
|
158
|
+
if (!BRIDGE_URL) throw new Error("SCRIPT_BRIDGE_URL is not set \u2014 script bridge server is not running");
|
|
159
|
+
const res = await fetch(`${BRIDGE_URL}/run-tool`, {
|
|
160
|
+
method: "POST",
|
|
161
|
+
headers: { "content-type": "application/json" },
|
|
162
|
+
body: JSON.stringify({ toolId, params })
|
|
163
|
+
});
|
|
164
|
+
const json = await res.json();
|
|
165
|
+
if (!res.ok || !json.success) throw new Error(json.error || `Bridge run-tool failed (${res.status})`);
|
|
166
|
+
return json.result;
|
|
167
|
+
},
|
|
168
|
+
/**
|
|
169
|
+
* Register a computed dataset. Flows into executedQueries alongside
|
|
170
|
+
* real SQL query results so component generation can render post-SQL
|
|
171
|
+
* transforms (weighted averages, clustering, joins) as charts.
|
|
172
|
+
*/
|
|
173
|
+
emit(name, rows, meta) {
|
|
174
|
+
executedQueries.push({
|
|
175
|
+
sourceId: `computed:${name}`,
|
|
176
|
+
sourceName: meta?.description || name,
|
|
177
|
+
sql: `-- computed dataset: ${name}`,
|
|
178
|
+
data: Array.isArray(rows) ? rows : [],
|
|
179
|
+
count: Array.isArray(rows) ? rows.length : 0,
|
|
180
|
+
executionTimeMs: 0,
|
|
181
|
+
virtual: true
|
|
182
|
+
});
|
|
183
|
+
},
|
|
184
|
+
/**
|
|
185
|
+
* Forward a short progress message to the parent's StreamBuffer so the UI
|
|
186
|
+
* continues to see per-script feedback during execution.
|
|
187
|
+
*/
|
|
188
|
+
log(chunk) {
|
|
189
|
+
send({ type: "stream", chunk });
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function send(msg) {
|
|
194
|
+
return process.stdout.write(encodeMessage(msg));
|
|
195
|
+
}
|
|
196
|
+
function sendError(message, phase, stack) {
|
|
197
|
+
send({ type: "error", message, stack, phase });
|
|
198
|
+
}
|
|
199
|
+
var splitter = new LineSplitter();
|
|
200
|
+
process.stdin.setEncoding("utf-8");
|
|
201
|
+
process.stdin.on("data", (chunk) => {
|
|
202
|
+
let lines;
|
|
203
|
+
try {
|
|
204
|
+
lines = splitter.push(chunk);
|
|
205
|
+
} catch (err) {
|
|
206
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
207
|
+
sendError(msg, "runtime");
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
for (const line of lines) {
|
|
211
|
+
let msg;
|
|
212
|
+
try {
|
|
213
|
+
msg = JSON.parse(line);
|
|
214
|
+
} catch {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
handleMessage(msg);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
function handleMessage(msg) {
|
|
221
|
+
switch (msg.type) {
|
|
222
|
+
case "init": {
|
|
223
|
+
if (initResolve) {
|
|
224
|
+
const now = new Date(msg.now || (/* @__PURE__ */ new Date()).toISOString());
|
|
225
|
+
const params = restoreParamTypes(msg.params || {}, msg.paramSchema);
|
|
226
|
+
initResolve({ params, now });
|
|
227
|
+
initResolve = null;
|
|
228
|
+
}
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
(async () => {
|
|
234
|
+
let getData;
|
|
235
|
+
try {
|
|
236
|
+
const mod = await import((0, import_url.pathToFileURL)(SCRIPT_ARG).href);
|
|
237
|
+
getData = mod.getData ?? (typeof mod.default === "function" ? mod.default : mod.default?.getData);
|
|
238
|
+
if (typeof getData !== "function") {
|
|
239
|
+
throw new Error(
|
|
240
|
+
`Script at ${SCRIPT_ARG} does not export getData \u2014 expected \`export async function getData(ctx, params)\``
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
} catch (err) {
|
|
244
|
+
const e = err;
|
|
245
|
+
sendError(e.message || String(err), "compile", e.stack);
|
|
246
|
+
process.exit(1);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const init = await initPromise;
|
|
250
|
+
const ctx = buildCtx(init);
|
|
251
|
+
const startedAt = Date.now();
|
|
252
|
+
try {
|
|
253
|
+
const raw = await getData(ctx, init.params);
|
|
254
|
+
const executionTimeMs = Date.now() - startedAt;
|
|
255
|
+
const data = normalizeResultData(raw);
|
|
256
|
+
const count = typeof raw?.count === "number" ? raw.count : data.length;
|
|
257
|
+
const sampleRows = data.slice(0, 5);
|
|
258
|
+
const result = {
|
|
259
|
+
type: "result",
|
|
260
|
+
data,
|
|
261
|
+
count,
|
|
262
|
+
executedQueries,
|
|
263
|
+
executionTimeMs,
|
|
264
|
+
sampleRows
|
|
265
|
+
};
|
|
266
|
+
const flushed = send(result);
|
|
267
|
+
if (flushed) {
|
|
268
|
+
process.exit(0);
|
|
269
|
+
} else {
|
|
270
|
+
process.stdout.once("drain", () => process.exit(0));
|
|
271
|
+
}
|
|
272
|
+
} catch (err) {
|
|
273
|
+
const e = err;
|
|
274
|
+
sendError(e.message || String(err), "runtime", e.stack);
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
})();
|
|
278
|
+
function normalizeResultData(raw) {
|
|
279
|
+
if (!raw) return [];
|
|
280
|
+
if (Array.isArray(raw)) return raw;
|
|
281
|
+
if (Array.isArray(raw.data)) return raw.data;
|
|
282
|
+
if (raw.data && typeof raw.data === "object") return [raw.data];
|
|
283
|
+
return [];
|
|
284
|
+
}
|
|
285
|
+
function restoreParamTypes(params, schema) {
|
|
286
|
+
if (!schema) return params;
|
|
287
|
+
const out = { ...params };
|
|
288
|
+
for (const [name, type] of Object.entries(schema)) {
|
|
289
|
+
const v = out[name];
|
|
290
|
+
if (v === void 0 || v === null) continue;
|
|
291
|
+
if (type === "date" && typeof v === "string") {
|
|
292
|
+
const d = new Date(v);
|
|
293
|
+
if (!isNaN(d.getTime())) out[name] = d;
|
|
294
|
+
} else if (type === "date_range" && typeof v === "object") {
|
|
295
|
+
const from = v.from;
|
|
296
|
+
const to = v.to;
|
|
297
|
+
out[name] = {
|
|
298
|
+
from: typeof from === "string" ? new Date(from) : from,
|
|
299
|
+
to: typeof to === "string" ? new Date(to) : to
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return out;
|
|
304
|
+
}
|
|
305
|
+
//# sourceMappingURL=script-bootstrap.js.map
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
// src/userResponse/scripts/script-bootstrap.ts
|
|
2
|
+
import { pathToFileURL } from "url";
|
|
3
|
+
|
|
4
|
+
// src/userResponse/scripts/script-ipc.ts
|
|
5
|
+
function encodeMessage(msg) {
|
|
6
|
+
return JSON.stringify(msg) + "\n";
|
|
7
|
+
}
|
|
8
|
+
var LineOverflowError = class extends Error {
|
|
9
|
+
constructor(buffered, limit) {
|
|
10
|
+
super(`IPC message exceeded ${limit} bytes without a newline (${buffered} buffered) \u2014 aborting to avoid unbounded memory.`);
|
|
11
|
+
this.buffered = buffered;
|
|
12
|
+
this.limit = limit;
|
|
13
|
+
this.name = "LineOverflowError";
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
var DEFAULT_MAX_IPC_BYTES = (() => {
|
|
17
|
+
const envVal = Number(process.env.SCRIPT_MAX_IPC_BYTES);
|
|
18
|
+
return Number.isFinite(envVal) && envVal > 0 ? Math.floor(envVal) : 64 * 1024 * 1024;
|
|
19
|
+
})();
|
|
20
|
+
var LineSplitter = class {
|
|
21
|
+
constructor(maxBytes = DEFAULT_MAX_IPC_BYTES) {
|
|
22
|
+
this.maxBytes = maxBytes;
|
|
23
|
+
this.buffer = "";
|
|
24
|
+
}
|
|
25
|
+
push(chunk) {
|
|
26
|
+
this.buffer += chunk;
|
|
27
|
+
const lines = [];
|
|
28
|
+
let idx;
|
|
29
|
+
while ((idx = this.buffer.indexOf("\n")) !== -1) {
|
|
30
|
+
const line = this.buffer.slice(0, idx);
|
|
31
|
+
this.buffer = this.buffer.slice(idx + 1);
|
|
32
|
+
if (line.length > 0) lines.push(line);
|
|
33
|
+
}
|
|
34
|
+
if (this.buffer.length > this.maxBytes) {
|
|
35
|
+
const buffered = this.buffer.length;
|
|
36
|
+
this.buffer = "";
|
|
37
|
+
throw new LineOverflowError(buffered, this.maxBytes);
|
|
38
|
+
}
|
|
39
|
+
return lines;
|
|
40
|
+
}
|
|
41
|
+
/** Flush any remaining partial data (useful on stream close) */
|
|
42
|
+
flush() {
|
|
43
|
+
if (this.buffer.length === 0) return null;
|
|
44
|
+
const rest = this.buffer;
|
|
45
|
+
this.buffer = "";
|
|
46
|
+
return rest;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// src/userResponse/scripts/script-bootstrap.ts
|
|
51
|
+
var SCRIPT_ARG = process.argv[2];
|
|
52
|
+
if (!SCRIPT_ARG) {
|
|
53
|
+
sendError("Bootstrap was invoked without a script path.", "compile");
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
var BRIDGE_URL = process.env.SCRIPT_BRIDGE_URL;
|
|
57
|
+
var executedQueries = [];
|
|
58
|
+
var PREVIEW_MAX_ROWS = 10;
|
|
59
|
+
var PREVIEW_MAX_CHARS = 200;
|
|
60
|
+
var QUERY_ROW_CAP = 250;
|
|
61
|
+
function previewRows(data) {
|
|
62
|
+
return data.slice(0, PREVIEW_MAX_ROWS).map((row) => {
|
|
63
|
+
if (!row || typeof row !== "object") return row;
|
|
64
|
+
const out = {};
|
|
65
|
+
for (const [k, v] of Object.entries(row)) {
|
|
66
|
+
out[k] = typeof v === "string" && v.length > PREVIEW_MAX_CHARS ? v.slice(0, PREVIEW_MAX_CHARS) + "\u2026" : v;
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
var initResolve = null;
|
|
72
|
+
var initPromise = new Promise((resolve) => {
|
|
73
|
+
initResolve = resolve;
|
|
74
|
+
});
|
|
75
|
+
function buildCtx(init) {
|
|
76
|
+
return {
|
|
77
|
+
now: init.now,
|
|
78
|
+
async query(toolId, sql) {
|
|
79
|
+
if (!BRIDGE_URL) throw new Error("SCRIPT_BRIDGE_URL is not set \u2014 script bridge server is not running");
|
|
80
|
+
const startedAt = Date.now();
|
|
81
|
+
send({ type: "stream", chunk: `
|
|
82
|
+
\u{1F4DD} **Querying ${toolId}:**
|
|
83
|
+
\`\`\`sql
|
|
84
|
+
${sql}
|
|
85
|
+
\`\`\`
|
|
86
|
+
|
|
87
|
+
` });
|
|
88
|
+
send({ type: "stream", chunk: `__QUERY_TIMER_START_Executing query__` });
|
|
89
|
+
const res = await fetch(`${BRIDGE_URL}/query`, {
|
|
90
|
+
method: "POST",
|
|
91
|
+
headers: { "content-type": "application/json" },
|
|
92
|
+
body: JSON.stringify({ toolId, sql, limit: QUERY_ROW_CAP })
|
|
93
|
+
});
|
|
94
|
+
const json = await res.json();
|
|
95
|
+
const executionTimeMs = Date.now() - startedAt;
|
|
96
|
+
send({ type: "stream", chunk: `__QUERY_TIMER_DONE_${(executionTimeMs / 1e3).toFixed(1)}__
|
|
97
|
+
|
|
98
|
+
` });
|
|
99
|
+
if (!res.ok || !json.success) {
|
|
100
|
+
const errMsg = json.error || `Bridge query failed (${res.status})`;
|
|
101
|
+
send({ type: "stream", chunk: `\u274C **Query failed on ${toolId}:** ${errMsg}
|
|
102
|
+
|
|
103
|
+
` });
|
|
104
|
+
throw new Error(errMsg);
|
|
105
|
+
}
|
|
106
|
+
if (json.metadata?.truncated) {
|
|
107
|
+
const total = json.metadata.totalCount ?? "more";
|
|
108
|
+
const hasGroupBy = /\bGROUP\s+BY\b/i.test(sql);
|
|
109
|
+
if (hasGroupBy) {
|
|
110
|
+
send({ type: "stream", chunk: `\u2139\uFE0F Only the top ${QUERY_ROW_CAP} of ${total} grouped rows are returned (query is already aggregated via GROUP BY) \u2014 expected for a top-N request.
|
|
111
|
+
|
|
112
|
+
` });
|
|
113
|
+
} else {
|
|
114
|
+
const msg = `ctx.query truncated the result of this query to ${QUERY_ROW_CAP} rows (it matched ${total}). Do NOT compute over raw rows \u2014 AGGREGATE IN SQL (GROUP BY / mode() / mode(...) FILTER (WHERE \u2026) / COUNT(...) FILTER (\u2026) / CASE) so the query returns the final, already-reduced rows, then have getData return those.`;
|
|
115
|
+
send({ type: "stream", chunk: `\u274C **Result truncated on ${toolId}:** ${msg}
|
|
116
|
+
|
|
117
|
+
` });
|
|
118
|
+
throw new Error(msg);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const data = json.data ?? [];
|
|
122
|
+
const count = json.count ?? data.length;
|
|
123
|
+
send({ type: "stream", chunk: `\u2705 **${count} rows from ${toolId}**
|
|
124
|
+
|
|
125
|
+
` });
|
|
126
|
+
if (data.length > 0) {
|
|
127
|
+
send({ type: "stream", chunk: `<DataTable>${JSON.stringify(previewRows(data))}</DataTable>
|
|
128
|
+
|
|
129
|
+
` });
|
|
130
|
+
}
|
|
131
|
+
executedQueries.push({
|
|
132
|
+
sourceId: toolId,
|
|
133
|
+
sourceName: toolId,
|
|
134
|
+
// enriched by runner after result arrives
|
|
135
|
+
sql,
|
|
136
|
+
data,
|
|
137
|
+
count,
|
|
138
|
+
executionTimeMs
|
|
139
|
+
});
|
|
140
|
+
return { data, count };
|
|
141
|
+
},
|
|
142
|
+
/**
|
|
143
|
+
* Call a tool with structured params (typically a direct tool whose
|
|
144
|
+
* fn(params) does not expect a SQL string). Returns whatever the tool
|
|
145
|
+
* returned — shape is up to the tool.
|
|
146
|
+
*
|
|
147
|
+
* Use this when you need a pre-built business-logic tool (e.g.,
|
|
148
|
+
* "Calculate Shelf Life") that takes structured input rather than SQL.
|
|
149
|
+
* For SQL source tools, keep using ctx.query.
|
|
150
|
+
*
|
|
151
|
+
* runTool results do NOT auto-flow into executedQueries. If you want
|
|
152
|
+
* the result visualised, register it via ctx.emit() after extracting
|
|
153
|
+
* the array you want to chart.
|
|
154
|
+
*/
|
|
155
|
+
async runTool(toolId, params = {}) {
|
|
156
|
+
if (!BRIDGE_URL) throw new Error("SCRIPT_BRIDGE_URL is not set \u2014 script bridge server is not running");
|
|
157
|
+
const res = await fetch(`${BRIDGE_URL}/run-tool`, {
|
|
158
|
+
method: "POST",
|
|
159
|
+
headers: { "content-type": "application/json" },
|
|
160
|
+
body: JSON.stringify({ toolId, params })
|
|
161
|
+
});
|
|
162
|
+
const json = await res.json();
|
|
163
|
+
if (!res.ok || !json.success) throw new Error(json.error || `Bridge run-tool failed (${res.status})`);
|
|
164
|
+
return json.result;
|
|
165
|
+
},
|
|
166
|
+
/**
|
|
167
|
+
* Register a computed dataset. Flows into executedQueries alongside
|
|
168
|
+
* real SQL query results so component generation can render post-SQL
|
|
169
|
+
* transforms (weighted averages, clustering, joins) as charts.
|
|
170
|
+
*/
|
|
171
|
+
emit(name, rows, meta) {
|
|
172
|
+
executedQueries.push({
|
|
173
|
+
sourceId: `computed:${name}`,
|
|
174
|
+
sourceName: meta?.description || name,
|
|
175
|
+
sql: `-- computed dataset: ${name}`,
|
|
176
|
+
data: Array.isArray(rows) ? rows : [],
|
|
177
|
+
count: Array.isArray(rows) ? rows.length : 0,
|
|
178
|
+
executionTimeMs: 0,
|
|
179
|
+
virtual: true
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
/**
|
|
183
|
+
* Forward a short progress message to the parent's StreamBuffer so the UI
|
|
184
|
+
* continues to see per-script feedback during execution.
|
|
185
|
+
*/
|
|
186
|
+
log(chunk) {
|
|
187
|
+
send({ type: "stream", chunk });
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function send(msg) {
|
|
192
|
+
return process.stdout.write(encodeMessage(msg));
|
|
193
|
+
}
|
|
194
|
+
function sendError(message, phase, stack) {
|
|
195
|
+
send({ type: "error", message, stack, phase });
|
|
196
|
+
}
|
|
197
|
+
var splitter = new LineSplitter();
|
|
198
|
+
process.stdin.setEncoding("utf-8");
|
|
199
|
+
process.stdin.on("data", (chunk) => {
|
|
200
|
+
let lines;
|
|
201
|
+
try {
|
|
202
|
+
lines = splitter.push(chunk);
|
|
203
|
+
} catch (err) {
|
|
204
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
205
|
+
sendError(msg, "runtime");
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
for (const line of lines) {
|
|
209
|
+
let msg;
|
|
210
|
+
try {
|
|
211
|
+
msg = JSON.parse(line);
|
|
212
|
+
} catch {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
handleMessage(msg);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
function handleMessage(msg) {
|
|
219
|
+
switch (msg.type) {
|
|
220
|
+
case "init": {
|
|
221
|
+
if (initResolve) {
|
|
222
|
+
const now = new Date(msg.now || (/* @__PURE__ */ new Date()).toISOString());
|
|
223
|
+
const params = restoreParamTypes(msg.params || {}, msg.paramSchema);
|
|
224
|
+
initResolve({ params, now });
|
|
225
|
+
initResolve = null;
|
|
226
|
+
}
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
(async () => {
|
|
232
|
+
let getData;
|
|
233
|
+
try {
|
|
234
|
+
const mod = await import(pathToFileURL(SCRIPT_ARG).href);
|
|
235
|
+
getData = mod.getData ?? (typeof mod.default === "function" ? mod.default : mod.default?.getData);
|
|
236
|
+
if (typeof getData !== "function") {
|
|
237
|
+
throw new Error(
|
|
238
|
+
`Script at ${SCRIPT_ARG} does not export getData \u2014 expected \`export async function getData(ctx, params)\``
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
} catch (err) {
|
|
242
|
+
const e = err;
|
|
243
|
+
sendError(e.message || String(err), "compile", e.stack);
|
|
244
|
+
process.exit(1);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const init = await initPromise;
|
|
248
|
+
const ctx = buildCtx(init);
|
|
249
|
+
const startedAt = Date.now();
|
|
250
|
+
try {
|
|
251
|
+
const raw = await getData(ctx, init.params);
|
|
252
|
+
const executionTimeMs = Date.now() - startedAt;
|
|
253
|
+
const data = normalizeResultData(raw);
|
|
254
|
+
const count = typeof raw?.count === "number" ? raw.count : data.length;
|
|
255
|
+
const sampleRows = data.slice(0, 5);
|
|
256
|
+
const result = {
|
|
257
|
+
type: "result",
|
|
258
|
+
data,
|
|
259
|
+
count,
|
|
260
|
+
executedQueries,
|
|
261
|
+
executionTimeMs,
|
|
262
|
+
sampleRows
|
|
263
|
+
};
|
|
264
|
+
const flushed = send(result);
|
|
265
|
+
if (flushed) {
|
|
266
|
+
process.exit(0);
|
|
267
|
+
} else {
|
|
268
|
+
process.stdout.once("drain", () => process.exit(0));
|
|
269
|
+
}
|
|
270
|
+
} catch (err) {
|
|
271
|
+
const e = err;
|
|
272
|
+
sendError(e.message || String(err), "runtime", e.stack);
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
})();
|
|
276
|
+
function normalizeResultData(raw) {
|
|
277
|
+
if (!raw) return [];
|
|
278
|
+
if (Array.isArray(raw)) return raw;
|
|
279
|
+
if (Array.isArray(raw.data)) return raw.data;
|
|
280
|
+
if (raw.data && typeof raw.data === "object") return [raw.data];
|
|
281
|
+
return [];
|
|
282
|
+
}
|
|
283
|
+
function restoreParamTypes(params, schema) {
|
|
284
|
+
if (!schema) return params;
|
|
285
|
+
const out = { ...params };
|
|
286
|
+
for (const [name, type] of Object.entries(schema)) {
|
|
287
|
+
const v = out[name];
|
|
288
|
+
if (v === void 0 || v === null) continue;
|
|
289
|
+
if (type === "date" && typeof v === "string") {
|
|
290
|
+
const d = new Date(v);
|
|
291
|
+
if (!isNaN(d.getTime())) out[name] = d;
|
|
292
|
+
} else if (type === "date_range" && typeof v === "object") {
|
|
293
|
+
const from = v.from;
|
|
294
|
+
const to = v.to;
|
|
295
|
+
out[name] = {
|
|
296
|
+
from: typeof from === "string" ? new Date(from) : from,
|
|
297
|
+
to: typeof to === "string" ? new Date(to) : to
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return out;
|
|
302
|
+
}
|
|
303
|
+
//# sourceMappingURL=script-bootstrap.mjs.map
|