@querypanel/node-sdk 1.0.43 → 1.0.45

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
@@ -80,6 +80,54 @@ console.table(response.rows);
80
80
  console.log(response.chart.vegaLiteSpec);
81
81
  ```
82
82
 
83
+ ## Session History & Context-Aware Queries
84
+
85
+ The SDK can link related questions into a session so follow-ups like “filter that to Europe” use prior context. The backend generates a QueryPanel session ID for every query and returns it in the response so you can reuse it.
86
+
87
+ ```ts
88
+ const first = await qp.ask("Revenue by country", {
89
+ tenantId: "tenant_123",
90
+ database: "analytics",
91
+ });
92
+
93
+ const querypanelSessionId = first.querypanelSessionId;
94
+
95
+ const followUp = await qp.ask("Now filter that to Europe", {
96
+ tenantId: "tenant_123",
97
+ database: "analytics",
98
+ querypanelSessionId, // same QueryPanel session keeps context
99
+ });
100
+
101
+ console.log(followUp.sql);
102
+ ```
103
+
104
+ ### Managing Session History
105
+
106
+ ```ts
107
+ // List sessions
108
+ const sessions = await qp.listSessions({
109
+ tenantId: "tenant_123",
110
+ pagination: { page: 1, limit: 20 },
111
+ sortBy: "updated_at",
112
+ });
113
+
114
+ // Get a session with its turns
115
+ const session = await qp.getSession("session_abc123", {
116
+ tenantId: "tenant_123",
117
+ includeTurns: true,
118
+ });
119
+
120
+ // Update session title
121
+ await qp.updateSession(
122
+ "session_abc123",
123
+ { title: "Q4 Revenue Analysis" },
124
+ { tenantId: "tenant_123" },
125
+ );
126
+
127
+ // Delete a session
128
+ await qp.deleteSession("session_abc123", { tenantId: "tenant_123" });
129
+ ```
130
+
83
131
  ## Saving & Managing Charts
84
132
 
85
133
  The SDK allows you to save generated charts to the QueryPanel system.
package/dist/index.cjs CHANGED
@@ -32,7 +32,9 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  ClickHouseAdapter: () => ClickHouseAdapter,
34
34
  PostgresAdapter: () => PostgresAdapter,
35
+ QueryErrorCode: () => QueryErrorCode,
35
36
  QueryPanelSdkAPI: () => QueryPanelSdkAPI,
37
+ QueryPipelineError: () => QueryPipelineError,
36
38
  anonymizeResults: () => anonymizeResults
37
39
  });
38
40
  module.exports = __toCommonJS(index_exports);
@@ -626,6 +628,22 @@ var ApiClient = class {
626
628
  signal
627
629
  });
628
630
  }
631
+ async postWithHeaders(path, body, tenantId, userId, scopes, signal, sessionId) {
632
+ const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
633
+ method: "POST",
634
+ headers: await this.buildHeaders(
635
+ tenantId,
636
+ userId,
637
+ scopes,
638
+ true,
639
+ sessionId
640
+ ),
641
+ body: JSON.stringify(body ?? {}),
642
+ signal
643
+ });
644
+ const data = await this.parseResponse(response);
645
+ return { data, headers: response.headers };
646
+ }
629
647
  async put(path, body, tenantId, userId, scopes, signal, sessionId) {
630
648
  return await this.request(path, {
631
649
  method: "PUT",
@@ -640,6 +658,20 @@ var ApiClient = class {
640
658
  signal
641
659
  });
642
660
  }
661
+ async patch(path, body, tenantId, userId, scopes, signal, sessionId) {
662
+ return await this.request(path, {
663
+ method: "PATCH",
664
+ headers: await this.buildHeaders(
665
+ tenantId,
666
+ userId,
667
+ scopes,
668
+ true,
669
+ sessionId
670
+ ),
671
+ body: JSON.stringify(body ?? {}),
672
+ signal
673
+ });
674
+ }
643
675
  async delete(path, tenantId, userId, scopes, signal, sessionId) {
644
676
  return await this.request(path, {
645
677
  method: "DELETE",
@@ -655,6 +687,9 @@ var ApiClient = class {
655
687
  }
656
688
  async request(path, init) {
657
689
  const response = await this.fetchImpl(`${this.baseUrl}${path}`, init);
690
+ return await this.parseResponse(response);
691
+ }
692
+ async parseResponse(response) {
658
693
  const text = await response.text();
659
694
  let json;
660
695
  try {
@@ -881,6 +916,57 @@ var QueryEngine = class {
881
916
  }
882
917
  };
883
918
 
919
+ // src/errors.ts
920
+ var QueryErrorCode = {
921
+ // Moderation errors
922
+ MODERATION_FAILED: "MODERATION_FAILED",
923
+ // Guardrail errors
924
+ RELEVANCE_CHECK_FAILED: "RELEVANCE_CHECK_FAILED",
925
+ SECURITY_CHECK_FAILED: "SECURITY_CHECK_FAILED",
926
+ // SQL generation errors
927
+ SQL_GENERATION_FAILED: "SQL_GENERATION_FAILED",
928
+ // SQL validation errors
929
+ SQL_VALIDATION_FAILED: "SQL_VALIDATION_FAILED",
930
+ // Context retrieval errors
931
+ CONTEXT_RETRIEVAL_FAILED: "CONTEXT_RETRIEVAL_FAILED",
932
+ // General errors
933
+ INTERNAL_ERROR: "INTERNAL_ERROR",
934
+ AUTHENTICATION_REQUIRED: "AUTHENTICATION_REQUIRED",
935
+ VALIDATION_ERROR: "VALIDATION_ERROR"
936
+ };
937
+ var QueryPipelineError = class extends Error {
938
+ constructor(message, code, details) {
939
+ super(message);
940
+ this.code = code;
941
+ this.details = details;
942
+ this.name = "QueryPipelineError";
943
+ }
944
+ /**
945
+ * Check if this is a moderation error
946
+ */
947
+ isModeration() {
948
+ return this.code === QueryErrorCode.MODERATION_FAILED;
949
+ }
950
+ /**
951
+ * Check if this is a relevance error (question not related to database)
952
+ */
953
+ isRelevanceError() {
954
+ return this.code === QueryErrorCode.RELEVANCE_CHECK_FAILED;
955
+ }
956
+ /**
957
+ * Check if this is a security error (SQL injection, prompt injection, etc.)
958
+ */
959
+ isSecurityError() {
960
+ return this.code === QueryErrorCode.SECURITY_CHECK_FAILED;
961
+ }
962
+ /**
963
+ * Check if this is any guardrail error (relevance or security)
964
+ */
965
+ isGuardrailError() {
966
+ return this.isRelevanceError() || this.isSecurityError();
967
+ }
968
+ };
969
+
884
970
  // src/routes/charts.ts
885
971
  async function createChart(client, body, options, signal) {
886
972
  const tenantId = resolveTenantId(client, options?.tenantId);
@@ -917,15 +1003,10 @@ async function listCharts(client, queryEngine, options, signal) {
917
1003
  );
918
1004
  if (options?.includeData) {
919
1005
  response.data = await Promise.all(
920
- response.data.map(async (chart) => ({
921
- ...chart,
922
- vega_lite_spec: {
923
- ...chart.vega_lite_spec,
924
- data: {
925
- values: await executeChartQuery(queryEngine, chart, tenantId)
926
- }
927
- }
928
- }))
1006
+ response.data.map(async (chart) => {
1007
+ const rows = await executeChartQuery(queryEngine, chart, tenantId);
1008
+ return hydrateChartWithData(chart, rows);
1009
+ })
929
1010
  );
930
1011
  }
931
1012
  return response;
@@ -939,15 +1020,8 @@ async function getChart(client, queryEngine, id, options, signal) {
939
1020
  options?.scopes,
940
1021
  signal
941
1022
  );
942
- return {
943
- ...chart,
944
- vega_lite_spec: {
945
- ...chart.vega_lite_spec,
946
- data: {
947
- values: await executeChartQuery(queryEngine, chart, tenantId)
948
- }
949
- }
950
- };
1023
+ const rows = await executeChartQuery(queryEngine, chart, tenantId);
1024
+ return hydrateChartWithData(chart, rows);
951
1025
  }
952
1026
  async function updateChart(client, id, body, options, signal) {
953
1027
  const tenantId = resolveTenantId(client, options?.tenantId);
@@ -998,6 +1072,31 @@ async function executeChartQuery(queryEngine, chart, tenantId) {
998
1072
  return [];
999
1073
  }
1000
1074
  }
1075
+ function hydrateChartWithData(chart, rows) {
1076
+ const spec = chart.vega_lite_spec;
1077
+ if (chart.spec_type === "vizspec") {
1078
+ const existingData = spec.data ?? {};
1079
+ return {
1080
+ ...chart,
1081
+ vega_lite_spec: {
1082
+ ...spec,
1083
+ data: {
1084
+ ...existingData,
1085
+ values: rows
1086
+ }
1087
+ }
1088
+ };
1089
+ }
1090
+ return {
1091
+ ...chart,
1092
+ vega_lite_spec: {
1093
+ ...spec,
1094
+ data: {
1095
+ values: rows
1096
+ }
1097
+ }
1098
+ };
1099
+ }
1001
1100
 
1002
1101
  // src/routes/active-charts.ts
1003
1102
  async function createActiveChart(client, body, options, signal) {
@@ -1172,6 +1271,7 @@ var import_node_crypto3 = __toESM(require("crypto"), 1);
1172
1271
  async function ask(client, queryEngine, question, options, signal) {
1173
1272
  const tenantId = resolveTenantId4(client, options.tenantId);
1174
1273
  const sessionId = import_node_crypto3.default.randomUUID();
1274
+ const querypanelSessionId = options.querypanelSessionId ?? sessionId;
1175
1275
  const maxRetry = options.maxRetry ?? 0;
1176
1276
  let attempt = 0;
1177
1277
  let lastError = options.lastError;
@@ -1188,10 +1288,11 @@ async function ask(client, queryEngine, question, options, signal) {
1188
1288
  enforceTenantIsolation: metadata.enforceTenantIsolation
1189
1289
  };
1190
1290
  }
1191
- const queryResponse = await client.post(
1291
+ const queryResponse = await client.postWithHeaders(
1192
1292
  "/query",
1193
1293
  {
1194
1294
  question,
1295
+ ...querypanelSessionId ? { session_id: querypanelSessionId } : {},
1195
1296
  ...lastError ? { last_error: lastError } : {},
1196
1297
  ...previousSql ? { previous_sql: previousSql } : {},
1197
1298
  ...options.maxRetry ? { max_retry: options.maxRetry } : {},
@@ -1205,17 +1306,30 @@ async function ask(client, queryEngine, question, options, signal) {
1205
1306
  signal,
1206
1307
  sessionId
1207
1308
  );
1208
- const dbName = queryResponse.database ?? options.database ?? queryEngine.getDefaultDatabase();
1309
+ const responseSessionId = queryResponse.headers.get("x-querypanel-session-id") ?? querypanelSessionId;
1310
+ if (!queryResponse.data.success) {
1311
+ throw new QueryPipelineError(
1312
+ queryResponse.data.error || "Query generation failed",
1313
+ queryResponse.data.code || "INTERNAL_ERROR",
1314
+ queryResponse.data.details
1315
+ );
1316
+ }
1317
+ const sql = queryResponse.data.sql;
1318
+ const dialect = queryResponse.data.dialect;
1319
+ if (!sql || !dialect) {
1320
+ throw new Error("Query response missing required SQL or dialect");
1321
+ }
1322
+ const dbName = queryResponse.data.database ?? options.database ?? queryEngine.getDefaultDatabase();
1209
1323
  if (!dbName) {
1210
1324
  throw new Error(
1211
1325
  "No database attached. Call attachPostgres/attachClickhouse first."
1212
1326
  );
1213
1327
  }
1214
- const paramMetadata = Array.isArray(queryResponse.params) ? queryResponse.params : [];
1328
+ const paramMetadata = Array.isArray(queryResponse.data.params) ? queryResponse.data.params : [];
1215
1329
  const paramValues = queryEngine.mapGeneratedParams(paramMetadata);
1216
1330
  try {
1217
1331
  const execution = await queryEngine.validateAndExecute(
1218
- queryResponse.sql,
1332
+ sql,
1219
1333
  paramValues,
1220
1334
  dbName,
1221
1335
  tenantId
@@ -1232,12 +1346,12 @@ async function ask(client, queryEngine, question, options, signal) {
1232
1346
  "/vizspec",
1233
1347
  {
1234
1348
  question,
1235
- sql: queryResponse.sql,
1236
- rationale: queryResponse.rationale,
1349
+ sql,
1350
+ rationale: queryResponse.data.rationale,
1237
1351
  fields: execution.fields,
1238
1352
  rows: anonymizeResults(rows),
1239
1353
  max_retries: options.chartMaxRetries ?? 3,
1240
- query_id: queryResponse.queryId
1354
+ query_id: queryResponse.data.queryId
1241
1355
  },
1242
1356
  tenantId,
1243
1357
  options.userId,
@@ -1255,12 +1369,12 @@ async function ask(client, queryEngine, question, options, signal) {
1255
1369
  "/chart",
1256
1370
  {
1257
1371
  question,
1258
- sql: queryResponse.sql,
1259
- rationale: queryResponse.rationale,
1372
+ sql,
1373
+ rationale: queryResponse.data.rationale,
1260
1374
  fields: execution.fields,
1261
1375
  rows: anonymizeResults(rows),
1262
1376
  max_retries: options.chartMaxRetries ?? 3,
1263
- query_id: queryResponse.queryId
1377
+ query_id: queryResponse.data.queryId
1264
1378
  },
1265
1379
  tenantId,
1266
1380
  options.userId,
@@ -1279,18 +1393,19 @@ async function ask(client, queryEngine, question, options, signal) {
1279
1393
  }
1280
1394
  }
1281
1395
  return {
1282
- sql: queryResponse.sql,
1396
+ sql,
1283
1397
  params: paramValues,
1284
1398
  paramMetadata,
1285
- rationale: queryResponse.rationale,
1286
- dialect: queryResponse.dialect,
1287
- queryId: queryResponse.queryId,
1399
+ rationale: queryResponse.data.rationale,
1400
+ dialect,
1401
+ queryId: queryResponse.data.queryId,
1288
1402
  rows,
1289
1403
  fields: execution.fields,
1290
1404
  chart,
1291
- context: queryResponse.context,
1405
+ context: queryResponse.data.context,
1292
1406
  attempts: attempt + 1,
1293
- target_db: dbName
1407
+ target_db: dbName,
1408
+ querypanelSessionId: responseSessionId ?? void 0
1294
1409
  };
1295
1410
  } catch (error) {
1296
1411
  attempt++;
@@ -1298,7 +1413,7 @@ async function ask(client, queryEngine, question, options, signal) {
1298
1413
  throw error;
1299
1414
  }
1300
1415
  lastError = error instanceof Error ? error.message : String(error);
1301
- previousSql = queryResponse.sql;
1416
+ previousSql = queryResponse.data.sql ?? previousSql;
1302
1417
  console.warn(
1303
1418
  `SQL execution failed (attempt ${attempt}/${maxRetry + 1}): ${lastError}. Retrying...`
1304
1419
  );
@@ -1533,10 +1648,79 @@ async function modifyChart(client, queryEngine, input, options, signal) {
1533
1648
  };
1534
1649
  }
1535
1650
 
1651
+ // src/routes/sessions.ts
1652
+ async function listSessions(client, options, signal) {
1653
+ const tenantId = resolveTenantId6(client, options?.tenantId);
1654
+ const params = new URLSearchParams();
1655
+ if (options?.pagination?.page)
1656
+ params.set("page", `${options.pagination.page}`);
1657
+ if (options?.pagination?.limit)
1658
+ params.set("limit", `${options.pagination.limit}`);
1659
+ if (options?.sortBy) params.set("sort_by", options.sortBy);
1660
+ if (options?.sortDir) params.set("sort_dir", options.sortDir);
1661
+ if (options?.title) params.set("title", options.title);
1662
+ if (options?.userFilter) params.set("user_id", options.userFilter);
1663
+ if (options?.createdFrom) params.set("created_from", options.createdFrom);
1664
+ if (options?.createdTo) params.set("created_to", options.createdTo);
1665
+ if (options?.updatedFrom) params.set("updated_from", options.updatedFrom);
1666
+ if (options?.updatedTo) params.set("updated_to", options.updatedTo);
1667
+ return await client.get(
1668
+ `/sessions${params.toString() ? `?${params.toString()}` : ""}`,
1669
+ tenantId,
1670
+ options?.userId,
1671
+ options?.scopes,
1672
+ signal
1673
+ );
1674
+ }
1675
+ async function getSession(client, sessionId, options, signal) {
1676
+ const tenantId = resolveTenantId6(client, options?.tenantId);
1677
+ const params = new URLSearchParams();
1678
+ if (options?.includeTurns !== void 0) {
1679
+ params.set("include_turns", `${options.includeTurns}`);
1680
+ }
1681
+ return await client.get(
1682
+ `/sessions/${encodeURIComponent(sessionId)}${params.toString() ? `?${params.toString()}` : ""}`,
1683
+ tenantId,
1684
+ options?.userId,
1685
+ options?.scopes,
1686
+ signal
1687
+ );
1688
+ }
1689
+ async function updateSession(client, sessionId, body, options, signal) {
1690
+ const tenantId = resolveTenantId6(client, options?.tenantId);
1691
+ return await client.patch(
1692
+ `/sessions/${encodeURIComponent(sessionId)}`,
1693
+ body,
1694
+ tenantId,
1695
+ options?.userId,
1696
+ options?.scopes,
1697
+ signal
1698
+ );
1699
+ }
1700
+ async function deleteSession(client, sessionId, options, signal) {
1701
+ const tenantId = resolveTenantId6(client, options?.tenantId);
1702
+ await client.delete(
1703
+ `/sessions/${encodeURIComponent(sessionId)}`,
1704
+ tenantId,
1705
+ options?.userId,
1706
+ options?.scopes,
1707
+ signal
1708
+ );
1709
+ }
1710
+ function resolveTenantId6(client, tenantId) {
1711
+ const resolved = tenantId ?? client.getDefaultTenantId();
1712
+ if (!resolved) {
1713
+ throw new Error(
1714
+ "tenantId is required. Provide it per request or via defaultTenantId option."
1715
+ );
1716
+ }
1717
+ return resolved;
1718
+ }
1719
+
1536
1720
  // src/routes/vizspec.ts
1537
1721
  var import_node_crypto5 = __toESM(require("crypto"), 1);
1538
1722
  async function generateVizSpec(client, input, options, signal) {
1539
- const tenantId = resolveTenantId6(client, options?.tenantId);
1723
+ const tenantId = resolveTenantId7(client, options?.tenantId);
1540
1724
  const sessionId = import_node_crypto5.default.randomUUID();
1541
1725
  const response = await client.post(
1542
1726
  "/vizspec",
@@ -1557,7 +1741,7 @@ async function generateVizSpec(client, input, options, signal) {
1557
1741
  );
1558
1742
  return response;
1559
1743
  }
1560
- function resolveTenantId6(client, tenantId) {
1744
+ function resolveTenantId7(client, tenantId) {
1561
1745
  const resolved = tenantId ?? client.getDefaultTenantId();
1562
1746
  if (!resolved) {
1563
1747
  throw new Error(
@@ -1670,6 +1854,7 @@ var QueryPanelSdkAPI = class {
1670
1854
  * console.log(result.sql); // Generated SQL
1671
1855
  * console.log(result.rows); // Query results
1672
1856
  * console.log(result.chart); // Vega-Lite chart spec
1857
+ * console.log(result.querypanelSessionId); // Use for follow-ups
1673
1858
  *
1674
1859
  * // With automatic SQL repair on failure
1675
1860
  * const result = await qp.ask("Show monthly trends", {
@@ -1845,6 +2030,92 @@ var QueryPanelSdkAPI = class {
1845
2030
  signal
1846
2031
  );
1847
2032
  }
2033
+ // Session history CRUD operations
2034
+ /**
2035
+ * Lists query sessions with pagination and filtering.
2036
+ *
2037
+ * @param options - Filtering, pagination, and sort options
2038
+ * @param signal - Optional AbortSignal for cancellation
2039
+ * @returns Paginated list of sessions
2040
+ *
2041
+ * @example
2042
+ * ```typescript
2043
+ * const sessions = await qp.listSessions({
2044
+ * tenantId: "tenant_123",
2045
+ * pagination: { page: 1, limit: 20 },
2046
+ * sortBy: "updated_at",
2047
+ * });
2048
+ * ```
2049
+ */
2050
+ async listSessions(options, signal) {
2051
+ return await listSessions(this.client, options, signal);
2052
+ }
2053
+ /**
2054
+ * Retrieves a session by session_id with optional turn history.
2055
+ *
2056
+ * @param sessionId - QueryPanel session identifier used in ask()
2057
+ * @param options - Tenant, user, scopes, and includeTurns flag
2058
+ * @param signal - Optional AbortSignal for cancellation
2059
+ * @returns Session metadata with optional turns
2060
+ *
2061
+ * @example
2062
+ * ```typescript
2063
+ * const session = await qp.getSession("session_123", {
2064
+ * tenantId: "tenant_123",
2065
+ * includeTurns: true,
2066
+ * });
2067
+ * ```
2068
+ */
2069
+ async getSession(sessionId, options, signal) {
2070
+ return await getSession(
2071
+ this.client,
2072
+ sessionId,
2073
+ options,
2074
+ signal
2075
+ );
2076
+ }
2077
+ /**
2078
+ * Updates session metadata (title).
2079
+ *
2080
+ * @param sessionId - QueryPanel session identifier to update
2081
+ * @param body - Fields to update
2082
+ * @param options - Tenant, user, and scope options
2083
+ * @param signal - Optional AbortSignal for cancellation
2084
+ * @returns Updated session
2085
+ *
2086
+ * @example
2087
+ * ```typescript
2088
+ * const updated = await qp.updateSession(
2089
+ * "session_123",
2090
+ * { title: "Q4 Revenue Analysis" },
2091
+ * { tenantId: "tenant_123" },
2092
+ * );
2093
+ * ```
2094
+ */
2095
+ async updateSession(sessionId, body, options, signal) {
2096
+ return await updateSession(
2097
+ this.client,
2098
+ sessionId,
2099
+ body,
2100
+ options,
2101
+ signal
2102
+ );
2103
+ }
2104
+ /**
2105
+ * Deletes a session and its turn history.
2106
+ *
2107
+ * @param sessionId - QueryPanel session identifier to delete
2108
+ * @param options - Tenant, user, and scope options
2109
+ * @param signal - Optional AbortSignal for cancellation
2110
+ *
2111
+ * @example
2112
+ * ```typescript
2113
+ * await qp.deleteSession("session_123", { tenantId: "tenant_123" });
2114
+ * ```
2115
+ */
2116
+ async deleteSession(sessionId, options, signal) {
2117
+ await deleteSession(this.client, sessionId, options, signal);
2118
+ }
1848
2119
  /**
1849
2120
  * Retrieves a single chart by ID with live data.
1850
2121
  *
@@ -2049,7 +2320,9 @@ var QueryPanelSdkAPI = class {
2049
2320
  0 && (module.exports = {
2050
2321
  ClickHouseAdapter,
2051
2322
  PostgresAdapter,
2323
+ QueryErrorCode,
2052
2324
  QueryPanelSdkAPI,
2325
+ QueryPipelineError,
2053
2326
  anonymizeResults
2054
2327
  });
2055
2328
  //# sourceMappingURL=index.cjs.map