@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 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
- constructor() {
1305
- this.databases = /* @__PURE__ */ new Map();
1306
- this.databaseMetadata = /* @__PURE__ */ new Map();
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
- return pipeline ?? "v2";
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
- const pipeline = resolveQueryPipeline(options.pipeline);
2004
- const queryEndpoint = pipeline === "v2" ? "/v2/query" : "/query";
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
- ...pipeline === "v2" && options.systemPrompt?.trim() ? { system_prompt: options.systemPrompt.trim() } : {},
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, pipeline, hasSessionContext = false) {
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
- if (pipeline === "v2") {
2205
- const from = normalizeDateInput(modifications.dateRange.from);
2206
- const to = normalizeDateInput(modifications.dateRange.to);
2207
- if (from && to) {
2208
- hints.push(`change date range to ${from} through ${to}`);
2209
- } else if (from) {
2210
- hints.push(`change start date to ${from}`);
2211
- } else if (to) {
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 (pipeline === "v2" && hasSessionContext) {
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
- const pipeline = resolveQueryPipeline(options?.pipeline);
2350
- const queryEndpoint = pipeline === "v2" ? "/v2/query" : "/query";
2351
- let finalSql = input.sql;
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
- if (pipeline === "v2") {
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: input.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
- if (pipeline === "v2") {
2416
- applyDateRangeOverrides(
2417
- input.sqlModifications?.dateRange,
2418
- finalParams,
2419
- paramMetadata
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 !== input.sql;
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
- const params = input.params ?? {};
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
- input.sql,
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