@querypanel/node-sdk 1.3.0 → 1.3.3

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
@@ -544,7 +544,7 @@ Every request is signed with `RS256` using the private key you pass to the const
544
544
 
545
545
  - HTTP errors propagate as thrown `Error` instances that include `status` (and `details` when available).
546
546
  - `syncSchema` automatically skips embedding if the schema hasn't changed (drift detection). Use `syncSchema(..., { forceReindex: true })` to force updates.
547
- - `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.
548
548
 
549
549
  ### Automatic SQL repair and retry
550
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(
@@ -2212,35 +2250,35 @@ async function resolveQuery(client, queryEngine, queryId, options, signal) {
2212
2250
  dialect: response.dialect
2213
2251
  };
2214
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
+ }
2215
2266
 
2216
2267
  // src/routes/modify.ts
2217
- function buildModifiedQuestion(originalQuestion, modifications, pipeline, hasSessionContext = false) {
2268
+ function buildModifiedQuestion(originalQuestion, modifications, hasSessionContext = false) {
2218
2269
  const hints = [];
2219
2270
  if (modifications.timeGranularity) {
2220
2271
  hints.push(`group results by ${modifications.timeGranularity}`);
2221
2272
  }
2222
2273
  if (modifications.dateRange) {
2223
- if (pipeline === "v2") {
2224
- const from = normalizeDateInput(modifications.dateRange.from);
2225
- const to = normalizeDateInput(modifications.dateRange.to);
2226
- if (from && to) {
2227
- hints.push(`change date range to ${from} through ${to}`);
2228
- } else if (from) {
2229
- hints.push(`change start date to ${from}`);
2230
- } else if (to) {
2231
- hints.push(`change end date to ${to}`);
2232
- }
2233
- } else {
2234
- const parts = [];
2235
- if (modifications.dateRange.from) {
2236
- parts.push(`from ${modifications.dateRange.from}`);
2237
- }
2238
- if (modifications.dateRange.to) {
2239
- parts.push(`to ${modifications.dateRange.to}`);
2240
- }
2241
- if (parts.length > 0) {
2242
- hints.push(`filter date range ${parts.join(" ")}`);
2243
- }
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}`);
2244
2282
  }
2245
2283
  }
