@querypanel/node-sdk 1.0.35 → 1.0.37

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
@@ -1,6 +1,6 @@
1
1
  # QueryPanel Node SDK
2
2
 
3
- A TypeScript-first client for the QueryPanel Bun/Hono API. It 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).
3
+ 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
4
 
5
5
  ## Installation
6
6
 
@@ -10,7 +10,7 @@ bun add @querypanel/sdk
10
10
  npm install @querypanel/sdk
11
11
  ```
12
12
 
13
- > **Runtime:** Node.js 18+ (or Bun). The SDK relies on the native `fetch` API.
13
+ > **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.
14
14
 
15
15
  ## Quickstart
16
16
 
@@ -72,6 +72,86 @@ console.table(response.rows);
72
72
  console.log(response.chart.vegaLiteSpec);
73
73
  ```
74
74
 
75
+ ## Saving & Managing Charts
76
+
77
+ The SDK allows you to save generated charts to the QueryPanel system.
78
+
79
+ > **Privacy Note:** QueryPanel only stores the chart definition (SQL query, parameters, and Vega-Lite spec). We **never** store the actual result data rows. The data is fetched live from your database whenever the chart is rendered or refreshed.
80
+
81
+ ```ts
82
+ // 1. Ask a question to generate a chart
83
+ const response = await qp.ask("Show revenue by country", {
84
+ tenantId: "tenant_123",
85
+ database: "analytics",
86
+ });
87
+
88
+ if (response.chart.vegaLiteSpec) {
89
+ // 2. Save the chart (only stores SQL + metadata, no data)
90
+ const savedChart = await qp.createChart({
91
+ title: "Revenue by Country",
92
+ sql: response.sql,
93
+ sql_params: response.params,
94
+ vega_lite_spec: response.chart.vegaLiteSpec,
95
+ query_id: response.queryId,
96
+ target_db: response.target_db,
97
+ }, {
98
+ tenantId: "tenant_123",
99
+ userId: "user_456" // Optional: associate with a user
100
+ });
101
+
102
+ console.log(`Chart saved with ID: ${savedChart.id}`);
103
+ }
104
+
105
+ // 3. List saved charts (History)
106
+ const charts = await qp.listCharts({ tenantId: "tenant_123" });
107
+ ```
108
+
109
+ ## Building a Dashboard (Active Charts)
110
+
111
+ While `createChart` and `listCharts` manage your **history** of saved queries, "Active Charts" are designed for building **dashboards**. You can "pin" a saved chart to a dashboard, control its order, and fetch it with live data in a single call.
112
+
113
+ ```ts
114
+ // 1. Pin a saved chart to the dashboard
115
+ const activeChart = await qp.createActiveChart({
116
+ chart_id: "saved_chart_id_from_history",
117
+ order: 1, // Optional: for sorting in UI
118
+ meta: { width: "full", variant: "dark" } // Optional: UI layout hints
119
+ }, {
120
+ tenantId: "tenant_123"
121
+ });
122
+
123
+ // 2. Load the dashboard with live data
124
+ // Passing { withData: true } executes the SQL for each chart immediately
125
+ const dashboard = await qp.listActiveCharts({
126
+ tenantId: "tenant_123",
127
+ withData: true
128
+ });
129
+
130
+ dashboard.data.forEach(item => {
131
+ console.log(`Chart: ${item.chart?.title}`);
132
+ console.log(`Data points: ${item.chart?.vega_lite_spec.data.values.length}`);
133
+ });
134
+ ```
135
+
136
+ ## Deno Support
137
+
138
+ 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:
139
+
140
+ ```ts
141
+ import { QueryPanelSdkAPI } from "https://esm.sh/@querypanel/sdk";
142
+
143
+ const qp = new QueryPanelSdkAPI(
144
+ Deno.env.get("QUERYPANEL_URL")!,
145
+ Deno.env.get("PRIVATE_KEY")!,
146
+ Deno.env.get("ORGANIZATION_ID")!,
147
+ );
148
+
149
+ // Use the SDK as normal - JWT signing works automatically
150
+ const response = await qp.ask("Show top products", {
151
+ tenantId: "tenant_123",
152
+ });
153
+ ```
154
+
75
155
  ## Building locally
76
156
 
