@querypanel/node-sdk 1.2.10 → 1.3.0-beta.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
@@ -73,6 +73,7 @@ const response = await qp.ask("Top countries by revenue", {
73
73
  tenantId: "tenant_123",
74
74
  database: "analytics",
75
75
  });
76
+ // Defaults to pipeline "v2" (POST /v2/query). Pass pipeline: "v1" only for legacy.
76
77
 
77
78
  console.log(response.sql);
78
79
  console.log(response.params);
@@ -82,17 +83,18 @@ console.log(response.chart.vegaLiteSpec);
82
83
 
83
84
  ### Custom system instructions (v2 pipeline)
84
85
 
85
- If you need to enforce tenant-specific policies that should apply to every query (for example, data retention windows), you can inject additional **system prompt** instructions into the v2 pipeline:
86
+ If you need to enforce tenant-specific policies that should apply to every query (for example, data retention windows), you can inject additional **system prompt** instructions into the v2 pipeline (the default):
86
87
 
87
88
  ```ts
88
89
  const response = await qp.ask("Revenue by product", {
89
90
  tenantId: "tenant_123",
90
91
  database: "analytics",
91
- pipeline: "v2",
92
92
  systemPrompt: "Retention policy: only query data from the last 30 days.",
93
93
  });
94
94
  ```
95
95
 
96
+ > **Deprecated:** `pipeline: "v1"` still hits `POST /query` for compatibility. Prefer omitting `pipeline` or passing `"v2"`.
97
+
96
98
  ### Restricting VizSpec chart types
97
99
 
98
100
  If your UI only supports a subset of VizSpec chart kinds (for example no area or scatter), set **`supportedChartTypes`** on the SDK constructor or on each `ask` / `modifyChart` call. The API’s `/vizspec` step then steers generation toward allowed types only (`line`, `bar`, `column`, `area`, `scatter`, `pie`). This applies whenever you use **`chartType: "vizspec"`** (including the default v2 flow after SQL runs).
package/dist/index.cjs CHANGED
@@ -38,7 +38,10 @@ __export(index_exports, {
38
38
  QueryErrorCode: () => QueryErrorCode,
39
39
  QueryPanelSdkAPI: () => QueryPanelSdkAPI,
40
40
  QueryPipelineError: () => QueryPipelineError,
41
- anonymizeResults: () => anonymizeResults
41
+ anonymizeResults: () => anonymizeResults,
42
+ mapGeneratedParams: () => mapGeneratedParams,
43
+ normalizeSqlParamsInput: () => normalizeSqlParamsInput,
44
+ resolveQueryPipeline: () => resolveQueryPipeline
42
45
  });
43
46
  module.exports = __toCommonJS(index_exports);
44
47
 
@@ -497,6 +500,37 @@ function toNumber(value) {
497
500
  }
498
501
 
499
502
  // src/adapters/postgres.ts
