@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 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
- // Attach PostgreSQL database using the SDK's PostgresAdapter
46
- // The SDK will automatically handle tenant isolation when tenantFieldName is provided
47
- qp.attachPostgres(
48
- "pg_demo", // a uniq identifier for QueryPanel
49
- createPostgresClientFn(),
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
- database: "pg_demo", // database name
52
- description: "PostgreSQL demo database", // some description that QueryPanel can use
53
- tenantFieldName: "tenant_id", // SDK will automatically filter by tenant_id
54
- enforceTenantIsolation: true, // Ensures all queries include tenant_id filter
55
- allowedTables: ["orders"], // Only sync 'orders' table - 'users' will be excluded
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", // uniq identifier for QueryPanel
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 `node-sdk/src/index.ts`—every route lives in one file. Pull requests are welcome for additional adapters, richer param coercion, or convenience helpers around charts/annotations.
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.kind = options.kind ?? "postgres";
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 "postgres";
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: "postgres",
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 = {