@querypanel/node-sdk 1.3.1 → 1.3.5
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 +47 -19
- package/dist/index.cjs +194 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -4
- package/dist/index.d.ts +30 -4
- package/dist/index.js +194 -26
- package/dist/index.js.map +1 -1
- package/package.json +16 -3
package/README.md
CHANGED
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
# QueryPanel Node SDK
|
|
2
2
|
|
|
3
|
+
`@querypanel/node-sdk` is the headless, server-side SDK for [QueryPanel](https://querypanel.io) embedded analytics. Use it in your Node.js (18+), Bun, or Deno backend to mint RS256 tenant JWTs, sync your Postgres, Amazon Redshift, ClickHouse, or BigQuery schema, and turn natural-language questions into SQL (natural-language-to-SQL / text-to-SQL) for multi-tenant SaaS products. The generated SQL runs against your database through a client function you provide, so database credentials and query results stay on your servers. For chart generation the SDK sends column names and value types, not row values (see `anonymizeResults`).
|
|
4
|
+
|
|
5
|
+
- Docs: [querypanel.io/docs/node-sdk](https://querypanel.io/docs/node-sdk)
|
|
6
|
+
- Tenant isolation: [querypanel.io/docs/tenant-isolation](https://querypanel.io/docs/tenant-isolation)
|
|
7
|
+
- JWT setup: [querypanel.io/docs/jwt](https://querypanel.io/docs/jwt)
|
|
8
|
+
|
|
9
|
+
## When to use the Node SDK vs the React SDK
|
|
10
|
+
|
|
11
|
+
- **[React SDK](https://www.npmjs.com/package/@querypanel/react-sdk) (headful, start here for React apps):** `@querypanel/react-sdk` renders customer-facing dashboards with an AI assistant and per-tenant customization directly in your React tree, not an iframe ([why that matters](https://querypanel.io/blog/iframe-vs-native-react-embedded-analytics-2026)). It still needs this package on your server to mint the tenant JWT (`createJwt`).
|
|
12
|
+
- **Node SDK (this package, headless):** use it alone when you build your own UI and want `ask()` to return SQL, rows, and a chart spec from your API routes.
|
|
13
|
+
|
|
14
|
+
How it compares to other embedded analytics tools: [querypanel.io/compare](https://querypanel.io/compare).
|
|
15
|
+
|
|
16
|
+
## Overview
|
|
17
|
+
|
|
3
18
|
A TypeScript-first client for the QueryPanel Bun/Hono API. Its primary function is to **generate SQL from natural language**, but it also signs JWTs with your service private key, syncs database schemas, enforces tenant isolation, and wraps every public route under `src/routes/` (query, ingest, charts, active charts, and knowledge base).
|
|
4
19
|
|
|
5
20
|
## Installation
|
|
6
21
|
|
|
7
22
|
```bash
|
|
8
|
-
bun add @querypanel/sdk
|
|
23
|
+
bun add @querypanel/node-sdk
|
|
9
24
|
# or
|
|
10
|
-
npm install @querypanel/sdk
|
|
25
|
+
npm install @querypanel/node-sdk
|
|
11
26
|
```
|
|
12
27
|
|
|
13
28
|
> **Runtime:** Node.js 18+, Deno, or Bun. The SDK uses Web Crypto API for JWT signing and the native `fetch` API, making it compatible with modern JavaScript runtimes.
|
|
@@ -15,8 +30,9 @@ npm install @querypanel/sdk
|
|
|
15
30
|
## Quickstart
|
|
16
31
|
|
|
17
32
|
```ts
|
|
18
|
-
import { QueryPanelSdkAPI } from "@querypanel/sdk";
|
|
33
|
+
import { QueryPanelSdkAPI } from "@querypanel/node-sdk";
|
|
19
34
|
import { Pool } from "pg";
|
|
35
|
+
import { createClient } from "@clickhouse/client";
|
|
20
36
|
|
|
21
37
|
const qp = new QueryPanelSdkAPI(
|
|
22
38
|
process.env.QUERYPANEL_URL!,
|
|
@@ -42,21 +58,31 @@ const createPostgresClient = () => async (sql: string, params?: unknown[]) => {
|
|
|
42
58
|
}
|
|
43
59
|
};
|
|
44
60
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"pg_demo",
|
|
49
|
-
|
|
61
|
+
// Attach PostgreSQL database using the SDK's PostgresAdapter
|
|
62
|
+
// The SDK will automatically handle tenant isolation when tenantFieldName is provided
|
|
63
|
+
qp.attachPostgres(
|
|
64
|
+
"pg_demo", // a unique identifier for QueryPanel
|
|
65
|
+
createPostgresClient(),
|
|
50
66
|
{
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
67
|
+
database: "pg_demo", // database name
|
|
68
|
+
description: "PostgreSQL demo database", // some description that QueryPanel can use
|
|
69
|
+
tenantFieldName: "tenant_id", // SDK will automatically filter by tenant_id
|
|
70
|
+
enforceTenantIsolation: true, // Ensures all queries include tenant_id filter
|
|
71
|
+
allowedTables: ["orders"], // Only sync 'orders' table - 'users' will be excluded
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// Amazon Redshift speaks the Postgres wire protocol, so a pg-based client works.
|
|
76
|
+
// attachRedshift introspects via Redshift's svv_* views and generates Redshift SQL.
|
|
77
|
+
qp.attachRedshift("warehouse", createPostgresClient(), {
|
|
78
|
+
database: "dev",
|
|
79
|
+
tenantFieldName: "tenant_id",
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const clickhouse = createClient({ url: process.env.CLICKHOUSE_URL });
|
|
57
83
|
|
|
58
84
|
qp.attachClickhouse(
|
|
59
|
-
"analytics", //
|
|
85
|
+
"analytics", // unique identifier for QueryPanel
|
|
60
86
|
(params) => clickhouse.query(params),
|
|
61
87
|
{
|
|
62
88
|
database: "analytics", // database name
|
|
@@ -143,7 +169,7 @@ const response = await qp.ask("Show order volume by week", {
|
|
|
143
169
|
If your UI only supports a subset of chart kinds (for example no area or scatter), set **`supportedChartTypes`** on the SDK constructor or on each `ask` / `modifyChart` call (`line`, `bar`, `column`, `area`, `scatter`, `pie`).
|
|
144
170
|
|
|
145
171
|
```ts
|
|
146
|
-
import { QueryPanelSdkAPI, ALL_VIZ_CHART_TYPES, type ChartType } from "@querypanel/sdk";
|
|
172
|
+
import { QueryPanelSdkAPI, ALL_VIZ_CHART_TYPES, type ChartType } from "@querypanel/node-sdk";
|
|
147
173
|
|
|
148
174
|
const allowed: ChartType[] = ["line", "bar", "column", "pie"];
|
|
149
175
|
|
|
@@ -512,7 +538,7 @@ The API accepts up to **100** UUIDs per request. Examples: `?ids=a&ids=b` or `?i
|
|
|
512
538
|
The SDK is fully compatible with Deno (including Supabase Edge Functions) thanks to its use of Web Crypto API for JWT signing. No additional configuration needed:
|
|
513
539
|
|
|
514
540
|
```ts
|
|
515
|
-
import { QueryPanelSdkAPI } from "https://esm.sh/@querypanel/sdk";
|
|
541
|
+
import { QueryPanelSdkAPI } from "https://esm.sh/@querypanel/node-sdk";
|
|
516
542
|
|
|
517
543
|
const qp = new QueryPanelSdkAPI(
|
|
518
544
|
Deno.env.get("QUERYPANEL_URL")!,
|
|
@@ -526,10 +552,12 @@ const response = await qp.ask("Show top products", {
|
|
|
526
552
|
});
|
|
527
553
|
```
|
|
528
554
|
|
|
555
|
+
BigQuery is attached the same way with `attachBigQuery(name, clientFn, options)`. Any other engine can be plugged in with `attachDatabase(name, adapter)` and your own `DatabaseAdapter` implementation.
|
|
556
|
+
|
|
529
557
|
## Building locally
|
|
530
558
|
|
|
531
559
|
```bash
|
|
532
|
-
cd node-sdk
|
|
560
|
+
cd admin/packages/node-sdk
|
|
533
561
|
bun install
|
|
534
562
|
bun run build
|
|
535
563
|
```
|
|
@@ -571,4 +599,4 @@ Without `maxRetry`, execution errors throw immediately (default behavior).
|
|
|
571
599
|
|
|
572
600
|
## Need more?
|
|
573
601
|
|
|
574
|
-
Open an issue or extend `
|
|
602
|
+
Open an issue or extend the SDK: the public class is in `src/index.ts` and each API route has a module under `src/routes/`. Pull requests are welcome for additional adapters, richer param coercion, or convenience helpers around charts/annotations.
|
package/dist/index.cjs
CHANGED
|
@@ -548,7 +548,8 @@ var PostgresAdapter = class {
|
|
|
548
548
|
this.clientFn = clientFn;
|
|
549
549
|
this.databaseName = options.database ?? "postgres";
|
|
550
550
|
this.defaultSchema = options.defaultSchema ?? "public";
|
|
551
|
-
this.
|
|
551
|
+
this.dialect = options.dialect ?? "postgres";
|
|
552
|
+
this.kind = options.kind ?? this.dialect;
|
|
552
553
|
if (options.allowedTables) {
|
|
553
554
|
this.allowedTables = normalizeTableFilter3(
|
|
554
555
|
options.allowedTables,
|
|
@@ -560,6 +561,7 @@ var PostgresAdapter = class {
|
|
|
560
561
|
databaseName;
|
|
561
562
|
defaultSchema;
|
|
562
563
|
kind;
|
|
564
|
+
dialect;
|
|
563
565
|
allowedTables;
|
|
564
566
|
async execute(sql, params) {
|
|
565
567
|
if (this.allowedTables) {
|
|
@@ -639,7 +641,7 @@ var PostgresAdapter = class {
|
|
|
639
641
|
await this.clientFn(`EXPLAIN ${sql}`, paramArray);
|
|
640
642
|
}
|
|
641
643
|
getDialect() {
|
|
642
|
-
return
|
|
644
|
+
return this.dialect;
|
|
643
645
|
}
|
|
644
646
|
/**
|
|
645
647
|
* Simplified introspection: only collect table/column metadata for IngestRequest
|
|
@@ -648,12 +650,13 @@ var PostgresAdapter = class {
|
|
|
648
650
|
async introspect(options) {
|
|
649
651
|
const tablesToIntrospect = options?.tables ? normalizeTableFilter3(options.tables, this.defaultSchema) : this.allowedTables;
|
|
650
652
|
const normalizedTables = tablesToIntrospect ?? [];
|
|
653
|
+
const isRedshift = this.dialect === "redshift";
|
|
651
654
|
const tablesResult = await this.clientFn(
|
|
652
|
-
buildTablesQuery2(normalizedTables)
|
|
655
|
+
isRedshift ? buildRedshiftTablesQuery(normalizedTables) : buildTablesQuery2(normalizedTables)
|
|
653
656
|
);
|
|
654
657
|
const tableRows = tablesResult.rows;
|
|
655
658
|
const columnsResult = await this.clientFn(
|
|
656
|
-
buildColumnsQuery2(normalizedTables)
|
|
659
|
+
isRedshift ? buildRedshiftColumnsQuery(normalizedTables) : buildColumnsQuery2(normalizedTables)
|
|
657
660
|
);
|
|
658
661
|
const columnRows = columnsResult.rows;
|
|
659
662
|
const tablesByKey = /* @__PURE__ */ new Map();
|
|
@@ -767,10 +770,11 @@ function buildColumnsQuery2(tables) {
|
|
|
767
770
|
AND kcu.column_name = cols.column_name
|
|
768
771
|
) AS is_primary_key
|
|
769
772
|
FROM information_schema.columns cols
|
|
773
|
+
LEFT JOIN pg_catalog.pg_namespace n ON n.nspname = cols.table_schema
|
|
770
774
|
LEFT JOIN pg_catalog.pg_class c
|
|
771
775
|
ON c.relname = cols.table_name
|
|
776
|
+
AND c.relnamespace = n.oid
|
|
772
777
|
AND c.relkind IN ('r', 'v', 'm')
|
|
773
|
-
LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
774
778
|
LEFT JOIN pg_catalog.pg_attribute attr
|
|
775
779
|
ON attr.attrelid = c.oid
|
|
776
780
|
AND attr.attname = cols.column_name
|
|
@@ -780,6 +784,38 @@ function buildColumnsQuery2(tables) {
|
|
|
780
784
|
${filter}
|
|
781
785
|
ORDER BY cols.table_schema, cols.table_name, cols.ordinal_position;`;
|
|
782
786
|
}
|
|
787
|
+
var REDSHIFT_EXCLUDED_SCHEMAS = "('pg_catalog', 'information_schema', 'pg_internal', 'pg_automv')";
|
|
788
|
+
function buildRedshiftTablesQuery(tables) {
|
|
789
|
+
const filter = buildFilterClause(tables, "table_schema", "table_name");
|
|
790
|
+
return `SELECT
|
|
791
|
+
table_name,
|
|
792
|
+
table_schema AS schema_name,
|
|
793
|
+
table_type,
|
|
794
|
+
remarks AS comment
|
|
795
|
+
FROM svv_tables
|
|
796
|
+
WHERE table_catalog = current_database()
|
|
797
|
+
AND table_schema NOT IN ${REDSHIFT_EXCLUDED_SCHEMAS}
|
|
798
|
+
AND table_schema NOT LIKE 'pg_temp%'
|
|
799
|
+
${filter}
|
|
800
|
+
ORDER BY table_schema, table_name;`;
|
|
801
|
+
}
|
|
802
|
+
function buildRedshiftColumnsQuery(tables) {
|
|
803
|
+
const filter = buildFilterClause(tables, "table_schema", "table_name");
|
|
804
|
+
return `SELECT
|
|
805
|
+
table_name,
|
|
806
|
+
table_schema,
|
|
807
|
+
column_name,
|
|
808
|
+
data_type,
|
|
809
|
+
NULL AS udt_name,
|
|
810
|
+
remarks AS description,
|
|
811
|
+
FALSE AS is_primary_key
|
|
812
|
+
FROM svv_columns
|
|
813
|
+
WHERE table_catalog = current_database()
|
|
814
|
+
AND table_schema NOT IN ${REDSHIFT_EXCLUDED_SCHEMAS}
|
|
815
|
+
AND table_schema NOT LIKE 'pg_temp%'
|
|
816
|
+
${filter}
|
|
817
|
+
ORDER BY table_schema, table_name, ordinal_position;`;
|
|
818
|
+
}
|
|
783
819
|
function buildFilterClause(tables, schemaExpr, tableExpr) {
|
|
784
820
|
if (!tables.length) return "";
|
|
785
821
|
const clauses = tables.map(({ schema, table }) => {
|
|
@@ -2292,6 +2328,7 @@ function buildModifiedQuestion(originalQuestion, modifications, hasSessionContex
|
|
|
2292
2328
|
}
|
|
2293
2329
|
return `${originalQuestion} (${hints.join(", ")})`;
|
|
2294
2330
|
}
|
|
2331
|
+
var REWRITE_DATEFILTER_ENDPOINT = "/v2/rewrite-datefilter";
|
|
2295
2332
|
var START_PARAM_KEY_REGEX = /(^|_)(start|from)(_|$)/i;
|
|
2296
2333
|
var END_PARAM_KEY_REGEX = /(^|_)(end|to)(_|$)/i;
|
|
2297
2334
|
var ISO_DATETIME_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z?$/;
|
|
@@ -2328,31 +2365,118 @@ function normalizeGeneratedParamKey(param, index) {
|
|
|
2328
2365
|
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);
|
|
2329
2366
|
return nameCandidate.replace(/[{}]/g, "").replace(/(.+):.*$/, "$1").replace(/^[:$]/, "").trim();
|
|
2330
2367
|
}
|
|
2331
|
-
function
|
|
2368
|
+
function collectParamKeys(params, paramMetadata) {
|
|
2369
|
+
const keys = new Set(Object.keys(params));
|
|
2370
|
+
for (let i = 0; i < paramMetadata.length; i++) {
|
|
2371
|
+
const param = paramMetadata[i];
|
|
2372
|
+
if (!param) continue;
|
|
2373
|
+
keys.add(normalizeGeneratedParamKey(param, i));
|
|
2374
|
+
}
|
|
2375
|
+
return [...keys];
|
|
2376
|
+
}
|
|
2377
|
+
function placeholderKey(token) {
|
|
2378
|
+
if (token.startsWith("$") || token.startsWith("@")) return token.slice(1);
|
|
2379
|
+
return token.slice(1, -1).replace(/:.+$/, "").trim();
|
|
2380
|
+
}
|
|
2381
|
+
function isDateLike(value) {
|
|
2382
|
+
return typeof value === "string" && /^\d{4}-\d{2}-\d{2}/.test(value.trim());
|
|
2383
|
+
}
|
|
2384
|
+
function dateBoundKeysFromSql(sql) {
|
|
2385
|
+
const start = /* @__PURE__ */ new Set();
|
|
2386
|
+
const end = /* @__PURE__ */ new Set();
|
|
2387
|
+
const comparison = /(>=|<=|>|<)\s*(\$\d+|\{[^}]+\}|@[A-Za-z_][\w]*)/g;
|
|
2388
|
+
const between = /BETWEEN\s+(\$\d+|\{[^}]+\}|@[A-Za-z_][\w]*)\s+AND\s+(\$\d+|\{[^}]+\}|@[A-Za-z_][\w]*)/gi;
|
|
2389
|
+
for (const match of sql.matchAll(comparison)) {
|
|
2390
|
+
const key = placeholderKey(match[2] ?? "");
|
|
2391
|
+
if (!key) continue;
|
|
2392
|
+
if (match[1] === ">=" || match[1] === ">") start.add(key);
|
|
2393
|
+
else end.add(key);
|
|
2394
|
+
}
|
|
2395
|
+
for (const match of sql.matchAll(between)) {
|
|
2396
|
+
const startKey = placeholderKey(match[1] ?? "");
|
|
2397
|
+
const endKey = placeholderKey(match[2] ?? "");
|
|
2398
|
+
if (startKey) start.add(startKey);
|
|
2399
|
+
if (endKey) end.add(endKey);
|
|
2400
|
+
}
|
|
2401
|
+
return { start, end };
|
|
2402
|
+
}
|
|
2403
|
+
function paramValueForKey(key, params, paramMetadata) {
|
|
2404
|
+
if (key in params) return params[key];
|
|
2405
|
+
for (let i = 0; i < paramMetadata.length; i++) {
|
|
2406
|
+
const param = paramMetadata[i];
|
|
2407
|
+
if (!param) continue;
|
|
2408
|
+
if (normalizeGeneratedParamKey(param, i) === key) return param.value;
|
|
2409
|
+
}
|
|
2410
|
+
return void 0;
|
|
2411
|
+
}
|
|
2412
|
+
function dateBoundKeys(sql, params, paramMetadata, boundary) {
|
|
2413
|
+
const fromSql = dateBoundKeysFromSql(sql)[boundary];
|
|
2414
|
+
const keys = [];
|
|
2415
|
+
for (const key of collectParamKeys(params, paramMetadata)) {
|
|
2416
|
+
const named = keyLooksLikeDateBoundary(key, boundary);
|
|
2417
|
+
if (!named && !fromSql.has(key)) continue;
|
|
2418
|
+
if (!isDateLike(paramValueForKey(key, params, paramMetadata))) continue;
|
|
2419
|
+
keys.push(key);
|
|
2420
|
+
}
|
|
2421
|
+
return keys;
|
|
2422
|
+
}
|
|
2423
|
+
function canSubstituteDateRangeParams(dateRange, sql, params, paramMetadata = []) {
|
|
2424
|
+
if (!dateRange) return false;
|
|
2425
|
+
const from = normalizeDateInput(dateRange.from);
|
|
2426
|
+
const to = normalizeDateInput(dateRange.to);
|
|
2427
|
+
if (!from && !to) return false;
|
|
2428
|
+
if (from && dateBoundKeys(sql, params, paramMetadata, "start").length === 0) {
|
|
2429
|
+
return false;
|
|
2430
|
+
}
|
|
2431
|
+
if (to && dateBoundKeys(sql, params, paramMetadata, "end").length === 0) {
|
|
2432
|
+
return false;
|
|
2433
|
+
}
|
|
2434
|
+
return true;
|
|
2435
|
+
}
|
|
2436
|
+
function isDateRangeOnlySqlModification(modifications) {
|
|
2437
|
+
if (!modifications?.dateRange) return false;
|
|
2438
|
+
if (modifications.customSql) return false;
|
|
2439
|
+
if (modifications.timeGranularity) return false;
|
|
2440
|
+
if (typeof modifications.additionalInstructions === "string" && modifications.additionalInstructions.trim().length > 0) {
|
|
2441
|
+
return false;
|
|
2442
|
+
}
|
|
2443
|
+
return true;
|
|
2444
|
+
}
|
|
2445
|
+
function applyDateRangeOverrides(dateRange, sql, params, paramMetadata) {
|
|
2332
2446
|
if (!dateRange) return;
|
|
2333
2447
|
const from = normalizeDateInput(dateRange.from);
|
|
2334
2448
|
const to = normalizeDateInput(dateRange.to);
|
|
2335
2449
|
if (!from && !to) return;
|
|
2450
|
+
const startKeys = new Set(
|
|
2451
|
+
from ? dateBoundKeys(sql, params, paramMetadata, "start") : []
|
|
2452
|
+
);
|
|
2453
|
+
const endKeys = new Set(
|
|
2454
|
+
to ? dateBoundKeys(sql, params, paramMetadata, "end") : []
|
|
2455
|
+
);
|
|
2336
2456
|
for (const [key, value] of Object.entries(params)) {
|
|
2337
|
-
if (
|
|
2338
|
-
params[key] = formatDateOverride(from, "start", value);
|
|
2457
|
+
if (startKeys.has(key)) {
|
|
2458
|
+
params[key] = formatDateOverride(from ?? "", "start", value);
|
|
2339
2459
|
}
|
|
2340
|
-
if (
|
|
2341
|
-
params[key] = formatDateOverride(to, "end", value);
|
|
2460
|
+
if (endKeys.has(key)) {
|
|
2461
|
+
params[key] = formatDateOverride(to ?? "", "end", value);
|
|
2342
2462
|
}
|
|
2343
2463
|
}
|
|
2344
2464
|
for (let i = 0; i < paramMetadata.length; i++) {
|
|
2345
2465
|
const param = paramMetadata[i];
|
|
2346
2466
|
if (!param) continue;
|
|
2347
2467
|
const key = normalizeGeneratedParamKey(param, i);
|
|
2348
|
-
if (
|
|
2349
|
-
param.value = formatDateOverride(from, "start", param.value);
|
|
2468
|
+
if (startKeys.has(key)) {
|
|
2469
|
+
param.value = formatDateOverride(from ?? "", "start", param.value);
|
|
2350
2470
|
}
|
|
2351
|
-
if (
|
|
2352
|
-
param.value = formatDateOverride(to, "end", param.value);
|
|
2471
|
+
if (endKeys.has(key)) {
|
|
2472
|
+
param.value = formatDateOverride(to ?? "", "end", param.value);
|
|
2353
2473
|
}
|
|
2354
2474
|
}
|
|
2355
2475
|
}
|
|
2476
|
+
function buildPreviousParams(params, paramMetadata) {
|
|
2477
|
+
if (paramMetadata.length > 0) return paramMetadata;
|
|
2478
|
+
return Object.entries(params).map(([name, value]) => ({ name, value }));
|
|
2479
|
+
}
|
|
2356
2480
|
function buildVizHints(modifications) {
|
|
2357
2481
|
const hints = {};
|
|
2358
2482
|
if (modifications.kind) {
|
|
@@ -2403,6 +2527,9 @@ async function modifyChart(client, queryEngine, input, options, signal) {
|
|
|
2403
2527
|
const hasSqlMods = !!input.sqlModifications;
|
|
2404
2528
|
const hasVizMods = !!input.vizModifications;
|
|
2405
2529
|
const hasCustomSql = !!input.sqlModifications?.customSql;
|
|
2530
|
+
const lightweight = options?.lightweight === true;
|
|
2531
|
+
const dateOnly = lightweight && isDateRangeOnlySqlModification(input.sqlModifications);
|
|
2532
|
+
const skipChartLlm = lightweight && !hasCustomSql && (dateOnly || hasVizMods && !hasSqlMods);
|
|
2406
2533
|
resolveQueryPipeline(options?.pipeline);
|
|
2407
2534
|
const queryEndpoint = "/v2/query";
|
|
2408
2535
|
let currentSql = input.sql;
|
|
@@ -2456,6 +2583,20 @@ async function modifyChart(client, queryEngine, input, options, signal) {
|
|
|
2456
2583
|
paramMetadata = [];
|
|
2457
2584
|
queryId = void 0;
|
|
2458
2585
|
sqlChanged = true;
|
|
2586
|
+
} else if (dateOnly && canSubstituteDateRangeParams(
|
|
2587
|
+
input.sqlModifications?.dateRange,
|
|
2588
|
+
finalSql,
|
|
2589
|
+
finalParams,
|
|
2590
|
+
paramMetadata
|
|
2591
|
+
)) {
|
|
2592
|
+
finalParams = { ...finalParams };
|
|
2593
|
+
applyDateRangeOverrides(
|
|
2594
|
+
input.sqlModifications?.dateRange,
|
|
2595
|
+
finalSql,
|
|
2596
|
+
finalParams,
|
|
2597
|
+
paramMetadata
|
|
2598
|
+
);
|
|
2599
|
+
sqlChanged = false;
|
|
2459
2600
|
} else if (hasSqlMods && !hasCustomSql) {
|
|
2460
2601
|
const modifiedQuestion = buildModifiedQuestion(
|
|
2461
2602
|
input.question,
|
|
@@ -2463,18 +2604,36 @@ async function modifyChart(client, queryEngine, input, options, signal) {
|
|
|
2463
2604
|
hasQuerypanelSessionContext
|
|
2464
2605
|
);
|
|
2465
2606
|
finalQuestion = modifiedQuestion;
|
|
2466
|
-
const
|
|
2607
|
+
const sharedBody = {
|
|
2608
|
+
session_id: querypanelSessionId,
|
|
2609
|
+
previous_sql: currentSql,
|
|
2610
|
+
...tenantSettings ? { tenant_settings: tenantSettings } : {},
|
|
2611
|
+
...databaseName ? { database: databaseName } : {},
|
|
2612
|
+
...metadata?.dialect ? { dialect: metadata.dialect } : {},
|
|
2613
|
+
debug: options?.debug ?? false
|
|
2614
|
+
};
|
|
2615
|
+
const [endpoint, body] = dateOnly ? [
|
|
2616
|
+
REWRITE_DATEFILTER_ENDPOINT,
|
|
2617
|
+
{
|
|
2618
|
+
...sharedBody,
|
|
2619
|
+
question: input.question,
|
|
2620
|
+
previous_params: buildPreviousParams(finalParams, paramMetadata),
|
|
2621
|
+
date_range: {
|
|
2622
|
+
from: normalizeDateInput(input.sqlModifications?.dateRange?.from),
|
|
2623
|
+
to: normalizeDateInput(input.sqlModifications?.dateRange?.to)
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
] : [
|
|
2467
2627
|
queryEndpoint,
|
|
2468
2628
|
{
|
|
2629
|
+
...sharedBody,
|
|
2469
2630
|
question: modifiedQuestion,
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
debug: options?.debug ?? false
|
|
2477
|
-
},
|
|
2631
|
+
...options?.maxRetry ? { max_retry: options.maxRetry } : {}
|
|
2632
|
+
}
|
|
2633
|
+
];
|
|
2634
|
+
const queryResponse = await client.post(
|
|
2635
|
+
endpoint,
|
|
2636
|
+
body,
|
|
2478
2637
|
tenantId,
|
|
2479
2638
|
options?.userId,
|
|
2480
2639
|
options?.scopes,
|
|
@@ -2486,6 +2645,7 @@ async function modifyChart(client, queryEngine, input, options, signal) {
|
|
|
2486
2645
|
finalParams = queryEngine.mapGeneratedParams(paramMetadata);
|
|
2487
2646
|
applyDateRangeOverrides(
|
|
2488
2647
|
input.sqlModifications?.dateRange,
|
|
2648
|
+
finalSql,
|
|
2489
2649
|
finalParams,
|
|
2490
2650
|
paramMetadata
|
|
2491
2651
|
);
|
|
@@ -2506,7 +2666,7 @@ async function modifyChart(client, queryEngine, input, options, signal) {
|
|
|
2506
2666
|
};
|
|
2507
2667
|
const vizHints = hasVizMods ? buildVizHints(input.vizModifications) : {};
|
|
2508
2668
|
const vizHintsForChart = chartType === "vizspec" ? vizHints : stripVizSpecOnlyHints(vizHints);
|
|
2509
|
-
if (chartType === "vizspec") {
|
|
2669
|
+
if (!skipChartLlm && chartType === "vizspec") {
|
|
2510
2670
|
const vizspecResponse = await client.post(
|
|
2511
2671
|
"/vizspec",
|
|
2512
2672
|
{
|
|
@@ -2533,7 +2693,7 @@ async function modifyChart(client, queryEngine, input, options, signal) {
|
|
|
2533
2693
|
specType: "vizspec",
|
|
2534
2694
|
notes: rows.length === 0 ? vizspecResponse.notes ?? "Query returned no rows." : vizspecResponse.notes
|
|
2535
2695
|
};
|
|
2536
|
-
} else {
|
|
2696
|
+
} else if (!skipChartLlm) {
|
|
2537
2697
|
const chartResponse = await client.post(
|
|
2538
2698
|
"/chart",
|
|
2539
2699
|
{
|
|
@@ -2789,7 +2949,7 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
|
|
|
2789
2949
|
const adapter = new PostgresAdapter(clientFn, options);
|
|
2790
2950
|
const metadata = {
|
|
2791
2951
|
name,
|
|
2792
|
-
dialect:
|
|
2952
|
+
dialect: adapter.getDialect(),
|
|
2793
2953
|
description: options?.description,
|
|
2794
2954
|
tags: options?.tags,
|
|
2795
2955
|
tenantFieldName: options?.tenantFieldName,
|
|
@@ -2801,6 +2961,14 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
|
|
|
2801
2961
|
};
|
|
2802
2962
|
this.queryEngine.attachDatabase(name, adapter, metadata);
|
|
2803
2963
|
}
|
|
2964
|
+
/**
|
|
2965
|
+
* Attach an Amazon Redshift database. Redshift speaks the Postgres wire
|
|
2966
|
+
* protocol, so `clientFn` can wrap a `pg` client; introspection uses
|
|
2967
|
+
* Redshift's svv_* catalog views and SQL is generated for Redshift.
|
|
2968
|
+
*/
|
|
2969
|
+
attachRedshift(name, clientFn, options) {
|
|
2970
|
+
this.attachPostgres(name, clientFn, { ...options, dialect: "redshift" });
|
|
2971
|
+
}
|
|
2804
2972
|
attachBigQuery(name, clientFn, options) {
|
|
2805
2973
|
const adapter = new BigQueryAdapter(clientFn, options);
|
|
2806
2974
|
const metadata = {
|