@querypanel/node-sdk 1.3.0-beta.2 → 1.3.1
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 +30 -2
- package/dist/index.cjs +142 -50
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -17
- package/dist/index.d.ts +40 -17
- package/dist/index.js +142 -50
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -286,9 +286,37 @@ const { data, missingIds } = await qp.getChartsByIds(
|
|
|
286
286
|
Saved charts now include the original `prompt` so you can show the question
|
|
287
287
|
alongside chart history or reuse it in follow-up workflows.
|
|
288
288
|
|
|
289
|
+
## Referencing Queries by `queryId` (recommended)
|
|
290
|
+
|
|
291
|
+
`ask()` and `modifyChart()` return a server-issued `queryId`. The server stores the generated SQL and bind params for that id, scoped to the authenticated workspace and tenant. **Pass the id instead of SQL** so your clients never send executable SQL or bind params:
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
const response = await qp.ask("revenue by country", { tenantId, database: "analytics" });
|
|
295
|
+
|
|
296
|
+
// Modify: SQL and params are loaded from the server, client `sql` / `params` are ignored
|
|
297
|
+
const modified = await qp.modifyChart(
|
|
298
|
+
{ queryId: response.queryId, question: "revenue by country", database: "analytics",
|
|
299
|
+
vizModifications: { chartType: "bar" } },
|
|
300
|
+
{ tenantId },
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
// Run a stored query (e.g. in a dashboard)
|
|
304
|
+
await qp.runSqlForDashboard({ queryId: response.queryId, database: "analytics" }, { tenantId });
|
|
305
|
+
|
|
306
|
+
// Save a chart: the server stores the SQL and params it holds for `query_id`
|
|
307
|
+
await qp.createChart({ title: "Revenue", query_id: modified.queryId, vega_lite_spec: spec }, { tenantId });
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
- When `queryId` / `query_id` is present it **takes priority**: any `sql` / `sql_params` you also send is ignored.
|
|
311
|
+
- An id that belongs to another tenant, or does not exist, is rejected (`404`).
|
|
312
|
+
- `sql` is still accepted when no id is sent, for backward compatibility. Charts saved without a `query_id` (for example older charts) keep using their stored SQL.
|
|
313
|
+
- `modifyChart()` returns the input `queryId` when only visualization changes, and a new `queryId` when SQL is regenerated. Custom SQL (`customSql`) has no id.
|
|
314
|
+
- Generation fails if the server cannot record the query, so a successful `ask()` always returns a `queryId`.
|
|
315
|
+
- Requires an API that supports `GET /v2/queries/:id`; upgrade the API before the SDK.
|
|
316
|
+
|
|
289
317
|
## Modifying Charts
|
|
290
318
|
|
|
291
|
-
The `modifyChart()` method allows you to edit SQL and/or visualization settings, then re-execute and regenerate charts. It works with both fresh `ask()` responses and saved charts.
|
|
319
|
+
The `modifyChart()` method allows you to edit SQL and/or visualization settings, then re-execute and regenerate charts. It works with both fresh `ask()` responses and saved charts. Prefer `queryId` over `sql` (see above).
|
|
292
320
|
|
|
293
321
|
### Changing Visualization Settings
|
|
294
322
|
|
|
@@ -516,7 +544,7 @@ Every request is signed with `RS256` using the private key you pass to the const
|
|
|
516
544
|
|
|
517
545
|
- HTTP errors propagate as thrown `Error` instances that include `status` (and `details` when available).
|
|
518
546
|
- `syncSchema` automatically skips embedding if the schema hasn't changed (drift detection). Use `syncSchema(..., { forceReindex: true })` to force updates.
|
|
519
|
-
- `ask()` raises immediately for guardrail/moderation errors because `/query` responds with 4xx/5xx.
|
|
547
|
+
- `ask()` raises immediately for guardrail/moderation errors because `/v2/query` responds with 4xx/5xx. `pipeline: "v1"` is not accepted.
|
|
520
548
|
|
|
521
549
|
### Automatic SQL repair and retry
|
|
522
550
|
|
package/dist/index.cjs
CHANGED
|
@@ -61,6 +61,13 @@ var BigQueryAdapter = class {
|
|
|
61
61
|
);
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
+
clientFn;
|
|
65
|
+
projectId;
|
|
66
|
+
datasetProjectId;
|
|
67
|
+
defaultDataset;
|
|
68
|
+
databaseName;
|
|
69
|
+
kind;
|
|
70
|
+
allowedTables;
|
|
64
71
|
async execute(sql, params) {
|
|
65
72
|
if (this.allowedTables) {
|
|
66
73
|
this.validateQueryTables(sql);
|
|
@@ -291,6 +298,11 @@ var ClickHouseAdapter = class {
|
|
|
291
298
|
this.allowedTables = normalizeTableFilter2(options.allowedTables);
|
|
292
299
|
}
|
|
293
300
|
}
|
|
301
|
+
clientFn;
|
|
302
|
+
databaseName;
|
|
303
|
+
defaultFormat;
|
|
304
|
+
kind;
|
|
305
|
+
allowedTables;
|
|
294
306
|
async execute(sql, params) {
|
|
295
307
|
if (this.allowedTables) {
|
|
296
308
|
this.validateQueryTables(sql);
|
|
@@ -544,6 +556,11 @@ var PostgresAdapter = class {
|
|
|
544
556
|
);
|
|
545
557
|
}
|
|
546
558
|
}
|
|
559
|
+
clientFn;
|
|
560
|
+
databaseName;
|
|
561
|
+
defaultSchema;
|
|
562
|
+
kind;
|
|
563
|
+
allowedTables;
|
|
547
564
|
async execute(sql, params) {
|
|
548
565
|
if (this.allowedTables) {
|
|
549
566
|
this.validateQueryTables(sql);
|
|
@@ -852,6 +869,9 @@ var QueryPipelineError = class extends Error {
|
|
|
852
869
|
this.status = status;
|
|
853
870
|
this.name = "QueryPipelineError";
|
|
854
871
|
}
|
|
872
|
+
code;
|
|
873
|
+
details;
|
|
874
|
+
status;
|
|
855
875
|
/**
|
|
856
876
|
* Check if this is a moderation error
|
|
857
877
|
*/
|
|
@@ -904,8 +924,14 @@ var QueryPipelineError = class extends Error {
|
|
|
904
924
|
|
|
905
925
|
// src/core/client.ts
|
|
906
926
|
var ApiClient = class {
|
|
927
|
+
baseUrl;
|
|
928
|
+
privateKey;
|
|
929
|
+
organizationId;
|
|
930
|
+
defaultTenantId;
|
|
931
|
+
additionalHeaders;
|
|
932
|
+
fetchImpl;
|
|
933
|
+
cryptoKey = null;
|
|
907
934
|
constructor(baseUrl, privateKey, organizationId, options) {
|
|
908
|
-
this.cryptoKey = null;
|
|
909
935
|
if (!baseUrl) {
|
|
910
936
|
throw new Error("Base URL is required");
|
|
911
937
|
}
|
|
@@ -1159,6 +1185,8 @@ var ApiClient = class {
|
|
|
1159
1185
|
|
|
1160
1186
|
// src/core/callback-client.ts
|
|
1161
1187
|
var CallbackApiClient = class {
|
|
1188
|
+
requestHandler;
|
|
1189
|
+
defaultTenantId;
|
|
1162
1190
|
constructor(requestHandler, options) {
|
|
1163
1191
|
this.requestHandler = requestHandler;
|
|
1164
1192
|
this.defaultTenantId = options?.defaultTenantId;
|
|
@@ -1301,10 +1329,9 @@ function normalizeSqlParamsInput(params) {
|
|
|
1301
1329
|
|
|
1302
1330
|
// src/core/query-engine.ts
|
|
1303
1331
|
var QueryEngine = class {
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
}
|
|
1332
|
+
databases = /* @__PURE__ */ new Map();
|
|
1333
|
+
databaseMetadata = /* @__PURE__ */ new Map();
|
|
1334
|
+
defaultDatabase;
|
|
1308
1335
|
attachDatabase(name, adapter, metadata) {
|
|
1309
1336
|
this.databases.set(name, adapter);
|
|
1310
1337
|
this.databaseMetadata.set(name, metadata);
|
|
@@ -1990,7 +2017,13 @@ var import_node_crypto4 = __toESM(require("crypto"), 1);
|
|
|
1990
2017
|
// src/routes/query.ts
|
|
1991
2018
|
var import_node_crypto3 = __toESM(require("crypto"), 1);
|
|
1992
2019
|
function resolveQueryPipeline(pipeline) {
|
|
1993
|
-
|
|
2020
|
+
const requested = pipeline;
|
|
2021
|
+
if (requested != null && requested !== "v2") {
|
|
2022
|
+
throw new Error(
|
|
2023
|
+
`Query pipeline "${requested}" was removed. ask() and modifyChart() always use POST /v2/query.`
|
|
2024
|
+
);
|
|
2025
|
+
}
|
|
2026
|
+
return "v2";
|
|
1994
2027
|
}
|
|
1995
2028
|
async function ask(client, queryEngine, question, options, signal) {
|
|
1996
2029
|
const tenantId = resolveTenantId5(client, options.tenantId);
|
|
@@ -2000,8 +2033,9 @@ async function ask(client, queryEngine, question, options, signal) {
|
|
|
2000
2033
|
let attempt = 0;
|
|
2001
2034
|
let lastError = options.lastError;
|
|
2002
2035
|
let previousSql = options.previousSql;
|
|
2003
|
-
|
|
2004
|
-
|
|
2036
|
+
let parentQueryId;
|
|
2037
|
+
resolveQueryPipeline(options.pipeline);
|
|
2038
|
+
const queryEndpoint = "/v2/query";
|
|
2005
2039
|
while (attempt <= maxRetry) {
|
|
2006
2040
|
const databaseName = options.database ?? queryEngine.getDefaultDatabase();
|
|
2007
2041
|
const metadata = databaseName ? queryEngine.getDatabaseMetadata(databaseName) : void 0;
|
|
@@ -2031,7 +2065,8 @@ async function ask(client, queryEngine, question, options, signal) {
|
|
|
2031
2065
|
...databaseName ? { database: databaseName } : {},
|
|
2032
2066
|
...metadata?.dialect ? { dialect: metadata.dialect } : {},
|
|
2033
2067
|
...options.model?.trim() ? { model: options.model.trim() } : {},
|
|
2034
|
-
...
|
|
2068
|
+
...options.systemPrompt?.trim() ? { system_prompt: options.systemPrompt.trim() } : {},
|
|
2069
|
+
...parentQueryId ? { parent_query_id: parentQueryId } : {},
|
|
2035
2070
|
debug: options.debug ?? false
|
|
2036
2071
|
},
|
|
2037
2072
|
tenantId,
|
|
@@ -2053,6 +2088,9 @@ async function ask(client, queryEngine, question, options, signal) {
|
|
|
2053
2088
|
if (!sql || !dialect) {
|
|
2054
2089
|
throw new Error("Query response missing required SQL or dialect");
|
|
2055
2090
|
}
|
|
2091
|
+
if (queryResponse.data.queryId) {
|
|
2092
|
+
parentQueryId = queryResponse.data.queryId;
|
|
2093
|
+
}
|
|
2056
2094
|
const dbName = queryResponse.data.database ?? options.database ?? queryEngine.getDefaultDatabase();
|
|
2057
2095
|
if (!dbName) {
|
|
2058
2096
|
throw new Error(
|
|
@@ -2194,34 +2232,53 @@ function anonymizeResults(rows) {
|
|
|
2194
2232
|
});
|
|
2195
2233
|
}
|
|
2196
2234
|
|
|
2235
|
+
// src/routes/queries.ts
|
|
2236
|
+
async function resolveQuery(client, queryEngine, queryId, options, signal) {
|
|
2237
|
+
const response = await client.get(
|
|
2238
|
+
`/v2/queries/${encodeURIComponent(queryId)}`,
|
|
2239
|
+
options.tenantId,
|
|
2240
|
+
options.userId,
|
|
2241
|
+
options.scopes,
|
|
2242
|
+
signal
|
|
2243
|
+
);
|
|
2244
|
+
const paramMetadata = Array.isArray(response.params) ? response.params : [];
|
|
2245
|
+
return {
|
|
2246
|
+
queryId: response.queryId,
|
|
2247
|
+
sql: response.sql,
|
|
2248
|
+
params: queryEngine.mapGeneratedParams(paramMetadata),
|
|
2249
|
+
paramMetadata,
|
|
2250
|
+
dialect: response.dialect
|
|
2251
|
+
};
|
|
2252
|
+
}
|
|
2253
|
+
async function markQueryExecuted(client, queryId, options, signal) {
|
|
2254
|
+
try {
|
|
2255
|
+
await client.post(
|
|
2256
|
+
`/v2/queries/${encodeURIComponent(queryId)}/executed`,
|
|
2257
|
+
{},
|
|
2258
|
+
options.tenantId,
|
|
2259
|
+
options.userId,
|
|
2260
|
+
options.scopes,
|
|
2261
|
+
signal
|
|
2262
|
+
);
|
|
2263
|
+
} catch {
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2197
2267
|
// src/routes/modify.ts
|
|
2198
|
-
function buildModifiedQuestion(originalQuestion, modifications,
|
|
2268
|
+
function buildModifiedQuestion(originalQuestion, modifications, hasSessionContext = false) {
|
|
2199
2269
|
const hints = [];
|
|
2200
2270
|
if (modifications.timeGranularity) {
|
|
2201
2271
|
hints.push(`group results by ${modifications.timeGranularity}`);
|
|
2202
2272
|
}
|
|
2203
2273
|
if (modifications.dateRange) {
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
hints.push(`change end date to ${to}`);
|
|
2213
|
-
}
|
|
2214
|
-
} else {
|
|
2215
|
-
const parts = [];
|
|
2216
|
-
if (modifications.dateRange.from) {
|
|
2217
|
-
parts.push(`from ${modifications.dateRange.from}`);
|
|
2218
|
-
}
|
|
2219
|
-
if (modifications.dateRange.to) {
|
|
2220
|
-
parts.push(`to ${modifications.dateRange.to}`);
|
|
2221
|
-
}
|
|
2222
|
-
if (parts.length > 0) {
|
|
2223
|
-
hints.push(`filter date range ${parts.join(" ")}`);
|
|
2224
|
-
}
|
|
2274
|
+
const from = normalizeDateInput(modifications.dateRange.from);
|
|
2275
|
+
const to = normalizeDateInput(modifications.dateRange.to);
|
|
2276
|
+
if (from && to) {
|
|
2277
|
+
hints.push(`change date range to ${from} through ${to}`);
|
|
2278
|
+
} else if (from) {
|
|
2279
|
+
hints.push(`change start date to ${from}`);
|
|
2280
|
+
} else if (to) {
|
|
2281
|
+
hints.push(`change end date to ${to}`);
|
|
2225
2282
|
}
|
|
2226
2283
|
}
|
|
2227
2284
|
if (modifications.additionalInstructions) {
|
|
@@ -2230,7 +2287,7 @@ function buildModifiedQuestion(originalQuestion, modifications, pipeline, hasSes
|
|
|
2230
2287
|
if (hints.length === 0) {
|
|
2231
2288
|
return originalQuestion;
|
|
2232
2289
|
}
|
|
2233
|
-
if (
|
|
2290
|
+
if (hasSessionContext) {
|
|
2234
2291
|
return hints.join(", ");
|
|
2235
2292
|
}
|
|
2236
2293
|
return `${originalQuestion} (${hints.join(", ")})`;
|
|
@@ -2346,13 +2403,30 @@ async function modifyChart(client, queryEngine, input, options, signal) {
|
|
|
2346
2403
|
const hasSqlMods = !!input.sqlModifications;
|
|
2347
2404
|
const hasVizMods = !!input.vizModifications;
|
|
2348
2405
|
const hasCustomSql = !!input.sqlModifications?.customSql;
|
|
2349
|
-
|
|
2350
|
-
const queryEndpoint =
|
|
2351
|
-
let
|
|
2406
|
+
resolveQueryPipeline(options?.pipeline);
|
|
2407
|
+
const queryEndpoint = "/v2/query";
|
|
2408
|
+
let currentSql = input.sql;
|
|
2352
2409
|
let finalParams = input.params ?? {};
|
|
2353
2410
|
let paramMetadata = [];
|
|
2354
2411
|
let rationale;
|
|
2355
2412
|
let queryId;
|
|
2413
|
+
if (input.queryId) {
|
|
2414
|
+
const resolved = await resolveQuery(
|
|
2415
|
+
client,
|
|
2416
|
+
queryEngine,
|
|
2417
|
+
input.queryId,
|
|
2418
|
+
{ tenantId, userId: options?.userId, scopes: options?.scopes },
|
|
2419
|
+
signal
|
|
2420
|
+
);
|
|
2421
|
+
currentSql = resolved.sql;
|
|
2422
|
+
finalParams = resolved.params;
|
|
2423
|
+
paramMetadata = resolved.paramMetadata;
|
|
2424
|
+
queryId = resolved.queryId;
|
|
2425
|
+
}
|
|
2426
|
+
if (!currentSql) {
|
|
2427
|
+
throw new Error("modifyChart requires either queryId or sql.");
|
|
2428
|
+
}
|
|
2429
|
+
let finalSql = currentSql;
|
|
2356
2430
|
let sqlChanged = false;
|
|
2357
2431
|
let finalQuestion = input.question;
|
|
2358
2432
|
const databaseName = input.database ?? queryEngine.getDefaultDatabase();
|
|
@@ -2380,23 +2454,21 @@ async function modifyChart(client, queryEngine, input, options, signal) {
|
|
|
2380
2454
|
finalSql = input.sqlModifications.customSql;
|
|
2381
2455
|
finalParams = {};
|
|
2382
2456
|
paramMetadata = [];
|
|
2457
|
+
queryId = void 0;
|
|
2383
2458
|
sqlChanged = true;
|
|
2384
2459
|
} else if (hasSqlMods && !hasCustomSql) {
|
|
2385
2460
|
const modifiedQuestion = buildModifiedQuestion(
|
|
2386
2461
|
input.question,
|
|
2387
2462
|
input.sqlModifications,
|
|
2388
|
-
pipeline,
|
|
2389
2463
|
hasQuerypanelSessionContext
|
|
2390
2464
|
);
|
|
2391
|
-
|
|
2392
|
-
finalQuestion = modifiedQuestion;
|
|
2393
|
-
}
|
|
2465
|
+
finalQuestion = modifiedQuestion;
|
|
2394
2466
|
const queryResponse = await client.post(
|
|
2395
2467
|
queryEndpoint,
|
|
2396
2468
|
{
|
|
2397
2469
|
question: modifiedQuestion,
|
|
2398
2470
|
session_id: querypanelSessionId,
|
|
2399
|
-
previous_sql:
|
|
2471
|
+
previous_sql: currentSql,
|
|
2400
2472
|
...options?.maxRetry ? { max_retry: options.maxRetry } : {},
|
|
2401
2473
|
...tenantSettings ? { tenant_settings: tenantSettings } : {},
|
|
2402
2474
|
...databaseName ? { database: databaseName } : {},
|
|
@@ -2412,16 +2484,14 @@ async function modifyChart(client, queryEngine, input, options, signal) {
|
|
|
2412
2484
|
finalSql = queryResponse.sql;
|
|
2413
2485
|
paramMetadata = Array.isArray(queryResponse.params) ? queryResponse.params : [];
|
|
2414
2486
|
finalParams = queryEngine.mapGeneratedParams(paramMetadata);
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
);
|
|
2421
|
-
}
|
|
2487
|
+
applyDateRangeOverrides(
|
|
2488
|
+
input.sqlModifications?.dateRange,
|
|
2489
|
+
finalParams,
|
|
2490
|
+
paramMetadata
|
|
2491
|
+
);
|
|
2422
2492
|
rationale = queryResponse.rationale;
|
|
2423
2493
|
queryId = queryResponse.queryId;
|
|
2424
|
-
sqlChanged = finalSql !==
|
|
2494
|
+
sqlChanged = finalSql !== currentSql;
|
|
2425
2495
|
}
|
|
2426
2496
|
const execution = await queryEngine.validateAndExecute(
|
|
2427
2497
|
finalSql,
|
|
@@ -2636,6 +2706,9 @@ function resolveAttachEnforceTenantIsolation(tenantFieldName, enforceTenantIsola
|
|
|
2636
2706
|
return tenantFieldName ? true : void 0;
|
|
2637
2707
|
}
|
|
2638
2708
|
var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
|
|
2709
|
+
client;
|
|
2710
|
+
queryEngine;
|
|
2711
|
+
supportedChartTypes;
|
|
2639
2712
|
/**
|
|
2640
2713
|
* @param workspaceId - Workspace UUID (same value as your org in the dashboard). Passed to the API as JWT claim `organizationId`.
|
|
2641
2714
|
*/
|
|
@@ -2866,13 +2939,32 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
|
|
|
2866
2939
|
throw new Error("tenantId is required");
|
|
2867
2940
|
}
|
|
2868
2941
|
const databaseName = input.database ?? "db";
|
|
2869
|
-
|
|
2942
|
+
let sql = input.sql;
|
|
2943
|
+
let params = input.params ?? {};
|
|
2944
|
+
if (input.queryId) {
|
|
2945
|
+
const resolved = await resolveQuery(
|
|
2946
|
+
this.client,
|
|
2947
|
+
this.queryEngine,
|
|
2948
|
+
input.queryId,
|
|
2949
|
+
{ tenantId }
|
|
2950
|
+
);
|
|
2951
|
+
sql = resolved.sql;
|
|
2952
|
+
params = resolved.params;
|
|
2953
|
+
}
|
|
2954
|
+
if (!sql) {
|
|
2955
|
+
throw new Error("Either queryId or sql is required");
|
|
2956
|
+
}
|
|
2870
2957
|
const result = await this.queryEngine.validateAndExecute(
|
|
2871
|
-
|
|
2958
|
+
sql,
|
|
2872
2959
|
params,
|
|
2873
2960
|
databaseName,
|
|
2874
2961
|
tenantId
|
|
2875
2962
|
);
|
|
2963
|
+
if (input.queryId) {
|
|
2964
|
+
await markQueryExecuted(this.client, input.queryId, {
|
|
2965
|
+
tenantId
|
|
2966
|
+
});
|
|
2967
|
+
}
|
|
2876
2968
|
return { rows: result.rows, fields: result.fields };
|
|
2877
2969
|
}
|
|
2878
2970
|
// VizSpec generation
|