@querypanel/node-sdk 1.3.3 → 1.3.5
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 +47 -19
- package/dist/index.cjs +50 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -4
- package/dist/index.d.ts +23 -4
- package/dist/index.js +50 -6
- package/dist/index.js.map +1 -1
- package/package.json +16 -3
package/README.md
CHANGED
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
# QueryPanel Node SDK
|
|
2
2
|
|
|
3
|
+
`@querypanel/node-sdk` is the headless, server-side SDK for [QueryPanel](https://querypanel.io) embedded analytics. Use it in your Node.js (18+), Bun, or Deno backend to mint RS256 tenant JWTs, sync your Postgres, Amazon Redshift, ClickHouse, or BigQuery schema, and turn natural-language questions into SQL (natural-language-to-SQL / text-to-SQL) for multi-tenant SaaS products. The generated SQL runs against your database through a client function you provide, so database credentials and query results stay on your servers. For chart generation the SDK sends column names and value types, not row values (see `anonymizeResults`).
|
|
4
|
+
|
|
5
|
+
- Docs: [querypanel.io/docs/node-sdk](https://querypanel.io/docs/node-sdk)
|
|
6
|
+
- Tenant isolation: [querypanel.io/docs/tenant-isolation](https://querypanel.io/docs/tenant-isolation)
|
|
7
|
+
- JWT setup: [querypanel.io/docs/jwt](https://querypanel.io/docs/jwt)
|
|
8
|
+
|
|
9
|
+
## When to use the Node SDK vs the React SDK
|
|
10
|
+
|
|
11
|
+
- **[React SDK](https://www.npmjs.com/package/@querypanel/react-sdk) (headful, start here for React apps):** `@querypanel/react-sdk` renders customer-facing dashboards with an AI assistant and per-tenant customization directly in your React tree, not an iframe ([why that matters](https://querypanel.io/blog/iframe-vs-native-react-embedded-analytics-2026)). It still needs this package on your server to mint the tenant JWT (`createJwt`).
|
|
12
|
+
- **Node SDK (this package, headless):** use it alone when you build your own UI and want `ask()` to return SQL, rows, and a chart spec from your API routes.
|
|
13
|
+
|
|
14
|
+
How it compares to other embedded analytics tools: [querypanel.io/compare](https://querypanel.io/compare).
|
|
15
|
+
|
|
16
|
+
## Overview
|
|
17
|
+
|
|
3
18
|
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
19
|
|
|
5
20
|
## Installation
|
|
6
21
|
|
|
7
22
|
```bash
|
|
8
|
-
bun add @querypanel/sdk
|
|
23
|
+
bun add @querypanel/node-sdk
|
|
9
24
|
# or
|
|
10
|
-
npm install @querypanel/sdk
|
|
25
|
+
npm install @querypanel/node-sdk
|
|
11
26
|
```
|
|
12
27
|
|
|
13
28
|
> **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.
|
|
@@ -15,8 +30,9 @@ npm install @querypanel/sdk
|
|
|
15
30
|
## Quickstart
|
|
16
31
|
|
|
17
32
|
```ts
|
|
18
|
-
import { QueryPanelSdkAPI } from "@querypanel/sdk";
|
|
33
|
+
import { QueryPanelSdkAPI } from "@querypanel/node-sdk";
|
|
19
34
|
import { Pool } from "pg";
|
|
35
|
+
import { createClient } from "@clickhouse/client";
|
|
20
36
|
|
|
21
37
|
const qp = new QueryPanelSdkAPI(
|
|
22
38
|
process.env.QUERYPANEL_URL!,
|
|
@@ -42,21 +58,31 @@ const createPostgresClient = () => async (sql: string, params?: unknown[]) => {
|
|
|
42
58
|
}
|
|
43
59
|
};
|
|
44
60
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"pg_demo",
|
|
49
|
-
|
|
61
|
+
// Attach PostgreSQL database using the SDK's PostgresAdapter
|
|
62
|
+
// The SDK will automatically handle tenant isolation when tenantFieldName is provided
|
|
63
|
+
qp.attachPostgres(
|
|
64
|
+
"pg_demo", // a unique identifier for QueryPanel
|
|
65
|
+
createPostgresClient(),
|
|
50
66
|
{
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
67
|
+
database: "pg_demo", // database name
|
|
68
|
+
description: "PostgreSQL demo database", // some description that QueryPanel can use
|
|
69
|
+
tenantFieldName: "tenant_id", // SDK will automatically filter by tenant_id
|
|
70
|
+
enforceTenantIsolation: true, // Ensures all queries include tenant_id filter
|
|
71
|
+
allowedTables: ["orders"], // Only sync 'orders' table - 'users' will be excluded
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// Amazon Redshift speaks the Postgres wire protocol, so a pg-based client works.
|
|
76
|
+
// attachRedshift introspects via Redshift's svv_* views and generates Redshift SQL.
|
|
77
|
+
qp.attachRedshift("warehouse", createPostgresClient(), {
|
|
78
|
+
database: "dev",
|
|
79
|
+
tenantFieldName: "tenant_id",
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const clickhouse = createClient({ url: process.env.CLICKHOUSE_URL });
|
|
57
83
|
|
|
58
84
|
qp.attachClickhouse(
|
|
59
|
-
"analytics", //
|
|
85
|
+
"analytics", // unique identifier for QueryPanel
|
|
60
86
|
(params) => clickhouse.query(params),
|
|
61
87
|
{
|
|
62
88
|
database: "analytics", // database name
|
|
@@ -143,7 +169,7 @@ const response = await qp.ask("Show order volume by week", {
|
|
|
143
169
|
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`).
|
|
144
170
|
|
|
145
171
|
```ts
|
|
146
|
-
import { QueryPanelSdkAPI, ALL_VIZ_CHART_TYPES, type ChartType } from "@querypanel/sdk";
|
|
172
|
+
import { QueryPanelSdkAPI, ALL_VIZ_CHART_TYPES, type ChartType } from "@querypanel/node-sdk";
|
|
147
173
|
|
|
148
174
|
const allowed: ChartType[] = ["line", "bar", "column", "pie"];
|
|
149
175
|
|
|
@@ -512,7 +538,7 @@ The API accepts up to **100** UUIDs per request. Examples: `?ids=a&ids=b` or `?i
|
|
|
512
538
|
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:
|
|
513
539
|
|
|
514
540
|
```ts
|
|
515
|
-
import { QueryPanelSdkAPI } from "https://esm.sh/@querypanel/sdk";
|
|
541
|
+
import { QueryPanelSdkAPI } from "https://esm.sh/@querypanel/node-sdk";
|
|
516
542
|
|
|
517
543
|
const qp = new QueryPanelSdkAPI(
|
|
518
544
|
Deno.env.get("QUERYPANEL_URL")!,
|
|
@@ -526,10 +552,12 @@ const response = await qp.ask("Show top products", {
|
|
|
526
552
|
});
|
|
527
553
|
```
|
|
528
554
|
|
|
555
|
+
BigQuery is attached the same way with `attachBigQuery(name, clientFn, options)`. Any other engine can be plugged in with `attachDatabase(name, adapter)` and your own `DatabaseAdapter` implementation.
|
|
556
|
+
|
|
529
557
|
## Building locally
|
|
530
558
|
|
|
531
559
|
```bash
|
|
532
|
-
cd node-sdk
|
|
560
|
+
cd admin/packages/node-sdk
|
|
533
561
|
bun install
|
|
534
562
|
bun run build
|
|
535
563
|
```
|
|
@@ -571,4 +599,4 @@ Without `maxRetry`, execution errors throw immediately (default behavior).
|
|
|
571
599
|
|
|
572
600
|
## Need more?
|
|
573
601
|
|
|
574
|
-
Open an issue or extend `
|
|
602
|
+
Open an issue or extend the SDK: the public class is in `src/index.ts` and each API route has a module under `src/routes/`. Pull requests are welcome for additional adapters, richer param coercion, or convenience helpers around charts/annotations.
|
package/dist/index.cjs
CHANGED
|
@@ -548,7 +548,8 @@ var PostgresAdapter = class {
|
|
|
548
548
|
this.clientFn = clientFn;
|
|
549
549
|
this.databaseName = options.database ?? "postgres";
|
|
550
550
|
this.defaultSchema = options.defaultSchema ?? "public";
|
|
551
|
-
this.
|
|
551
|
+
this.dialect = options.dialect ?? "postgres";
|
|
552
|
+
this.kind = options.kind ?? this.dialect;
|
|
552
553
|
if (options.allowedTables) {
|
|
553
554
|
this.allowedTables = normalizeTableFilter3(
|
|
554
555
|
options.allowedTables,
|
|
@@ -560,6 +561,7 @@ var PostgresAdapter = class {
|
|
|
560
561
|
databaseName;
|
|
561
562
|
defaultSchema;
|
|
562
563
|
kind;
|
|
564
|
+
dialect;
|
|
563
565
|
allowedTables;
|
|
564
566
|
async execute(sql, params) {
|
|
565
567
|
if (this.allowedTables) {
|
|
@@ -639,7 +641,7 @@ var PostgresAdapter = class {
|
|
|
639
641
|
await this.clientFn(`EXPLAIN ${sql}`, paramArray);
|
|
640
642
|
}
|
|
641
643
|
getDialect() {
|
|
642
|
-
return
|
|
644
|
+
return this.dialect;
|
|
643
645
|
}
|
|
644
646
|
/**
|
|
645
647
|
* Simplified introspection: only collect table/column metadata for IngestRequest
|
|
@@ -648,12 +650,13 @@ var PostgresAdapter = class {
|
|
|
648
650
|
async introspect(options) {
|
|
649
651
|
const tablesToIntrospect = options?.tables ? normalizeTableFilter3(options.tables, this.defaultSchema) : this.allowedTables;
|
|
650
652
|
const normalizedTables = tablesToIntrospect ?? [];
|
|
653
|
+
const isRedshift = this.dialect === "redshift";
|
|
651
654
|
const tablesResult = await this.clientFn(
|
|
652
|
-
buildTablesQuery2(normalizedTables)
|
|
655
|
+
isRedshift ? buildRedshiftTablesQuery(normalizedTables) : buildTablesQuery2(normalizedTables)
|
|
653
656
|
);
|
|
654
657
|
const tableRows = tablesResult.rows;
|
|
655
658
|
const columnsResult = await this.clientFn(
|
|
656
|
-
buildColumnsQuery2(normalizedTables)
|
|
659
|
+
isRedshift ? buildRedshiftColumnsQuery(normalizedTables) : buildColumnsQuery2(normalizedTables)
|
|
657
660
|
);
|
|
658
661
|
const columnRows = columnsResult.rows;
|
|
659
662
|
const tablesByKey = /* @__PURE__ */ new Map();
|
|
@@ -767,10 +770,11 @@ function buildColumnsQuery2(tables) {
|
|
|
767
770
|
AND kcu.column_name = cols.column_name
|
|
768
771
|
) AS is_primary_key
|
|
769
772
|
FROM information_schema.columns cols
|
|
773
|
+
LEFT JOIN pg_catalog.pg_namespace n ON n.nspname = cols.table_schema
|
|
770
774
|
LEFT JOIN pg_catalog.pg_class c
|
|
771
775
|
ON c.relname = cols.table_name
|
|
776
|
+
AND c.relnamespace = n.oid
|
|
772
777
|
AND c.relkind IN ('r', 'v', 'm')
|
|
773
|
-
LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
774
778
|
LEFT JOIN pg_catalog.pg_attribute attr
|
|
775
779
|
ON attr.attrelid = c.oid
|
|
776
780
|
AND attr.attname = cols.column_name
|
|
@@ -780,6 +784,38 @@ function buildColumnsQuery2(tables) {
|
|
|
780
784
|
${filter}
|
|
781
785
|
ORDER BY cols.table_schema, cols.table_name, cols.ordinal_position;`;
|
|
782
786
|
}
|
|
787
|
+
var REDSHIFT_EXCLUDED_SCHEMAS = "('pg_catalog', 'information_schema', 'pg_internal', 'pg_automv')";
|
|
788
|
+
function buildRedshiftTablesQuery(tables) {
|
|
789
|
+
const filter = buildFilterClause(tables, "table_schema", "table_name");
|
|
790
|
+
return `SELECT
|
|
791
|
+
table_name,
|
|
792
|
+
table_schema AS schema_name,
|
|
793
|
+
table_type,
|
|
794
|
+
remarks AS comment
|
|
795
|
+
FROM svv_tables
|
|
796
|
+
WHERE table_catalog = current_database()
|
|
797
|
+
AND table_schema NOT IN ${REDSHIFT_EXCLUDED_SCHEMAS}
|
|
798
|
+
AND table_schema NOT LIKE 'pg_temp%'
|
|
799
|
+
${filter}
|
|
800
|
+
ORDER BY table_schema, table_name;`;
|
|
801
|
+
}
|
|
802
|
+
function buildRedshiftColumnsQuery(tables) {
|
|
803
|
+
const filter = buildFilterClause(tables, "table_schema", "table_name");
|
|
804
|
+
return `SELECT
|
|
805
|
+
table_name,
|
|
806
|
+
table_schema,
|
|
807
|
+
column_name,
|
|
808
|
+
data_type,
|
|
809
|
+
NULL AS udt_name,
|
|
810
|
+
remarks AS description,
|
|
811
|
+
FALSE AS is_primary_key
|
|
812
|
+
FROM svv_columns
|
|
813
|
+
WHERE table_catalog = current_database()
|
|
814
|
+
AND table_schema NOT IN ${REDSHIFT_EXCLUDED_SCHEMAS}
|
|
815
|
+
AND table_schema NOT LIKE 'pg_temp%'
|
|
816
|
+
${filter}
|
|
817
|
+
ORDER BY table_schema, table_name, ordinal_position;`;
|
|
818
|
+
}
|
|
783
819
|
function buildFilterClause(tables, schemaExpr, tableExpr) {
|
|
784
820
|
if (!tables.length) return "";
|
|
785
821
|
const clauses = tables.map(({ schema, table }) => {
|
|
@@ -2913,7 +2949,7 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
|
|
|
2913
2949
|
const adapter = new PostgresAdapter(clientFn, options);
|
|
2914
2950
|
const metadata = {
|
|
2915
2951
|
name,
|
|
2916
|
-
dialect:
|
|
2952
|
+
dialect: adapter.getDialect(),
|
|
2917
2953
|
description: options?.description,
|
|
2918
2954
|
tags: options?.tags,
|
|
2919
2955
|
tenantFieldName: options?.tenantFieldName,
|
|
@@ -2925,6 +2961,14 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
|
|
|
2925
2961
|
};
|
|
2926
2962
|
this.queryEngine.attachDatabase(name, adapter, metadata);
|
|
2927
2963
|
}
|
|
2964
|
+
/**
|
|
2965
|
+
* Attach an Amazon Redshift database. Redshift speaks the Postgres wire
|
|
2966
|
+
* protocol, so `clientFn` can wrap a `pg` client; introspection uses
|
|
2967
|
+
* Redshift's svv_* catalog views and SQL is generated for Redshift.
|
|
2968
|
+
*/
|
|
2969
|
+
attachRedshift(name, clientFn, options) {
|
|
2970
|
+
this.attachPostgres(name, clientFn, { ...options, dialect: "redshift" });
|
|
2971
|
+
}
|
|
2928
2972
|
attachBigQuery(name, clientFn, options) {
|
|
2929
2973
|
const adapter = new BigQueryAdapter(clientFn, options);
|
|
2930
2974
|
const metadata = {
|