503
+ function maxPostgresPlaceholderIndex(sql) {
504
+ let max = 0;
505
+ for (const match of sql.matchAll(/\$(\d+)\b/g)) {
506
+ const index = Number.parseInt(match[1], 10);
507
+ if (Number.isFinite(index) && index > max) {
508
+ max = index;
509
+ }
510
+ }
511
+ return max;
512
+ }
513
+ function filterParamsForPostgresSql(sql, params) {
514
+ if (!params || Object.keys(params).length === 0) {
515
+ return void 0;
516
+ }
517
+ const placeholderCount = maxPostgresPlaceholderIndex(sql);
518
+ if (placeholderCount === 0) {
519
+ return void 0;
520
+ }
521
+ const hasNumericKeys = Object.keys(params).some((key) => /^\d+$/.test(key));
522
+ if (hasNumericKeys) {
523
+ const filtered = {};
524
+ for (let index = 1; index <= placeholderCount; index += 1) {
525
+ const key = String(index);
526
+ if (key in params) {
527
+ filtered[key] = params[key];
528
+ }
529
+ }
530
+ return Object.keys(filtered).length > 0 ? filtered : void 0;
531
+ }
532
+ return params;
533
+ }
500
534
  var PostgresAdapter = class {
501
535
  constructor(clientFn, options = {}) {
502
536
  this.clientFn = clientFn;
@@ -514,9 +548,10 @@ var PostgresAdapter = class {
514
548
  if (this.allowedTables) {
515
549
  this.validateQueryTables(sql);
516
550
  }
551
+ const boundParams = filterParamsForPostgresSql(sql, params);
517
552
  let paramArray;
518
- if (params) {
519
- paramArray = this.convertNamedToPositionalParams(params);
553
+ if (boundParams) {
554
+ paramArray = this.convertNamedToPositionalParams(sql, boundParams);
520
555
  }
521
556
  const result = await this.clientFn(sql, paramArray);
522
557
  const fields = result.fields.map((f) => f.name);
@@ -546,36 +581,43 @@ var PostgresAdapter = class {
546
581
  }
547
582
  }
548
583
  /**
549
- * Convert params to positional array for PostgreSQL.
550
- * PostgreSQL expects $1, $2, $3 in SQL and an array of values [val1, val2, val3].
551
- * When the record has numeric keys ("1", "2", "3") — e.g. from mapGeneratedParams —
552
- * use only those in order so placeholder order matches SQL. Otherwise fall back to
553
- * named keys in alphabetical order for backward compatibility.
584
+ * Convert params to positional array for PostgreSQL ($1, $2, …).
585
+ * Values must align with v2 generation order via numeric keys from mapGeneratedParams.
554
586
  */
555
- convertNamedToPositionalParams(params) {
556
- const numericKeys = Object.keys(params).filter((k) => /^\d+$/.test(k)).map((k) => Number.parseInt(k, 10)).sort((a, b) => a - b);
557
- if (numericKeys.length > 0) {
558
- const positionalParams = [];
559
- for (const key of numericKeys) {
560
- let val = params[String(key)];
561
- if (typeof val === "string") {
562
- const match = val.match(/^<([a-zA-Z0-9_]+)>$/);
563
- const namedKey = match?.[1];
564
- if (namedKey && namedKey in params) {
565
- val = params[namedKey];
566
- }
587
+ convertNamedToPositionalParams(sql, params) {
588
+ const placeholderCount = maxPostgresPlaceholderIndex(sql);
589
+ if (placeholderCount === 0) {
590
+ return [];
591
+ }
592
+ const positionalParams = [];
593
+ for (let index = 1; index <= placeholderCount; index += 1) {
594
+ const key = String(index);
595
+ let val = params[key];
596
+ if (val === void 0 && placeholderCount === 1) {
597
+ const namedKeys = Object.keys(params).filter((k) => !/^\d+$/.test(k));
598
+ if (namedKeys.length === 1) {
599
+ val = params[namedKeys[0]];
600
+ }
601
+ }
602
+ if (val === void 0) {
603
+ throw new Error(`Missing value for PostgreSQL parameter $${index}`);
604
+ }
605
+ if (typeof val === "string") {
606
+ const match = val.match(/^<([a-zA-Z0-9_]+)>$/);
607
+ const namedKey = match?.[1];
608
+ if (namedKey && namedKey in params) {
609
+ val = params[namedKey];
567
610
  }
568
- positionalParams.push(val);
569
611
  }
570
- return positionalParams;
612
+ positionalParams.push(val);
571
613
  }
572
- const namedKeys = Object.keys(params).filter((k) => !/^\d+$/.test(k)).sort();
573
- return namedKeys.map((key) => params[key]);
614
+ return positionalParams;
574
615
  }
575
616
  async validate(sql, params) {
617
+ const boundParams = filterParamsForPostgresSql(sql, params);
576
618
  let paramArray;
577
- if (params) {
578
- paramArray = this.convertNamedToPositionalParams(params);
619
+ if (boundParams) {
620
+ paramArray = this.convertNamedToPositionalParams(sql, boundParams);
579
621
  }
580
622
  await this.clientFn(`EXPLAIN ${sql}`, paramArray);
581
623
  }
@@ -1218,6 +1260,45 @@ var CallbackApiClient = class {
1218
1260
  }
1219
1261
  };
1220
1262
 
1263
+ // src/core/map-generated-params.ts
1264
+ function mapGeneratedParams(params) {
1265
+ const record = {};
1266
+ params.forEach((param, index) => {
1267
+ const value = param.value;
1268
+ if (value === void 0) {
1269
+ return;
1270
+ }
1271
+ record[String(index + 1)] = value;
1272
+ 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);
1273
+ const key = nameCandidate.replace(/[{}:$]/g, "").trim();
1274
+ record[key] = value;
1275
+ });
1276
+ return record;
1277
+ }
1278
+ function normalizeSqlParamsInput(params) {
1279
+ if (params == null) {
1280
+ return {};
1281
+ }
1282
+ if (Array.isArray(params)) {
1283
+ if (params.length === 0) {
1284
+ return {};
1285
+ }
1286
+ const first = params[0];
1287
+ if (first && typeof first === "object" && "value" in first) {
1288
+ return mapGeneratedParams(params);
1289
+ }
1290
+ const positional = {};
1291
+ params.forEach((value, index) => {
1292
+ positional[String(index + 1)] = value;
1293
+ });
1294
+ return positional;
1295
+ }
1296
+ if (typeof params === "object") {
1297
+ return params;
1298
+ }
1299
+ return {};
1300
+ }
1301
+
1221
1302
  // src/core/query-engine.ts
1222
1303
  var QueryEngine = class {
1223
1304
  constructor() {
@@ -1288,18 +1369,7 @@ var QueryEngine = class {
1288
1369
  * - Named keys remain for tenant isolation / other lookups.
1289
1370
  */
1290
1371
  mapGeneratedParams(params) {
1291
- const record = {};
1292
- params.forEach((param, index) => {
1293
- const value = param.value;
1294
- if (value === void 0) {
1295
- return;
1296
- }
1297
- record[String(index + 1)] = value;
1298
- 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);
1299
- const key = nameCandidate.replace(/[{}:$]/g, "").trim();
1300
- record[key] = value;
1301
- });
1302
- return record;
1372
+ return mapGeneratedParams(params);
1303
1373
  }
1304
1374
  ensureTenantIsolation(sql, params, metadata, tenantId) {
1305
1375
  if (!metadata.tenantFieldName || metadata.enforceTenantIsolation === false) {
@@ -1319,8 +1389,10 @@ var QueryEngine = class {
1319
1389
  params[tenantField] = tenantId;
1320
1390
  tenantPredicate = `${tenantField} = @${tenantField}`;
1321
1391
  } else {
1322
- const escapedId = tenantId.replace(/'/g, "''");
1323
- tenantPredicate = `${tenantField} = '${escapedId}'`;
1392
+ const paramIndex = this.nextPostgresPlaceholderIndex(sql, params);
1393
+ params[String(paramIndex)] = tenantId;
1394
+ params[tenantField] = tenantId;
1395
+ tenantPredicate = `${tenantField} = $${paramIndex}`;
1324
1396
  }
1325
1397
  if (/\bwhere\b/i.test(sql)) {
1326
1398
  return sql.replace(
@@ -1330,6 +1402,23 @@ var QueryEngine = class {
1330
1402
  }
1331
1403
  return `${sql} WHERE ${tenantPredicate}`;
1332
1404
  }
1405
+ nextPostgresPlaceholderIndex(sql, params) {
1406
+ let max = 0;
1407
+ for (const match of sql.matchAll(/\$(\d+)\b/g)) {
1408
+ const index = Number.parseInt(match[1], 10);
1409
+ if (Number.isFinite(index) && index > max) {
1410
+ max = index;
1411
+ }
1412
+ }
1413
+ if (max > 0) {
1414
+ return max + 1;
1415
+ }
1416
+ const numericKeys = Object.keys(params).filter((key) => /^\d+$/.test(key)).map((key) => Number.parseInt(key, 10)).filter((index) => Number.isFinite(index));
1417
+ if (numericKeys.length > 0) {
1418
+ return Math.max(...numericKeys) + 1;
1419
+ }
1420
+ return 1;
1421
+ }
1333
1422
  };
1334
1423
 
1335
1424
  // src/routes/charts.ts
@@ -1864,6 +1953,7 @@ function resolveTenantId4(client, tenantId) {
1864
1953
  function buildSchemaRequest(databaseName, adapter, introspection, metadata) {
1865
1954
  const dialect = adapter.getDialect();
1866
1955
  const tables = introspection.tables.map((table) => ({
1956
+ schema_name: table.schema,
1867
1957
  table_name: table.name,
1868
1958
  description: table.comment ?? `Table ${table.name}`,
1869
1959
  columns: table.columns.map((column) => ({
@@ -1893,6 +1983,9 @@ var import_node_crypto4 = __toESM(require("crypto"), 1);
1893
1983
 
1894
1984
  // src/routes/query.ts
1895
1985
  var import_node_crypto3 = __toESM(require("crypto"), 1);
1986
+ function resolveQueryPipeline(pipeline) {
1987
+ return pipeline ?? "v2";
1988
+ }
1896
1989
  async function ask(client, queryEngine, question, options, signal) {
1897
1990
  const tenantId = resolveTenantId5(client, options.tenantId);
1898
1991
  const sessionId = import_node_crypto3.default.randomUUID();
@@ -1901,7 +1994,8 @@ async function ask(client, queryEngine, question, options, signal) {
1901
1994
  let attempt = 0;
1902
1995
  let lastError = options.lastError;
1903
1996
  let previousSql = options.previousSql;
1904
- const queryEndpoint = options.pipeline === "v2" ? "/v2/query" : "/query";
1997
+ const pipeline = resolveQueryPipeline(options.pipeline);
1998
+ const queryEndpoint = pipeline === "v2" ? "/v2/query" : "/query";
1905
1999
  while (attempt <= maxRetry) {
1906
2000
  const databaseName = options.database ?? queryEngine.getDefaultDatabase();
1907
2001
  const metadata = databaseName ? queryEngine.getDatabaseMetadata(databaseName) : void 0;
@@ -1925,7 +2019,7 @@ async function ask(client, queryEngine, question, options, signal) {
1925
2019
  ...databaseName ? { database: databaseName } : {},
1926
2020
  ...metadata?.dialect ? { dialect: metadata.dialect } : {},
1927
2021
  ...options.model?.trim() ? { model: options.model.trim() } : {},
1928
- ...options.pipeline === "v2" && options.systemPrompt?.trim() ? { system_prompt: options.systemPrompt.trim() } : {},
2022
+ ...pipeline === "v2" && options.systemPrompt?.trim() ? { system_prompt: options.systemPrompt.trim() } : {},
1929
2023
  debug: options.debug ?? false
1930
2024
  },
1931
2025
  tenantId,
@@ -2240,7 +2334,8 @@ async function modifyChart(client, queryEngine, input, options, signal) {
2240
2334
  const hasSqlMods = !!input.sqlModifications;
2241
2335
  const hasVizMods = !!input.vizModifications;
2242
2336
  const hasCustomSql = !!input.sqlModifications?.customSql;
2243
- const queryEndpoint = options?.pipeline === "v2" ? "/v2/query" : "/query";
2337
+ const pipeline = resolveQueryPipeline(options?.pipeline);
2338
+ const queryEndpoint = pipeline === "v2" ? "/v2/query" : "/query";
2244
2339
  let finalSql = input.sql;
2245
2340
  let finalParams = input.params ?? {};
2246
2341
  let paramMetadata = [];
@@ -2272,10 +2367,10 @@ async function modifyChart(client, queryEngine, input, options, signal) {
2272
2367
  const modifiedQuestion = buildModifiedQuestion(
2273
2368
  input.question,
2274
2369
  input.sqlModifications,
2275
- options?.pipeline,
2370
+ pipeline,
2276
2371
  hasQuerypanelSessionContext
2277
2372
  );
2278
- if (options?.pipeline === "v2") {
2373
+ if (pipeline === "v2") {
2279
2374
  finalQuestion = modifiedQuestion;
2280
2375
  }
2281
2376
  const queryResponse = await client.post(
@@ -2299,7 +2394,7 @@ async function modifyChart(client, queryEngine, input, options, signal) {
2299
2394
  finalSql = queryResponse.sql;
2300
2395
  paramMetadata = Array.isArray(queryResponse.params) ? queryResponse.params : [];
2301
2396
  finalParams = queryEngine.mapGeneratedParams(paramMetadata);
2302
- if (options?.pipeline === "v2") {
2397
+ if (pipeline === "v2") {
2303
2398
  applyDateRangeOverrides(
2304
2399
  input.sqlModifications?.dateRange,
2305
2400
  finalParams,
@@ -3540,6 +3635,9 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
3540
3635
  QueryErrorCode,
3541
3636
  QueryPanelSdkAPI,
3542
3637
  QueryPipelineError,
3543
- anonymizeResults
3638
+ anonymizeResults,
3639
+ mapGeneratedParams,
3640
+ normalizeSqlParamsInput,
3641
+ resolveQueryPipeline
3544
3642
  });
3545
3643
  //# sourceMappingURL=index.cjs.map