@querypanel/node-sdk 1.3.0-beta.1 → 1.3.0
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 +85 -16
- package/dist/index.cjs +98 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -4
- package/dist/index.d.ts +28 -4
- package/dist/index.js +98 -14
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ const createPostgresClient = () => async (sql: string, params?: unknown[]) => {
|
|
|
56
56
|
});
|
|
57
57
|
|
|
58
58
|
qp.attachClickhouse(
|
|
59
|
-
"
|
|
59
|
+
"analytics", // uniq identifier for QueryPanel
|
|
60
60
|
(params) => clickhouse.query(params),
|
|
61
61
|
{
|
|
62
62
|
database: "analytics", // database name
|
|
@@ -65,15 +65,10 @@ qp.attachClickhouse(
|
|
|
65
65
|
},
|
|
66
66
|
);
|
|
67
67
|
|
|
68
|
-
// Syncs schema. Skips embedding if schema hasn't changed (no drift).
|
|
69
|
-
// Pass { forceReindex: true } to force re-embedding.
|
|
70
|
-
await qp.syncSchema("analytics", { tenantId: "tenant_123" });
|
|
71
|
-
|
|
72
68
|
const response = await qp.ask("Top countries by revenue", {
|
|
73
69
|
tenantId: "tenant_123",
|
|
74
70
|
database: "analytics",
|
|
75
71
|
});
|
|
76
|
-
// Defaults to pipeline "v2" (POST /v2/query). Pass pipeline: "v1" only for legacy.
|
|
77
72
|
|
|
78
73
|
console.log(response.sql);
|
|
79
74
|
console.log(response.params);
|
|
@@ -81,23 +76,71 @@ console.table(response.rows);
|
|
|
81
76
|
console.log(response.chart.vegaLiteSpec);
|
|
82
77
|
```
|
|
83
78
|
|
|
84
|
-
|
|
79
|
+
Run `syncSchema` separately — see [Schema sync](#schema-sync).
|
|
80
|
+
|
|
81
|
+
### Schema sync
|
|
82
|
+
|
|
83
|
+
`syncSchema` introspects an attached database and uploads table/column metadata so natural-language questions can generate accurate SQL. Treat it like a **migration**, not part of `ask()`:
|
|
85
84
|
|
|
86
|
-
|
|
85
|
+
- Run it once when you first connect a database.
|
|
86
|
+
- Run it again when tables or columns change.
|
|
87
|
+
- Do not call it on every request — put it in a setup/deploy script instead.
|
|
87
88
|
|
|
88
89
|
```ts
|
|
89
|
-
|
|
90
|
+
// scripts/sync-schema.ts — run once at setup, or after schema changes
|
|
91
|
+
await qp.syncSchema("pg_demo", { tenantId: "tenant_123" });
|
|
92
|
+
await qp.syncSchema("analytics", { tenantId: "tenant_123" });
|
|
93
|
+
|
|
94
|
+
// Unchanged schemas skip re-embedding. Force a rebuild only when needed:
|
|
95
|
+
await qp.syncSchema("analytics", {
|
|
90
96
|
tenantId: "tenant_123",
|
|
97
|
+
forceReindex: true,
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Optionally limit the payload with `tables: ["orders"]`.
|
|
102
|
+
|
|
103
|
+
### Custom system prompt (`systemPrompt`)
|
|
104
|
+
|
|
105
|
+
Pass `systemPrompt` on `ask()` to inject extra instructions for SQL generation. Use it when a **client** needs policies QueryPanel does not know about — retention windows, a max date range, excluded event types, and similar product rules. Look up that client’s config in your backend and pass it on every question.
|
|
106
|
+
|
|
107
|
+
The text is appended to the SQL generator (and reflection) prompts. When it conflicts with generic defaults (for example the usual 30-day trend window), the caller instructions win. It does **not** replace tenant isolation — still attach with `tenantFieldName` and pass `tenantId`. Max length is 8,000 characters. There is no constructor default; `modifyChart()` does not take `systemPrompt`.
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
function systemPromptForClient(client: { retentionDays: number }) {
|
|
111
|
+
return [
|
|
112
|
+
`Data retention: only query rows from the last ${client.retentionDays} days.`,
|
|
113
|
+
`Never return a date range longer than ${client.retentionDays} days, even if the user asks for all time or a wider window.`,
|
|
114
|
+
"Clamp any requested range to that maximum and bind start/end dates as parameters.",
|
|
115
|
+
].join(" ");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const client = await loadClient(req); // your auth + billing/retention settings
|
|
119
|
+
|
|
120
|
+
const response = await qp.ask("Revenue by product over all time", {
|
|
121
|
+
tenantId: client.tenantId,
|
|
91
122
|
database: "analytics",
|
|
92
|
-
systemPrompt:
|
|
123
|
+
systemPrompt: systemPromptForClient(client),
|
|
93
124
|
});
|
|
94
125
|
```
|
|
95
126
|
|
|
96
|
-
|
|
127
|
+
Other per-client instructions:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
const response = await qp.ask("Show order volume by week", {
|
|
131
|
+
tenantId: client.tenantId,
|
|
132
|
+
database: "analytics",
|
|
133
|
+
systemPrompt: [
|
|
134
|
+
"Default time range: last 90 days unless the user names a shorter window.",
|
|
135
|
+
"Maximum lookback: 90 days from today.",
|
|
136
|
+
"Exclude orders with status refunded or cancelled.",
|
|
137
|
+
].join(" "),
|
|
138
|
+
});
|
|
139
|
+
```
|
|
97
140
|
|
|
98
|
-
### Restricting
|
|
141
|
+
### Restricting chart types
|
|
99
142
|
|
|
100
|
-
If your UI only supports a subset of
|
|
143
|
+
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`).
|
|
101
144
|
|
|
102
145
|
```ts
|
|
103
146
|
import { QueryPanelSdkAPI, ALL_VIZ_CHART_TYPES, type ChartType } from "@querypanel/sdk";
|
|
@@ -112,8 +155,6 @@ const qp = new QueryPanelSdkAPI(url, privateKey, workspaceId, {
|
|
|
112
155
|
await qp.ask("Revenue by month", {
|
|
113
156
|
tenantId: "t1",
|
|
114
157
|
database: "analytics",
|
|
115
|
-
chartType: "vizspec",
|
|
116
|
-
pipeline: "v2",
|
|
117
158
|
supportedChartTypes: allowed,
|
|
118
159
|
});
|
|
119
160
|
```
|
|
@@ -245,9 +286,37 @@ const { data, missingIds } = await qp.getChartsByIds(
|
|
|
245
286
|
Saved charts now include the original `prompt` so you can show the question
|
|
246
287
|
alongside chart history or reuse it in follow-up workflows.
|
|
247
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
|
+
|
|
248
317
|
## Modifying Charts
|
|
249
318
|
|
|
250
|
-
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).
|
|
251
320
|
|
|
252
321
|
### Changing Visualization Settings
|
|
253
322
|
|
package/dist/index.cjs
CHANGED
|
@@ -1968,11 +1968,17 @@ function buildSchemaRequest(databaseName, adapter, introspection, metadata) {
|
|
|
1968
1968
|
dialect,
|
|
1969
1969
|
tables
|
|
1970
1970
|
};
|
|
1971
|
-
if (metadata?.
|
|
1971
|
+
if (metadata?.enforceTenantIsolation === false) {
|
|
1972
|
+
request.tenant_settings = {
|
|
1973
|
+
tenantFieldName: metadata.tenantFieldName ?? "",
|
|
1974
|
+
tenantFieldType: metadata.tenantFieldType ?? "String",
|
|
1975
|
+
enforceTenantIsolation: false
|
|
1976
|
+
};
|
|
1977
|
+
} else if (metadata?.tenantFieldName && metadata?.tenantFieldType) {
|
|
1972
1978
|
request.tenant_settings = {
|
|
1973
1979
|
tenantFieldName: metadata.tenantFieldName,
|
|
1974
1980
|
tenantFieldType: metadata.tenantFieldType,
|
|
1975
|
-
enforceTenantIsolation:
|
|
1981
|
+
enforceTenantIsolation: true
|
|
1976
1982
|
};
|
|
1977
1983
|
}
|
|
1978
1984
|
return request;
|
|
@@ -2000,11 +2006,17 @@ async function ask(client, queryEngine, question, options, signal) {
|
|
|
2000
2006
|
const databaseName = options.database ?? queryEngine.getDefaultDatabase();
|
|
2001
2007
|
const metadata = databaseName ? queryEngine.getDatabaseMetadata(databaseName) : void 0;
|
|
2002
2008
|
let tenantSettings;
|
|
2003
|
-
if (metadata?.
|
|
2009
|
+
if (metadata?.enforceTenantIsolation === false) {
|
|
2010
|
+
tenantSettings = {
|
|
2011
|
+
tenantFieldName: metadata.tenantFieldName ?? "",
|
|
2012
|
+
tenantFieldType: metadata.tenantFieldType ?? "String",
|
|
2013
|
+
enforceTenantIsolation: false
|
|
2014
|
+
};
|
|
2015
|
+
} else if (metadata?.tenantFieldName) {
|
|
2004
2016
|
tenantSettings = {
|
|
2005
2017
|
tenantFieldName: metadata.tenantFieldName,
|
|
2006
2018
|
tenantFieldType: metadata.tenantFieldType,
|
|
2007
|
-
enforceTenantIsolation:
|
|
2019
|
+
enforceTenantIsolation: true
|
|
2008
2020
|
};
|
|
2009
2021
|
}
|
|
2010
2022
|
const queryResponse = await client.postWithHeaders(
|
|
@@ -2182,6 +2194,25 @@ function anonymizeResults(rows) {
|
|
|
2182
2194
|
});
|
|
2183
2195
|
}
|
|
2184
2196
|
|
|
2197
|
+
// src/routes/queries.ts
|
|
2198
|
+
async function resolveQuery(client, queryEngine, queryId, options, signal) {
|
|
2199
|
+
const response = await client.get(
|
|
2200
|
+
`/v2/queries/${encodeURIComponent(queryId)}`,
|
|
2201
|
+
options.tenantId,
|
|
2202
|
+
options.userId,
|
|
2203
|
+
options.scopes,
|
|
2204
|
+
signal
|
|
2205
|
+
);
|
|
2206
|
+
const paramMetadata = Array.isArray(response.params) ? response.params : [];
|
|
2207
|
+
return {
|
|
2208
|
+
queryId: response.queryId,
|
|
2209
|
+
sql: response.sql,
|
|
2210
|
+
params: queryEngine.mapGeneratedParams(paramMetadata),
|
|
2211
|
+
paramMetadata,
|
|
2212
|
+
dialect: response.dialect
|
|
2213
|
+
};
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2185
2216
|
// src/routes/modify.ts
|
|
2186
2217
|
function buildModifiedQuestion(originalQuestion, modifications, pipeline, hasSessionContext = false) {
|
|
2187
2218
|
const hints = [];
|
|
@@ -2336,11 +2367,28 @@ async function modifyChart(client, queryEngine, input, options, signal) {
|
|
|
2336
2367
|
const hasCustomSql = !!input.sqlModifications?.customSql;
|
|
2337
2368
|
const pipeline = resolveQueryPipeline(options?.pipeline);
|
|
2338
2369
|
const queryEndpoint = pipeline === "v2" ? "/v2/query" : "/query";
|
|
2339
|
-
let
|
|
2370
|
+
let currentSql = input.sql;
|
|
2340
2371
|
let finalParams = input.params ?? {};
|
|
2341
2372
|
let paramMetadata = [];
|
|
2342
2373
|
let rationale;
|
|
2343
2374
|
let queryId;
|
|
2375
|
+
if (input.queryId) {
|
|
2376
|
+
const resolved = await resolveQuery(
|
|
2377
|
+
client,
|
|
2378
|
+
queryEngine,
|
|
2379
|
+
input.queryId,
|
|
2380
|
+
{ tenantId, userId: options?.userId, scopes: options?.scopes },
|
|
2381
|
+
signal
|
|
2382
|
+
);
|
|
2383
|
+
currentSql = resolved.sql;
|
|
2384
|
+
finalParams = resolved.params;
|
|
2385
|
+
paramMetadata = resolved.paramMetadata;
|
|
2386
|
+
queryId = resolved.queryId;
|
|
2387
|
+
}
|
|
2388
|
+
if (!currentSql) {
|
|
2389
|
+
throw new Error("modifyChart requires either queryId or sql.");
|
|
2390
|
+
}
|
|
2391
|
+
let finalSql = currentSql;
|
|
2344
2392
|
let sqlChanged = false;
|
|
2345
2393
|
let finalQuestion = input.question;
|
|
2346
2394
|
const databaseName = input.database ?? queryEngine.getDefaultDatabase();
|
|
@@ -2351,17 +2399,24 @@ async function modifyChart(client, queryEngine, input, options, signal) {
|
|
|
2351
2399
|
}
|
|
2352
2400
|
const metadata = queryEngine.getDatabaseMetadata(databaseName);
|
|
2353
2401
|
let tenantSettings;
|
|
2354
|
-
if (metadata?.
|
|
2402
|
+
if (metadata?.enforceTenantIsolation === false) {
|
|
2403
|
+
tenantSettings = {
|
|
2404
|
+
tenantFieldName: metadata.tenantFieldName ?? "",
|
|
2405
|
+
tenantFieldType: metadata.tenantFieldType ?? "String",
|
|
2406
|
+
enforceTenantIsolation: false
|
|
2407
|
+
};
|
|
2408
|
+
} else if (metadata?.tenantFieldName) {
|
|
2355
2409
|
tenantSettings = {
|
|
2356
2410
|
tenantFieldName: metadata.tenantFieldName,
|
|
2357
2411
|
tenantFieldType: metadata.tenantFieldType,
|
|
2358
|
-
enforceTenantIsolation:
|
|
2412
|
+
enforceTenantIsolation: true
|
|
2359
2413
|
};
|
|
2360
2414
|
}
|
|
2361
2415
|
if (hasCustomSql) {
|
|
2362
2416
|
finalSql = input.sqlModifications.customSql;
|
|
2363
2417
|
finalParams = {};
|
|
2364
2418
|
paramMetadata = [];
|
|
2419
|
+
queryId = void 0;
|
|
2365
2420
|
sqlChanged = true;
|
|
2366
2421
|
} else if (hasSqlMods && !hasCustomSql) {
|
|
2367
2422
|
const modifiedQuestion = buildModifiedQuestion(
|
|
@@ -2378,7 +2433,7 @@ async function modifyChart(client, queryEngine, input, options, signal) {
|
|
|
2378
2433
|
{
|
|
2379
2434
|
question: modifiedQuestion,
|
|
2380
2435
|
session_id: querypanelSessionId,
|
|
2381
|
-
previous_sql:
|
|
2436
|
+
previous_sql: currentSql,
|
|
2382
2437
|
...options?.maxRetry ? { max_retry: options.maxRetry } : {},
|
|
2383
2438
|
...tenantSettings ? { tenant_settings: tenantSettings } : {},
|
|
2384
2439
|
...databaseName ? { database: databaseName } : {},
|
|
@@ -2403,7 +2458,7 @@ async function modifyChart(client, queryEngine, input, options, signal) {
|
|
|
2403
2458
|
}
|
|
2404
2459
|
rationale = queryResponse.rationale;
|
|
2405
2460
|
queryId = queryResponse.queryId;
|
|
2406
|
-
sqlChanged = finalSql !==
|
|
2461
|
+
sqlChanged = finalSql !== currentSql;
|
|
2407
2462
|
}
|
|
2408
2463
|
const execution = await queryEngine.validateAndExecute(
|
|
2409
2464
|
finalSql,
|
|
@@ -2611,6 +2666,12 @@ var ALL_VIZ_CHART_TYPES = [
|
|
|
2611
2666
|
];
|
|
2612
2667
|
|
|
2613
2668
|
// src/index.ts
|
|
2669
|
+
function resolveAttachEnforceTenantIsolation(tenantFieldName, enforceTenantIsolation) {
|
|
2670
|
+
if (enforceTenantIsolation !== void 0) {
|
|
2671
|
+
return enforceTenantIsolation;
|
|
2672
|
+
}
|
|
2673
|
+
return tenantFieldName ? true : void 0;
|
|
2674
|
+
}
|
|
2614
2675
|
var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
|
|
2615
2676
|
/**
|
|
2616
2677
|
* @param workspaceId - Workspace UUID (same value as your org in the dashboard). Passed to the API as JWT claim `organizationId`.
|
|
@@ -2681,7 +2742,10 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
|
|
|
2681
2742
|
tags: options?.tags,
|
|
2682
2743
|
tenantFieldName: options?.tenantFieldName,
|
|
2683
2744
|
tenantFieldType: options?.tenantFieldType ?? "String",
|
|
2684
|
-
enforceTenantIsolation:
|
|
2745
|
+
enforceTenantIsolation: resolveAttachEnforceTenantIsolation(
|
|
2746
|
+
options?.tenantFieldName,
|
|
2747
|
+
options?.enforceTenantIsolation
|
|
2748
|
+
)
|
|
2685
2749
|
};
|
|
2686
2750
|
this.queryEngine.attachDatabase(name, adapter, metadata);
|
|
2687
2751
|
}
|
|
@@ -2694,7 +2758,10 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
|
|
|
2694
2758
|
tags: options?.tags,
|
|
2695
2759
|
tenantFieldName: options?.tenantFieldName,
|
|
2696
2760
|
tenantFieldType: options?.tenantFieldType ?? "String",
|
|
2697
|
-
enforceTenantIsolation:
|
|
2761
|
+
enforceTenantIsolation: resolveAttachEnforceTenantIsolation(
|
|
2762
|
+
options?.tenantFieldName,
|
|
2763
|
+
options?.enforceTenantIsolation
|
|
2764
|
+
)
|
|
2698
2765
|
};
|
|
2699
2766
|
this.queryEngine.attachDatabase(name, adapter, metadata);
|
|
2700
2767
|
}
|
|
@@ -2707,7 +2774,10 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
|
|
|
2707
2774
|
tags: options?.tags,
|
|
2708
2775
|
tenantFieldName: options?.tenantFieldName,
|
|
2709
2776
|
tenantFieldType: options?.tenantFieldType ?? "String",
|
|
2710
|
-
enforceTenantIsolation:
|
|
2777
|
+
enforceTenantIsolation: resolveAttachEnforceTenantIsolation(
|
|
2778
|
+
options?.tenantFieldName,
|
|
2779
|
+
options?.enforceTenantIsolation
|
|
2780
|
+
)
|
|
2711
2781
|
};
|
|
2712
2782
|
this.queryEngine.attachDatabase(name, adapter, metadata);
|
|
2713
2783
|
}
|
|
@@ -2833,9 +2903,23 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
|
|
|
2833
2903
|
throw new Error("tenantId is required");
|
|
2834
2904
|
}
|
|
2835
2905
|
const databaseName = input.database ?? "db";
|
|
2836
|
-
|
|
2906
|
+
let sql = input.sql;
|
|
2907
|
+
let params = input.params ?? {};
|
|
2908
|
+
if (input.queryId) {
|
|
2909
|
+
const resolved = await resolveQuery(
|
|
2910
|
+
this.client,
|
|
2911
|
+
this.queryEngine,
|
|
2912
|
+
input.queryId,
|
|
2913
|
+
{ tenantId }
|
|
2914
|
+
);
|
|
2915
|
+
sql = resolved.sql;
|
|
2916
|
+
params = resolved.params;
|
|
2917
|
+
}
|
|
2918
|
+
if (!sql) {
|
|
2919
|
+
throw new Error("Either queryId or sql is required");
|
|
2920
|
+
}
|
|
2837
2921
|
const result = await this.queryEngine.validateAndExecute(
|
|
2838
|
-
|
|
2922
|
+
sql,
|
|
2839
2923
|
params,
|
|
2840
2924
|
databaseName,
|
|
2841
2925
|
tenantId
|