@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 +82 -2
- package/dist/index.cjs +76 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +76 -17
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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
|
|
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
|
|
692
|
-
|
|
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
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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 =
|
|
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 =
|
|
1151
|
+
const sessionId = crypto.randomUUID();
|
|
1093
1152
|
const maxRetry = options.maxRetry ?? 0;
|
|
1094
1153
|
let attempt = 0;
|
|
1095
1154
|
let lastError = options.lastError;
|