@querypanel/node-sdk 1.3.7 → 1.3.9

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
@@ -578,6 +578,26 @@ const response = await qp.ask("Show top products", {
578
578
 
579
579
  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.
580
580
 
581
+ ## One schema per tenant
582
+
583
+ If each tenant has its own schema with the same tables (for example `group_4048_indexed`, `group_4049_indexed`), attach with `tenantSchema` instead of `tenantFieldName`. Sync the schema of one tenant; the SQL QueryPanel generates names that schema, and the SDK rewrites it to the caller's schema (`{tenant}` replaced by `tenantId`) before running it:
584
+
585
+ ```ts
586
+ qp.attachRedshift("analytics", createClient, {
587
+ tenantSchema: {
588
+ template: "group_{tenant}_indexed",
589
+ sharedSchemas: ["lookups"], // optional: schemas every tenant may also read
590
+ },
591
+ });
592
+
593
+ await qp.ask("How many members joined last month?", { tenantId: "4048" });
594
+ // runs against group_4048_indexed
595
+ ```
596
+
597
+ Queries that read anything outside the tenant's schema, a shared schema or a CTE are refused, so a tenant cannot reach another tenant's schema. `allowedTables` written for the template schema (`group_4048_indexed.users`) applies to every tenant's schema. `tenantId` must be 1–64 letters, digits or `_`. Postgres and Redshift only. Adding a tenant needs no QueryPanel call: create its schema, then sign JWTs with its `tenantId`.
598
+
599
+ For embedded dashboards, set the same template as **Tenant schema template** on the datasource in QueryPanel and mint the usual JWT; `tenantId` is only the part that fills `{tenant}` (`"4048"`, not `"group_4048_indexed"`). See [Tenant isolation](https://querypanel.io/docs/tenant-isolation#schema-per-tenant).
600
+
581
601
  ## Building locally
582
602
 
583
603
  ```bash
@@ -621,6 +641,20 @@ The SDK will:
621
641
 
622
642
  Without `maxRetry`, execution errors throw immediately (default behavior).
623
643
 
644
+ #### Retry when the query returns nothing
645
+
646
+ A query can run fine and still return nothing because a filter value does not match how the data stores it (`state = 'approved'` when the column holds `APPROVE`). With `retryOnEmptyResult`, the SDK treats an empty result, or a single row of only zeros/NULLs such as `COUNT(*) = 0`, like an execution error and asks for a repaired query within the same `maxRetry` budget:
647
+
648
+ ```ts
649
+ const response = await qp.ask("How many transactions were declined?", {
650
+ tenantId: "tenant_123",
651
+ maxRetry: 1,
652
+ retryOnEmptyResult: true,
653
+ });
654
+ ```
655
+
656
+ Only the fact that the result was empty and the previous SQL are sent back; no rows or values leave your backend. If the last attempt is still empty, it is returned as is (no error). Off by default; each retry is one more generation round, so expect a few extra seconds on those questions.
657
+
624
658
  ## Need more?
625
659
 
626
660
  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
@@ -39,10 +39,14 @@ __export(index_exports, {
39
39
  QueryPanelSdkAPI: () => QueryPanelSdkAPI,
40
40
  QueryPipelineError: () => QueryPipelineError,
41
41
  SchemaTooLargeError: () => SchemaTooLargeError,
42
+ TenantSchemaError: () => TenantSchemaError,
42
43
  anonymizeResults: () => anonymizeResults,
43
44
  mapGeneratedParams: () => mapGeneratedParams,
44
45
  normalizeSqlParamsInput: () => normalizeSqlParamsInput,
45
- resolveQueryPipeline: () => resolveQueryPipeline
46
+ parseTenantSchemaTemplate: () => parseTenantSchemaTemplate,
47
+ resolveQueryPipeline: () => resolveQueryPipeline,
48
+ resolveTenantSchema: () => resolveTenantSchema,
49
+ rewriteTenantSchema: () => rewriteTenantSchema
46
50
  });
47
51
  module.exports = __toCommonJS(index_exports);
48
52
 
@@ -629,6 +633,248 @@ function toNumber(value) {
629
633
  return Number.isNaN(parsed) ? void 0 : parsed;
630
634
  }
631
635
 
636
+ // src/core/sql-relations.ts
637
+ function tokenize(sql) {
638
+ const tokens = [];
639
+ let i = 0;
640
+ while (i < sql.length) {
641
+ const ch = sql[i];
642
+ const next = sql[i + 1];
643
+ if (/\s/.test(ch)) {
644
+ i++;
645
+ } else if (ch === "-" && next === "-") {
646
+ while (i < sql.length && sql[i] !== "\n") i++;
647
+ } else if (ch === "/" && next === "*") {
648
+ const end = sql.indexOf("*/", i + 2);
649
+ i = end < 0 ? sql.length : end + 2;
650
+ } else if (ch === "'") {
651
+ i++;
652
+ while (i < sql.length) {
653
+ if (sql[i] === "\\") i += 2;
654
+ else if (sql[i] === "'" && sql[i + 1] === "'") i += 2;
655
+ else if (sql[i] === "'") break;
656
+ else i++;
657
+ }
658
+ i++;
659
+ } else if (ch === "$" && /[A-Za-z_$]/.test(next ?? "")) {
660
+ const tag = /^\$[A-Za-z_]*\$/.exec(sql.slice(i))?.[0];
661
+ if (tag) {
662
+ const end = sql.indexOf(tag, i + tag.length);
663
+ i = end < 0 ? sql.length : end + tag.length;
664
+ } else {
665
+ i++;
666
+ }
667
+ } else if (ch === '"') {
668
+ const start = i;
669
+ let text = "";
670
+ i++;
671
+ while (i < sql.length) {
672
+ if (sql[i] === '"' && sql[i + 1] === '"') {
673
+ text += '"';
674
+ i += 2;
675
+ } else if (sql[i] === '"') break;
676
+ else text += sql[i++];
677
+ }
678
+ i++;
679
+ tokens.push({ kind: "word", text, quoted: true, start, end: i });
680
+ } else if (/[A-Za-z_]/.test(ch)) {
681
+ const start = i;
682
+ while (i < sql.length && /[A-Za-z0-9_$]/.test(sql[i])) i++;
683
+ tokens.push({ kind: "word", text: sql.slice(start, i), quoted: false, start, end: i });
684
+ } else if (/[0-9]/.test(ch)) {
685
+ while (i < sql.length && /[0-9.eE]/.test(sql[i])) i++;
686
+ } else {
687
+ tokens.push({ kind: "punct", text: ch, start: i, end: i + 1 });
688
+ i++;
689
+ }
690
+ }
691
+ return tokens;
692
+ }
693
+ var identifier = (t) => t?.kind === "word" ? t.quoted ? t.text : t.text.toLowerCase() : void 0;
694
+ var isKeyword = (t, word) => t?.kind === "word" && !t.quoted && t.text.toLowerCase() === word;
695
+ var isPunct = (t, p) => t?.kind === "punct" && t.text === p;
696
+ var CLAUSE_WORDS = /* @__PURE__ */ new Set([
697
+ "where",
698
+ "group",
699
+ "order",
700
+ "limit",
701
+ "having",
702
+ "union",
703
+ "intersect",
704
+ "except",
705
+ "join",
706
+ "inner",
707
+ "left",
708
+ "right",
709
+ "full",
710
+ "cross",
711
+ "natural",
712
+ "on",
713
+ "using",
714
+ "window",
715
+ "qualify",
716
+ "offset",
717
+ "fetch",
718
+ "for",
719
+ "lateral",
720
+ "as"
721
+ ]);
722
+ var FROM_CLAUSE_END = /* @__PURE__ */ new Set([
723
+ "where",
724
+ "group",
725
+ "having",
726
+ "order",
727
+ "limit",
728
+ "offset",
729
+ "fetch",
730
+ "union",
731
+ "intersect",
732
+ "except",
733
+ "window",
734
+ "qualify",
735
+ "for",
736
+ "returning"
737
+ ]);
738
+ function cteNames(tokens) {
739
+ const names = /* @__PURE__ */ new Set();
740
+ for (let k = 0; k + 2 < tokens.length; k++) {
741
+ const value = identifier(tokens[k]);
742
+ if (!value) continue;
743
+ if (isKeyword(tokens[k + 1], "as") && isPunct(tokens[k + 2], "(")) names.add(value);
744
+ if (isPunct(tokens[k + 1], "(")) {
745
+ let j = k + 2;
746
+ while (j < tokens.length && !isPunct(tokens[j], ")")) j++;
747
+ if (isKeyword(tokens[j + 1], "as") && isPunct(tokens[j + 2], "(")) names.add(value);
748
+ }
749
+ }
750
+ return names;
751
+ }
752
+ function findRelations(sql) {
753
+ const tokens = tokenize(sql);
754
+ const ctes = cteNames(tokens);
755
+ const relations = [];
756
+ const readItem = (start) => {
757
+ let k = start;
758
+ if (isKeyword(tokens[k], "lateral")) k++;
759
+ const first = identifier(tokens[k]);
760
+ if (!first) return k;
761
+ const parts = [first];
762
+ k++;
763
+ while (isPunct(tokens[k], ".") && identifier(tokens[k + 1])) {
764
+ parts.push(identifier(tokens[k + 1]));
765
+ k += 2;
766
+ }
767
+ if (isPunct(tokens[k], "(")) return k;
768
+ if (!(parts.length === 1 && ctes.has(first))) relations.push({ parts });
769
+ if (isKeyword(tokens[k], "as")) k++;
770
+ const alias = tokens[k];
771
+ if (alias?.kind === "word" && (alias.quoted || !CLAUSE_WORDS.has(alias.text.toLowerCase()))) k++;
772
+ return k;
773
+ };
774
+ const selectAtDepth = [false];
775
+ const inFromAtDepth = [false];
776
+ const top = () => selectAtDepth.length - 1;
777
+ for (let k = 0; k < tokens.length; k++) {
778
+ const token = tokens[k];
779
+ if (isPunct(token, "(")) {
780
+ selectAtDepth.push(false);
781
+ inFromAtDepth.push(false);
782
+ } else if (isPunct(token, ")")) {
783
+ if (selectAtDepth.length > 1) {
784
+ selectAtDepth.pop();
785
+ inFromAtDepth.pop();
786
+ }
787
+ } else if (isKeyword(token, "select")) {
788
+ selectAtDepth[top()] = true;
789
+ inFromAtDepth[top()] = false;
790
+ } else if (isKeyword(token, "join")) {
791
+ k = readItem(k + 1) - 1;
792
+ } else if (isKeyword(token, "from") && selectAtDepth[top()] && !isKeyword(tokens[k - 1], "distinct")) {
793
+ inFromAtDepth[top()] = true;
794
+ k = readItem(k + 1) - 1;
795
+ } else if (isPunct(token, ",") && inFromAtDepth[top()]) {
796
+ k = readItem(k + 1) - 1;
797
+ } else if (token?.kind === "word" && !token.quoted && FROM_CLAUSE_END.has(token.text.toLowerCase())) {
798
+ inFromAtDepth[top()] = false;
799
+ }
800
+ }
801
+ return relations;
802
+ }
803
+
804
+ // src/core/tenant-schema.ts
805
+ var TenantSchemaError = class extends Error {
806
+ constructor(message) {
807
+ super(message);
808
+ this.name = "TenantSchemaError";
809
+ }
810
+ };
811
+ var PLACEHOLDER = "{tenant}";
812
+ var TEMPLATE_PART = /^[a-z0-9_]*$/;
813
+ var TENANT_ID = /^[A-Za-z0-9_]{1,64}$/;
814
+ function parseTenantSchemaTemplate(template) {
815
+ const value = template.trim().toLowerCase();
816
+ const index = value.indexOf(PLACEHOLDER);
817
+ if (index < 0 || value.indexOf(PLACEHOLDER, index + 1) >= 0) {
818
+ throw new TenantSchemaError(
819
+ `Tenant schema template must contain ${PLACEHOLDER} exactly once`
820
+ );
821
+ }
822
+ const prefix = value.slice(0, index);
823
+ const suffix = value.slice(index + PLACEHOLDER.length);
824
+ if (!TEMPLATE_PART.test(prefix) || !TEMPLATE_PART.test(suffix) || !(prefix || suffix)) {
825
+ throw new TenantSchemaError(
826
+ "Tenant schema template may only use a-z, 0-9 and _ around {tenant}"
827
+ );
828
+ }
829
+ return { prefix, suffix };
830
+ }
831
+ function matchesTemplate({ prefix, suffix }, schema) {
832
+ const value = schema.toLowerCase();
833
+ return value.length > prefix.length + suffix.length && value.startsWith(prefix) && value.endsWith(suffix) && /^[a-z0-9_]+$/.test(value.slice(prefix.length, value.length - suffix.length));
834
+ }
835
+ function resolveTenantSchema(template, tenantId) {
836
+ const { prefix, suffix } = parseTenantSchemaTemplate(template);
837
+ const id = tenantId.trim();
838
+ if (!TENANT_ID.test(id)) {
839
+ throw new TenantSchemaError(
840
+ "tenantId must be 1-64 characters of letters, digits or _ for schema-per-tenant isolation"
841
+ );
842
+ }
843
+ return `${prefix}${id.toLowerCase()}${suffix}`;
844
+ }
845
+ function rewriteTenantSchema(sql, template, tenantId) {
846
+ const parsed = parseTenantSchemaTemplate(template);
847
+ const tenantSchema = resolveTenantSchema(template, tenantId);
848
+ const tokens = tokenize(sql);
849
+ let rewritten = "";
850
+ let cursor = 0;
851
+ for (let k = 0; k < tokens.length; k++) {
852
+ const token = tokens[k];
853
+ const value = identifier(token);
854
+ if (!token || !value || !isPunct(tokens[k + 1], ".")) continue;
855
+ if (!matchesTemplate(parsed, value) || value.toLowerCase() === tenantSchema) continue;
856
+ const replacement = token.kind === "word" && token.quoted ? `"${tenantSchema}"` : tenantSchema;
857
+ rewritten += sql.slice(cursor, token.start) + replacement;
858
+ cursor = token.end;
859
+ }
860
+ rewritten += sql.slice(cursor);
861
+ const pattern = new RegExp(`${escapeRegex(parsed.prefix)}[a-z0-9_]+${escapeRegex(parsed.suffix)}`, "gi");
862
+ for (const match of rewritten.matchAll(pattern)) {
863
+ if (match[0].toLowerCase() !== tenantSchema) {
864
+ throw new TenantSchemaError("Query references another tenant's schema");
865
+ }
866
+ }
867
+ return rewritten;
868
+ }
869
+ var TENANT_SCHEMA_KEY = PLACEHOLDER;
870
+ function tenantSchemaKey(template) {
871
+ const parsed = parseTenantSchemaTemplate(template);
872
+ return (schema) => matchesTemplate(parsed, schema) ? TENANT_SCHEMA_KEY : schema.toLowerCase();
873
+ }
874
+ function escapeRegex(value) {
875
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
876
+ }
877
+
632
878
  // src/adapters/postgres.ts
633
879
  var WILDCARD = "*";
634
880
  function maxPostgresPlaceholderIndex(sql) {
@@ -675,6 +921,11 @@ var PostgresAdapter = class {
675
921
  this.defaultSchema
676
922
  );
677
923
  }
924
+ this.allowsRelation = buildRelationAllowlist(
925
+ this.allowedTables,
926
+ options.tenantSchema,
927
+ this.defaultSchema
928
+ );
678
929
  }
679
930
  clientFn;
680
931
  databaseName;
@@ -682,9 +933,11 @@ var PostgresAdapter = class {
682
933
  kind;
683
934
  dialect;
684
935
  allowedTables;
936
+ /** Undefined when queries are unrestricted. */
937
+ allowsRelation;
685
938
  async execute(sql, params) {
686
- if (this.allowedTables) {
687
- this.validateQueryTables(sql);
939
+ if (this.allowsRelation) {
940
+ this.validateQueryTables(sql, this.allowsRelation);
688
941
  }
689
942
  const boundParams = filterParamsForPostgresSql(sql, params);
690
943
  let paramArray;
@@ -695,29 +948,11 @@ var PostgresAdapter = class {
695
948
  const fields = result.fields.map((f) => f.name);
696
949
  return { fields, rows: result.rows };
697
950
  }
698
- validateQueryTables(sql) {
699
- if (!this.allowedTables || this.allowedTables.length === 0) {
700
- return;
701
- }
702
- const allowedSet = new Set(
703
- this.allowedTables.map((t) => tableKey2(t.schema, t.table))
704
- );
705
- const allowedSchemas = new Set(
706
- this.allowedTables.filter((t) => t.table === WILDCARD).map((t) => t.schema)
707
- );
708
- const neutralizedSql = sql.replace(/EXTRACT\s*\([^)]*FROM\s+[^)]+\)/gi, "EXTRACT(/*neutralized*/)").replace(/SUBSTRING\s*\([^)]*FROM\s+[^)]+\)/gi, "SUBSTRING(/*neutralized*/)").replace(/TRIM\s*\([^)]*FROM\s+[^)]+\)/gi, "TRIM(/*neutralized*/)").replace(/POSITION\s*\([^)]*FROM\s+[^)]+\)/gi, "POSITION(/*neutralized*/)");
709
- const tablePattern = /(?:FROM|JOIN)\s+(?:ONLY\s+)?(?:([a-zA-Z_][a-zA-Z0-9_]*)\.)?(["']?[a-zA-Z_][a-zA-Z0-9_]*["']?)/gi;
710
- const matches = neutralizedSql.matchAll(tablePattern);
711
- for (const match of matches) {
712
- const schema = match[1] ?? this.defaultSchema;
713
- const table = match[2]?.replace(/['"]/g, "");
714
- if (table) {
715
- const key = tableKey2(schema, table);
716
- if (!allowedSet.has(key) && !allowedSchemas.has(schema)) {
717
- throw new Error(
718
- `Query references table "${schema}.${table}" which is not in the allowed tables list`
719
- );
720
- }
951
+ validateQueryTables(sql, allows) {
952
+ for (const relation of findRelations(sql)) {
953
+ if (!allows(relation)) {
954
+ const name = relation.parts.length === 1 ? `${this.defaultSchema}.${relation.parts[0]}` : relation.parts.join(".");
955
+ throw new Error(`Query references table "${name}" which is not in the allowed tables list`);
721
956
  }
722
957
  }
723
958
  }
@@ -755,6 +990,9 @@ var PostgresAdapter = class {
755
990
  return positionalParams;
756
991
  }
757
992
  async validate(sql, params) {
993
+ if (this.allowsRelation) {
994
+ this.validateQueryTables(sql, this.allowsRelation);
995
+ }
758
996
  const boundParams = filterParamsForPostgresSql(sql, params);
759
997
  let paramArray;
760
998
  if (boundParams) {
@@ -1044,6 +1282,27 @@ function buildListTablesQuery(dialect, options) {
1044
1282
  LIMIT ${limit};`;
1045
1283
  return { sql, params };
1046
1284
  }
1285
+ function buildRelationAllowlist(allowedTables, tenantSchema, defaultSchema) {
1286
+ if (!allowedTables?.length && !tenantSchema) return void 0;
1287
+ const schemaKey = tenantSchema ? tenantSchemaKey(tenantSchema.template) : (schema) => schema.toLowerCase();
1288
+ const entries = [
1289
+ ...allowedTables?.length ? allowedTables : [{ schema: TENANT_SCHEMA_KEY, table: WILDCARD }],
1290
+ ...(tenantSchema?.sharedSchemas ?? []).map((schema) => ({ schema, table: WILDCARD }))
1291
+ ];
1292
+ const tables = /* @__PURE__ */ new Set();
1293
+ const wildcardSchemas = /* @__PURE__ */ new Set();
1294
+ for (const entry of entries) {
1295
+ const schema = entry.schema === TENANT_SCHEMA_KEY ? entry.schema : schemaKey(entry.schema);
1296
+ if (entry.table === WILDCARD) wildcardSchemas.add(schema);
1297
+ else tables.add(tableKey2(schema, entry.table.toLowerCase()));
1298
+ }
1299
+ return ({ parts }) => {
1300
+ if (parts.length > 2) return false;
1301
+ const table = parts[parts.length - 1].toLowerCase();
1302
+ const schema = schemaKey(parts.length === 2 ? parts[0] : defaultSchema);
1303
+ return wildcardSchemas.has(schema) || tables.has(tableKey2(schema, table));
1304
+ };
1305
+ }
1047
1306
  function tableKey2(schema, table) {
1048
1307
  return `${schema}.${table}`;
1049
1308
  }
@@ -1648,7 +1907,9 @@ var QueryEngine = class {
1648
1907
  const adapter = this.getDatabase(databaseName);
1649
1908
  const metadata = this.getDatabaseMetadata(databaseName);
1650
1909
  let finalSql = sql;
1651
- if (metadata) {
1910
+ if (metadata?.tenantSchema) {
1911
+ finalSql = rewriteTenantSchema(sql, metadata.tenantSchema.template, tenantId);
1912
+ } else if (metadata) {
1652
1913
  finalSql = this.ensureTenantIsolation(sql, params, metadata, tenantId);
1653
1914
  }
1654
1915
  await adapter.validate(finalSql, params);
@@ -2492,6 +2753,15 @@ function resolveQueryPipeline(pipeline) {
2492
2753
  }
2493
2754
  return "v2";
2494
2755
  }
2756
+ var EMPTY_RESULT_ERROR = "The query ran but returned no rows (or only zero/NULL values). A filter value probably does not match how the data stores it (case, spelling, codes such as 'APPROVE' vs 'approved'), or a filter is narrower than the question. Check each filter value against the column's documented values and fix it; keep every filter the question asks for.";
2757
+ function isEmptyLikeResult(rows) {
2758
+ if (rows.length === 0) return true;
2759
+ if (rows.length > 1) return false;
2760
+ const row = rows[0];
2761
+ if (!row || typeof row !== "object") return false;
2762
+ const values = Object.values(row);
2763
+ return values.length > 0 && values.every((v) => v === null || v === 0 || v === "0" || v === 0n);
2764
+ }
2495
2765
  async function ask(client, queryEngine, question, options, signal) {
2496
2766
  const tenantId = resolveTenantId5(client, options.tenantId);
2497
2767
  const sessionId = import_node_crypto3.default.randomUUID();
@@ -2574,6 +2844,17 @@ async function ask(client, queryEngine, question, options, signal) {
2574
2844
  tenantId
2575
2845
  );
2576
2846
  const rows = execution.rows ?? [];
2847
+ if (options.retryOnEmptyResult && attempt < maxRetry && isEmptyLikeResult(rows)) {
2848
+ attempt++;
2849
+ lastError = EMPTY_RESULT_ERROR;
2850
+ previousSql = sql;
2851
+ console.warn("Query returned no data, retrying query generation", {
2852
+ attempt,
2853
+ maxAttempts: maxRetry + 1,
2854
+ sql
2855
+ });
2856
+ continue;
2857
+ }
2577
2858
  const chartType = options.chartType ?? "vega-lite";
2578
2859
  let chart = {
2579
2860
  specType: chartType,
@@ -3377,6 +3658,9 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
3377
3658
  this.queryEngine.attachDatabase(name, adapter, metadata);
3378
3659
  }
3379
3660
  attachPostgres(name, clientFn, options) {
3661
+ if (options?.tenantSchema && options.tenantFieldName) {
3662
+ throw new Error("Use either tenantSchema or tenantFieldName for tenant isolation, not both");
3663
+ }
3380
3664
  const adapter = new PostgresAdapter(clientFn, options);
3381
3665
  const metadata = {
3382
3666
  name,
@@ -3388,7 +3672,8 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
3388
3672
  enforceTenantIsolation: resolveAttachEnforceTenantIsolation(
3389
3673
  options?.tenantFieldName,
3390
3674
  options?.enforceTenantIsolation
3391
- )
3675
+ ),
3676
+ tenantSchema: options?.tenantSchema
3392
3677
  };
3393
3678
  this.queryEngine.attachDatabase(name, adapter, metadata);
3394
3679
  }
@@ -4443,9 +4728,13 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
4443
4728
  QueryPanelSdkAPI,
4444
4729
  QueryPipelineError,
4445
4730
  SchemaTooLargeError,
4731
+ TenantSchemaError,
4446
4732
  anonymizeResults,
4447
4733
  mapGeneratedParams,
4448
4734
  normalizeSqlParamsInput,
4449
- resolveQueryPipeline
4735
+ parseTenantSchemaTemplate,
4736
+ resolveQueryPipeline,
4737
+ resolveTenantSchema,
4738
+ rewriteTenantSchema
4450
4739
  });
4451
4740
  //# sourceMappingURL=index.cjs.map