@querypanel/node-sdk 1.0.44 → 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
@@ -628,6 +628,22 @@ var ApiClient = class {
628
628
  signal
629
629
  });
630
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
+ }
631
647
  async put(path, body, tenantId, userId, scopes, signal, sessionId) {
632
648
  return await this.request(path, {
633
649
  method: "PUT",
@@ -642,6 +658,20 @@ var ApiClient = class {
642
658
  signal
643
659
  });
644
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
+ }
645
675
  async delete(path, tenantId, userId, scopes, signal, sessionId) {
646
676
  return await this.request(path, {
647
677
  method: "DELETE",
@@ -657,6 +687,9 @@ var ApiClient = class {
657
687
  }
658
688
  async request(path, init) {
659
689
  const response = await this.fetchImpl(`${this.baseUrl}${path}`, init);
690
+ return await this.parseResponse(response);
691
+ }
692
+ async parseResponse(response) {
660
693
  const text = await response.text();
661
694
  let json;
662
695
  try {
@@ -1238,6 +1271,7 @@ var import_node_crypto3 = __toESM(require("crypto"), 1);
1238
1271
  async function ask(client, queryEngine, question, options, signal) {
1239
1272
  const tenantId = resolveTenantId4(client, options.tenantId);
1240
1273
  const sessionId = import_node_crypto3.default.randomUUID();
1274
+ const querypanelSessionId = options.querypanelSessionId ?? sessionId;
1241
1275
  const maxRetry = options.maxRetry ?? 0;
1242
1276
  let attempt = 0;
1243
1277
  let lastError = options.lastError;
@@ -1254,10 +1288,11 @@ async function ask(client, queryEngine, question, options, signal) {
1254
1288
  enforceTenantIsolation: metadata.enforceTenantIsolation
1255
1289
  };
1256
1290
  }
1257
- const queryResponse = await client.post(
1291
+ const queryResponse = await client.postWithHeaders(
1258
1292
  "/query",
1259
1293
  {
1260
1294
  question,
1295
+ ...querypanelSessionId ? { session_id: querypanelSessionId } : {},
1261
1296
  ...lastError ? { last_error: lastError } : {},
1262
1297
  ...previousSql ? { previous_sql: previousSql } : {},
1263
1298
  ...options.maxRetry ? { max_retry: options.maxRetry } : {},
@@ -1271,24 +1306,30 @@ async function ask(client, queryEngine, question, options, signal) {
1271
1306
  signal,
1272
1307
  sessionId
1273
1308
  );
1274
- if (!queryResponse.success) {
1309
+ const responseSessionId = queryResponse.headers.get("x-querypanel-session-id") ?? querypanelSessionId;
1310
+ if (!queryResponse.data.success) {
1275
1311
  throw new QueryPipelineError(
1276
- queryResponse.error || "Query generation failed",
1277
- queryResponse.code || "INTERNAL_ERROR",
1278
- queryResponse.details
1312
+ queryResponse.data.error || "Query generation failed",
1313
+ queryResponse.data.code || "INTERNAL_ERROR",
1314
+ queryResponse.data.details
1279
1315
  );
1280
1316
  }
1281
- const dbName = queryResponse.database ?? options.database ?? queryEngine.getDefaultDatabase();
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();
1282
1323
  if (!dbName) {
1283
1324
  throw new Error(
1284
1325
  "No database attached. Call attachPostgres/attachClickhouse first."
1285
1326
  );
1286
1327
  }
1287
- const paramMetadata = Array.isArray(queryResponse.params) ? queryResponse.params : [];
1328
+ const paramMetadata = Array.isArray(queryResponse.data.params) ? queryResponse.data.params : [];
1288
1329
  const paramValues = queryEngine.mapGeneratedParams(paramMetadata);
1289
1330
  try {
1290
1331
  const execution = await queryEngine.validateAndExecute(
1291
- queryResponse.sql,
1332
+ sql,
1292
1333
  paramValues,
1293
1334
  dbName,
1294
1335
  tenantId
@@ -1305,12 +1346,12 @@ async function ask(client, queryEngine, question, options, signal) {
1305
1346
  "/vizspec",
1306
1347
  {
1307
1348
  question,
1308
- sql: queryResponse.sql,
1309
- rationale: queryResponse.rationale,
1349
+ sql,
1350
+ rationale: queryResponse.data.rationale,
1310
1351
  fields: execution.fields,
1311
1352
  rows: anonymizeResults(rows),
1312
1353
  max_retries: options.chartMaxRetries ?? 3,
1313
- query_id: queryResponse.queryId
1354
+ query_id: queryResponse.data.queryId
1314
1355
  },
1315
1356
  tenantId,
1316
1357
  options.userId,
@@ -1328,12 +1369,12 @@ async function ask(client, queryEngine, question, options, signal) {
1328
1369
  "/chart",
1329
1370
  {
1330
1371
  question,
1331
- sql: queryResponse.sql,
1332
- rationale: queryResponse.rationale,
1372
+ sql,
1373
+ rationale: queryResponse.data.rationale,
1333
1374
  fields: execution.fields,
1334
1375
  rows: anonymizeResults(rows),
1335
1376
  max_retries: options.chartMaxRetries ?? 3,
1336
- query_id: queryResponse.queryId
1377
+ query_id: queryResponse.data.queryId
1337
1378
  },
1338
1379
  tenantId,
1339
1380
  options.userId,
@@ -1352,18 +1393,19 @@ async function ask(client, queryEngine, question, options, signal) {
1352
1393
  }
1353
1394
  }
1354
1395
  return {
1355
- sql: queryResponse.sql,
1396
+ sql,
1356
1397
  params: paramValues,
1357
1398
  paramMetadata,
1358
- rationale: queryResponse.rationale,
1359
- dialect: queryResponse.dialect,
1360
- queryId: queryResponse.queryId,
1399
+ rationale: queryResponse.data.rationale,
1400
+ dialect,
1401
+ queryId: queryResponse.data.queryId,
1361
1402
  rows,
1362
1403
  fields: execution.fields,
1363
1404
  chart,
1364
- context: queryResponse.context,
1405
+ context: queryResponse.data.context,
1365
1406
  attempts: attempt + 1,
1366
- target_db: dbName
1407
+ target_db: dbName,
1408
+ querypanelSessionId: responseSessionId ?? void 0
1367
1409
  };
1368
1410
  } catch (error) {
1369
1411
  attempt++;
@@ -1371,7 +1413,7 @@ async function ask(client, queryEngine, question, options, signal) {
1371
1413
  throw error;
1372
1414
  }
1373
1415
  lastError = error instanceof Error ? error.message : String(error);
1374
- previousSql = queryResponse.sql;
1416
+ previousSql = queryResponse.data.sql ?? previousSql;
1375
1417
  console.warn(
1376
1418
  `SQL execution failed (attempt ${attempt}/${maxRetry + 1}): ${lastError}. Retrying...`
1377
1419
  );
@@ -1606,10 +1648,79 @@ async function modifyChart(client, queryEngine, input, options, signal) {
1606
1648
  };
1607
1649
  }
1608
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
+
1609
1720
  // src/routes/vizspec.ts
1610
1721
  var import_node_crypto5 = __toESM(require("crypto"), 1);
1611
1722
  async function generateVizSpec(client, input, options, signal) {
1612
- const tenantId = resolveTenantId6(client, options?.tenantId);
1723
+ const tenantId = resolveTenantId7(client, options?.tenantId);
1613
1724
  const sessionId = import_node_crypto5.default.randomUUID();
1614
1725
  const response = await client.post(
1615
1726
  "/vizspec",
@@ -1630,7 +1741,7 @@ async function generateVizSpec(client, input, options, signal) {
1630
1741
  );
1631
1742
  return response;
1632
1743
  }
1633
- function resolveTenantId6(client, tenantId) {
1744
+ function resolveTenantId7(client, tenantId) {
1634
1745
  const resolved = tenantId ?? client.getDefaultTenantId();
1635
1746
  if (!resolved) {
1636
1747
  throw new Error(
@@ -1743,6 +1854,7 @@ var QueryPanelSdkAPI = class {
1743
1854
  * console.log(result.sql); // Generated SQL
1744
1855
  * console.log(result.rows); // Query results
1745
1856
  * console.log(result.chart); // Vega-Lite chart spec
1857
+ * console.log(result.querypanelSessionId); // Use for follow-ups
1746
1858
  *
1747
1859
  * // With automatic SQL repair on failure
1748
1860
  * const result = await qp.ask("Show monthly trends", {
@@ -1918,6 +2030,92 @@ var QueryPanelSdkAPI = class {
1918
2030
  signal
1919
2031
  );
1920
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
+ }
1921
2119
  /**
1922
2120
  * Retrieves a single chart by ID with live data.
1923
2121
  *