@querypanel/node-sdk 1.3.8 → 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 +20 -0
- package/dist/index.cjs +298 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +44 -3
- package/dist/index.d.ts +44 -3
- package/dist/index.js +293 -28
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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);
|
|
@@ -3397,6 +3658,9 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
|
|
|
3397
3658
|
this.queryEngine.attachDatabase(name, adapter, metadata);
|
|
3398
3659
|
}
|
|
3399
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
|
+
}
|
|
3400
3664
|
const adapter = new PostgresAdapter(clientFn, options);
|
|
3401
3665
|
const metadata = {
|
|
3402
3666
|
name,
|
|
@@ -3408,7 +3672,8 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
|
|
|
3408
3672
|
enforceTenantIsolation: resolveAttachEnforceTenantIsolation(
|
|
3409
3673
|
options?.tenantFieldName,
|
|
3410
3674
|
options?.enforceTenantIsolation
|
|
3411
|
-
)
|
|
3675
|
+
),
|
|
3676
|
+
tenantSchema: options?.tenantSchema
|
|
3412
3677
|
};
|
|
3413
3678
|
this.queryEngine.attachDatabase(name, adapter, metadata);
|
|
3414
3679
|
}
|
|
@@ -4463,9 +4728,13 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
|
|
|
4463
4728
|
QueryPanelSdkAPI,
|
|
4464
4729
|
QueryPipelineError,
|
|
4465
4730
|
SchemaTooLargeError,
|
|
4731
|
+
TenantSchemaError,
|
|
4466
4732
|
anonymizeResults,
|
|
4467
4733
|
mapGeneratedParams,
|
|
4468
4734
|
normalizeSqlParamsInput,
|
|
4469
|
-
|
|
4735
|
+
parseTenantSchemaTemplate,
|
|
4736
|
+
resolveQueryPipeline,
|
|
4737
|
+
resolveTenantSchema,
|
|
4738
|
+
rewriteTenantSchema
|
|
4470
4739
|
});
|
|
4471
4740
|
//# sourceMappingURL=index.cjs.map
|