2246
2284
  if (modifications.additionalInstructions) {
@@ -2249,11 +2287,12 @@ function buildModifiedQuestion(originalQuestion, modifications, pipeline, hasSes
2249
2287
  if (hints.length === 0) {
2250
2288
  return originalQuestion;
2251
2289
  }
2252
- if (pipeline === "v2" && hasSessionContext) {
2290
+ if (hasSessionContext) {
2253
2291
  return hints.join(", ");
2254
2292
  }
2255
2293
  return `${originalQuestion} (${hints.join(", ")})`;
2256
2294
  }
2295
+ var REWRITE_DATEFILTER_ENDPOINT = "/v2/rewrite-datefilter";
2257
2296
  var START_PARAM_KEY_REGEX = /(^|_)(start|from)(_|$)/i;
2258
2297
  var END_PARAM_KEY_REGEX = /(^|_)(end|to)(_|$)/i;
2259
2298
  var ISO_DATETIME_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z?$/;
@@ -2290,31 +2329,118 @@ function normalizeGeneratedParamKey(param, index) {
2290
2329
  const nameCandidate = typeof param.name === "string" && param.name.trim() || typeof param.placeholder === "string" && param.placeholder.trim() || typeof param.position === "number" && String(param.position) || String(index + 1);
2291
2330
  return nameCandidate.replace(/[{}]/g, "").replace(/(.+):.*$/, "$1").replace(/^[:$]/, "").trim();
2292
2331
  }
2293
- function applyDateRangeOverrides(dateRange, params, paramMetadata) {
2332
+ function collectParamKeys(params, paramMetadata) {
2333
+ const keys = new Set(Object.keys(params));
2334
+ for (let i = 0; i < paramMetadata.length; i++) {
2335
+ const param = paramMetadata[i];
2336
+ if (!param) continue;
2337
+ keys.add(normalizeGeneratedParamKey(param, i));
2338
+ }
2339
+ return [...keys];
2340
+ }
2341
+ function placeholderKey(token) {
2342
+ if (token.startsWith("$") || token.startsWith("@")) return token.slice(1);
2343
+ return token.slice(1, -1).replace(/:.+$/, "").trim();
2344
+ }
2345
+ function isDateLike(value) {
2346
+ return typeof value === "string" && /^\d{4}-\d{2}-\d{2}/.test(value.trim());
2347
+ }
2348
+ function dateBoundKeysFromSql(sql) {
2349
+ const start = /* @__PURE__ */ new Set();
2350
+ const end = /* @__PURE__ */ new Set();
2351
+ const comparison = /(>=|<=|>|<)\s*(\$\d+|\{[^}]+\}|@[A-Za-z_][\w]*)/g;
2352
+ const between = /BETWEEN\s+(\$\d+|\{[^}]+\}|@[A-Za-z_][\w]*)\s+AND\s+(\$\d+|\{[^}]+\}|@[A-Za-z_][\w]*)/gi;
2353
+ for (const match of sql.matchAll(comparison)) {
2354
+ const key = placeholderKey(match[2] ?? "");
2355
+ if (!key) continue;
2356
+ if (match[1] === ">=" || match[1] === ">") start.add(key);
2357
+ else end.add(key);
2358
+ }
2359
+ for (const match of sql.matchAll(between)) {
2360
+ const startKey = placeholderKey(match[1] ?? "");
2361
+ const endKey = placeholderKey(match[2] ?? "");
2362
+ if (startKey) start.add(startKey);
2363
+ if (endKey) end.add(endKey);
2364
+ }
2365
+ return { start, end };
2366
+ }
2367
+ function paramValueForKey(key, params, paramMetadata) {
2368
+ if (key in params) return params[key];
2369
+ for (let i = 0; i < paramMetadata.length; i++) {
2370
+ const param = paramMetadata[i];
2371
+ if (!param) continue;
2372
+ if (normalizeGeneratedParamKey(param, i) === key) return param.value;
2373
+ }
2374
+ return void 0;
2375
+ }
2376
+ function dateBoundKeys(sql, params, paramMetadata, boundary) {
2377
+ const fromSql = dateBoundKeysFromSql(sql)[boundary];
2378
+ const keys = [];
2379
+ for (const key of collectParamKeys(params, paramMetadata)) {
2380
+ const named = keyLooksLikeDateBoundary(key, boundary);
2381
+ if (!named && !fromSql.has(key)) continue;
2382
+ if (!isDateLike(paramValueForKey(key, params, paramMetadata))) continue;
2383
+ keys.push(key);
2384
+ }
2385
+ return keys;
2386
+ }
2387
+ function canSubstituteDateRangeParams(dateRange, sql, params, paramMetadata = []) {
2388
+ if (!dateRange) return false;
2389
+ const from = normalizeDateInput(dateRange.from);
2390
+ const to = normalizeDateInput(dateRange.to);
2391
+ if (!from && !to) return false;
2392
+ if (from && dateBoundKeys(sql, params, paramMetadata, "start").length === 0) {
2393
+ return false;
2394
+ }
2395
+ if (to && dateBoundKeys(sql, params, paramMetadata, "end").length === 0) {
2396
+ return false;
2397
+ }
2398
+ return true;
2399
+ }
2400
+ function isDateRangeOnlySqlModification(modifications) {
2401
+ if (!modifications?.dateRange) return false;
2402
+ if (modifications.customSql) return false;
2403
+ if (modifications.timeGranularity) return false;
2404
+ if (typeof modifications.additionalInstructions === "string" && modifications.additionalInstructions.trim().length > 0) {
2405
+ return false;
2406
+ }
2407
+ return true;
2408
+ }
2409
+ function applyDateRangeOverrides(dateRange, sql, params, paramMetadata) {
2294
2410
  if (!dateRange) return;
2295
2411
  const from = normalizeDateInput(dateRange.from);
2296
2412
  const to = normalizeDateInput(dateRange.to);
2297
2413
  if (!from && !to) return;
2414
+ const startKeys = new Set(
2415
+ from ? dateBoundKeys(sql, params, paramMetadata, "start") : []
2416
+ );
2417
+ const endKeys = new Set(
2418
+ to ? dateBoundKeys(sql, params, paramMetadata, "end") : []
2419
+ );
2298
2420
  for (const [key, value] of Object.entries(params)) {
2299
- if (from && keyLooksLikeDateBoundary(key, "start")) {
2300
- params[key] = formatDateOverride(from, "start", value);
2421
+ if (startKeys.has(key)) {
2422
+ params[key] = formatDateOverride(from ?? "", "start", value);
2301
2423
  }
2302
- if (to && keyLooksLikeDateBoundary(key, "end")) {
2303
- params[key] = formatDateOverride(to, "end", value);
2424
+ if (endKeys.has(key)) {
2425
+ params[key] = formatDateOverride(to ?? "", "end", value);
2304
2426
  }
2305
2427
  }
2306
2428
  for (let i = 0; i < paramMetadata.length; i++) {
2307
2429
  const param = paramMetadata[i];
2308
2430
  if (!param) continue;
2309
2431
  const key = normalizeGeneratedParamKey(param, i);
2310
- if (from && keyLooksLikeDateBoundary(key, "start")) {
2311
- param.value = formatDateOverride(from, "start", param.value);
2432
+ if (startKeys.has(key)) {
2433
+ param.value = formatDateOverride(from ?? "", "start", param.value);
2312
2434
  }
2313
- if (to && keyLooksLikeDateBoundary(key, "end")) {
2314
- param.value = formatDateOverride(to, "end", param.value);
2435
+ if (endKeys.has(key)) {
2436
+ param.value = formatDateOverride(to ?? "", "end", param.value);
2315
2437
  }
2316
2438
  }
2317
2439
  }
2440
+ function buildPreviousParams(params, paramMetadata) {
2441
+ if (paramMetadata.length > 0) return paramMetadata;
2442
+ return Object.entries(params).map(([name, value]) => ({ name, value }));
2443
+ }
2318
2444
  function buildVizHints(modifications) {
2319
2445
  const hints = {};
2320
2446
  if (modifications.kind) {
@@ -2365,8 +2491,11 @@ async function modifyChart(client, queryEngine, input, options, signal) {
2365
2491
  const hasSqlMods = !!input.sqlModifications;
2366
2492
  const hasVizMods = !!input.vizModifications;
2367
2493
  const hasCustomSql = !!input.sqlModifications?.customSql;
2368
- const pipeline = resolveQueryPipeline(options?.pipeline);
2369
- const queryEndpoint = pipeline === "v2" ? "/v2/query" : "/query";
2494
+ const lightweight = options?.lightweight === true;
2495
+ const dateOnly = lightweight && isDateRangeOnlySqlModification(input.sqlModifications);
2496
+ const skipChartLlm = lightweight && !hasCustomSql && (dateOnly || hasVizMods && !hasSqlMods);
2497
+ resolveQueryPipeline(options?.pipeline);
2498
+ const queryEndpoint = "/v2/query";
2370
2499
  let currentSql = input.sql;
2371
2500
  let finalParams = input.params ?? {};
2372
2501
  let paramMetadata = [];
@@ -2418,28 +2547,57 @@ async function modifyChart(client, queryEngine, input, options, signal) {
2418
2547
  paramMetadata = [];
2419
2548
  queryId = void 0;
2420
2549
  sqlChanged = true;
2550
+ } else if (dateOnly && canSubstituteDateRangeParams(
2551
+ input.sqlModifications?.dateRange,
2552
+ finalSql,
2553
+ finalParams,
2554
+ paramMetadata
2555
+ )) {
2556
+ finalParams = { ...finalParams };
2557
+ applyDateRangeOverrides(
2558
+ input.sqlModifications?.dateRange,
2559
+ finalSql,
2560
+ finalParams,
2561
+ paramMetadata
2562
+ );
2563
+ sqlChanged = false;
2421
2564
  } else if (hasSqlMods && !hasCustomSql) {
2422
2565
  const modifiedQuestion = buildModifiedQuestion(
2423
2566
  input.question,
2424
2567
  input.sqlModifications,
2425
- pipeline,
2426
2568
  hasQuerypanelSessionContext
2427
2569
  );
2428
- if (pipeline === "v2") {
2429
- finalQuestion = modifiedQuestion;
2430
- }
2431
- const queryResponse = await client.post(
2570
+ finalQuestion = modifiedQuestion;
2571
+ const sharedBody = {
2572
+ session_id: querypanelSessionId,
2573
+ previous_sql: currentSql,
2574
+ ...tenantSettings ? { tenant_settings: tenantSettings } : {},
2575
+ ...databaseName ? { database: databaseName } : {},
2576
+ ...metadata?.dialect ? { dialect: metadata.dialect } : {},
2577
+ debug: options?.debug ?? false
2578
+ };
2579
+ const [endpoint, body] = dateOnly ? [
2580
+ REWRITE_DATEFILTER_ENDPOINT,
2581
+ {
2582
+ ...sharedBody,
2583
+ question: input.question,
2584
+ previous_params: buildPreviousParams(finalParams, paramMetadata),
2585
+ date_range: {
2586
+ from: normalizeDateInput(input.sqlModifications?.dateRange?.from),
2587
+ to: normalizeDateInput(input.sqlModifications?.dateRange?.to)
2588
+ }
2589
+ }
2590
+ ] : [
2432
2591
  queryEndpoint,
2433
2592
  {
2593
+ ...sharedBody,
2434
2594
  question: modifiedQuestion,
2435
- session_id: querypanelSessionId,
2436
- previous_sql: currentSql,
2437
- ...options?.maxRetry ? { max_retry: options.maxRetry } : {},
2438
- ...tenantSettings ? { tenant_settings: tenantSettings } : {},
2439
- ...databaseName ? { database: databaseName } : {},
2440
- ...metadata?.dialect ? { dialect: metadata.dialect } : {},
2441
- debug: options?.debug ?? false
2442
- },
2595
+ ...options?.maxRetry ? { max_retry: options.maxRetry } : {}
2596
+ }
2597
+ ];
2598
+ const queryResponse = await client.post(
2599
+ endpoint,
2600
+ body,
2443
2601
  tenantId,
2444
2602
  options?.userId,
2445
2603
  options?.scopes,
@@ -2449,13 +2607,12 @@ async function modifyChart(client, queryEngine, input, options, signal) {
2449
2607
  finalSql = queryResponse.sql;
2450
2608
  paramMetadata = Array.isArray(queryResponse.params) ? queryResponse.params : [];
2451
2609
  finalParams = queryEngine.mapGeneratedParams(paramMetadata);
2452
- if (pipeline === "v2") {
2453
- applyDateRangeOverrides(
2454
- input.sqlModifications?.dateRange,
2455
- finalParams,
2456
- paramMetadata
2457
- );
2458
- }
2610
+ applyDateRangeOverrides(
2611
+ input.sqlModifications?.dateRange,
2612
+ finalSql,
2613
+ finalParams,
2614
+ paramMetadata
2615
+ );
2459
2616
  rationale = queryResponse.rationale;
2460
2617
  queryId = queryResponse.queryId;
2461
2618
  sqlChanged = finalSql !== currentSql;
@@ -2473,7 +2630,7 @@ async function modifyChart(client, queryEngine, input, options, signal) {
2473
2630
  };
2474
2631
  const vizHints = hasVizMods ? buildVizHints(input.vizModifications) : {};
2475
2632
  const vizHintsForChart = chartType === "vizspec" ? vizHints : stripVizSpecOnlyHints(vizHints);
2476
- if (chartType === "vizspec") {
2633
+ if (!skipChartLlm && chartType === "vizspec") {
2477
2634
  const vizspecResponse = await client.post(
2478
2635
  "/vizspec",
2479
2636
  {
@@ -2500,7 +2657,7 @@ async function modifyChart(client, queryEngine, input, options, signal) {
2500
2657
  specType: "vizspec",
2501
2658
  notes: rows.length === 0 ? vizspecResponse.notes ?? "Query returned no rows." : vizspecResponse.notes
2502
2659
  };
2503
- } else {
2660
+ } else if (!skipChartLlm) {
2504
2661
  const chartResponse = await client.post(
2505
2662
  "/chart",
2506
2663
  {
@@ -2673,6 +2830,9 @@ function resolveAttachEnforceTenantIsolation(tenantFieldName, enforceTenantIsola
2673
2830
  return tenantFieldName ? true : void 0;
2674
2831
  }
2675
2832
  var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
2833
+ client;
2834
+ queryEngine;
2835
+ supportedChartTypes;
2676
2836
  /**
2677
2837
  * @param workspaceId - Workspace UUID (same value as your org in the dashboard). Passed to the API as JWT claim `organizationId`.
2678
2838
  */
@@ -2924,6 +3084,11 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
2924
3084
  databaseName,
2925
3085
  tenantId
2926
3086
  );
3087
+ if (input.queryId) {
3088
+ await markQueryExecuted(this.client, input.queryId, {
3089
+ tenantId
3090
+ });
3091
+ }
2927
3092
  return { rows: result.rows, fields: result.fields };
2928
3093
  }
2929
3094
  // VizSpec generation