@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 +4 -2
- package/dist/index.cjs +145 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -14
- package/dist/index.d.ts +27 -14
- package/dist/index.js +141 -46
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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 (
|
|
519
|
-
paramArray = this.convertNamedToPositionalParams(
|
|
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
|
-
*
|
|
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
|
|
557
|
-
if (
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
|
|
612
|
+
positionalParams.push(val);
|
|
571
613
|
}
|
|
572
|
-
|
|
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 (
|
|
578
|
-
paramArray = this.convertNamedToPositionalParams(
|
|
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
|
-
|
|
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
|
|
1323
|
-
|
|
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
|
|
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
|
-
...
|
|
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
|
|
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
|
-
|
|
2370
|
+
pipeline,
|
|
2276
2371
|
hasQuerypanelSessionContext
|
|
2277
2372
|
);
|
|
2278
|
-
if (
|
|
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 (
|
|
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
|