77
157
  ```bash
package/dist/index.cjs CHANGED
@@ -554,7 +554,6 @@ function sanitize2(value) {
554
554
  }
555
555
 
556
556
  // src/core/client.ts
557
- var import_node_crypto = require("crypto");
558
557
  var ApiClient = class {
559
558
  baseUrl;
560
559
  privateKey;
@@ -562,6 +561,7 @@ var ApiClient = class {
562
561
  defaultTenantId;
563
562
  additionalHeaders;
564
563
  fetchImpl;
564
+ cryptoKey = null;
565
565
  constructor(baseUrl, privateKey, organizationId, options) {
566
566
  if (!baseUrl) {
567
567
  throw new Error("Base URL is required");
@@ -677,6 +677,66 @@ var ApiClient = class {
677
677
  }
678
678
  return headers;
679
679
  }
680
+ /**
681
+ * Base64URL encode a string (works in both Node.js 18+ and Deno)
682
+ */
683
+ base64UrlEncode(str) {
684
+ const bytes = new TextEncoder().encode(str);
685
+ let binary = "";
686
+ const chunkSize = 8192;
687
+ for (let i = 0; i < bytes.length; i += chunkSize) {
688
+ const chunk = bytes.slice(i, i + chunkSize);
689
+ binary += String.fromCharCode(...chunk);
690
+ }
691
+ const base64 = btoa(binary);
692
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
693
+ }
694
+ /**
695
+ * Base64URL encode from Uint8Array (for binary data like signatures)
696
+ */
697
+ base64UrlEncodeBytes(bytes) {
698
+ let binary = "";
699
+ const chunkSize = 8192;
700
+ for (let i = 0; i < bytes.length; i += chunkSize) {
701
+ const chunk = bytes.slice(i, i + chunkSize);
702
+ binary += String.fromCharCode(...chunk);
703
+ }
704
+ const base64 = btoa(binary);
705
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
706
+ }
707
+ /**
708
+ * Import the private key into Web Crypto API format (cached after first import)
709
+ */
710
+ async getCryptoKey() {
711
+ if (this.cryptoKey) {
712
+ return this.cryptoKey;
713
+ }
714
+ this.cryptoKey = await crypto.subtle.importKey(
715
+ "pkcs8",
716
+ this.privateKeyToArrayBuffer(this.privateKey),
717
+ {
718
+ name: "RSASSA-PKCS1-v1_5",
719
+ hash: "SHA-256"
720
+ },
721
+ false,
722
+ ["sign"]
723
+ );
724
+ return this.cryptoKey;
725
+ }
726
+ /**
727
+ * Convert PEM private key to ArrayBuffer for Web Crypto API
728
+ */
729
+ privateKeyToArrayBuffer(pem) {
730
+ const pemHeader = "-----BEGIN PRIVATE KEY-----";
731
+ const pemFooter = "-----END PRIVATE KEY-----";
732
+ const pemContents = pem.replace(pemHeader, "").replace(pemFooter, "").replace(/\s/g, "");
733
+ const binaryString = atob(pemContents);
734
+ const bytes = new Uint8Array(binaryString.length);
735
+ for (let i = 0; i < binaryString.length; i++) {
736
+ bytes[i] = binaryString.charCodeAt(i);
737
+ }
738
+ return bytes.buffer;
739
+ }
680
740
  async generateJWT(tenantId, userId, scopes) {
681
741
  const header = {
682
742
  alg: "RS256",
@@ -688,19 +748,20 @@ var ApiClient = class {
688
748
  };
689
749
  if (userId) payload.userId = userId;
690
750
  if (scopes?.length) payload.scopes = scopes;
691
- const encodeJson = (obj) => {
692
- const json = JSON.stringify(obj);
693
- const base64 = Buffer.from(json).toString("base64");
694
- return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
695
- };
696
- const encodedHeader = encodeJson(header);
697
- const encodedPayload = encodeJson(payload);
751
+ const encodedHeader = this.base64UrlEncode(JSON.stringify(header));
752
+ const encodedPayload = this.base64UrlEncode(JSON.stringify(payload));
698
753
  const data = `${encodedHeader}.${encodedPayload}`;
699
- const signer = (0, import_node_crypto.createSign)("RSA-SHA256");
700
- signer.update(data);
701
- signer.end();
702
- const signature = signer.sign(this.privateKey);
703
- const encodedSignature = signature.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
754
+ const key = await this.getCryptoKey();
755
+ const dataBytes = new TextEncoder().encode(data);
756
+ const signature = await crypto.subtle.sign(
757
+ {
758
+ name: "RSASSA-PKCS1-v1_5"
759
+ },
760
+ key,
761
+ dataBytes
762
+ );
763
+ const signatureBytes = new Uint8Array(signature);
764
+ const encodedSignature = this.base64UrlEncodeBytes(signatureBytes);
704
765
  return `${data}.${encodedSignature}`;
705
766
  }
706
767
  };
@@ -1025,7 +1086,6 @@ function resolveTenantId2(client, tenantId) {
1025
1086
  }
1026
1087
 
1027
1088
  // src/routes/ingest.ts
1028
- var import_node_crypto2 = require("crypto");
1029
1089
  async function syncSchema(client, queryEngine, databaseName, options, signal) {
1030
1090
  const tenantId = resolveTenantId3(client, options.tenantId);
1031
1091
  const adapter = queryEngine.getDatabase(databaseName);
@@ -1037,7 +1097,7 @@ async function syncSchema(client, queryEngine, databaseName, options, signal) {
1037
1097
  if (options.forceReindex) {
1038
1098
  payload.force_reindex = true;
1039
1099
  }
1040
- const sessionId = (0, import_node_crypto2.randomUUID)();
1100
+ const sessionId = crypto.randomUUID();
1041
1101
  const response = await client.post(
1042
1102
  "/ingest",
1043
1103
  payload,
@@ -1086,10 +1146,9 @@ function buildSchemaRequest(databaseName, adapter, introspection, metadata) {
1086
1146
  }
1087
1147
 
1088
1148
  // src/routes/query.ts
1089
- var import_node_crypto3 = require("crypto");
1090
1149
  async function ask(client, queryEngine, question, options, signal) {
1091
1150
  const tenantId = resolveTenantId4(client, options.tenantId);
1092
- const sessionId = (0, import_node_crypto3.randomUUID)();
1151
+ const sessionId = crypto.randomUUID();
1093
1152
  const maxRetry = options.maxRetry ?? 0;
1094
1153
  let attempt = 0;
1095
1154
  let lastError = options.lastError;