@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/dist/index.d.cts CHANGED
@@ -230,6 +230,29 @@ declare class ClickHouseAdapter implements DatabaseAdapter {
230
230
  private extractRows;
231
231
  }
232
232
 
233
+ /** Attach option for schema-per-tenant isolation. */
234
+ interface TenantSchemaConfig {
235
+ /** Schema name with one `{tenant}` placeholder, e.g. `group_{tenant}_indexed`. */
236
+ template: string;
237
+ /** Extra schemas every tenant may read (lookup tables etc.). */
238
+ sharedSchemas?: string[];
239
+ }
240
+ declare class TenantSchemaError extends Error {
241
+ constructor(message: string);
242
+ }
243
+ interface ParsedTemplate {
244
+ prefix: string;
245
+ suffix: string;
246
+ }
247
+ declare function parseTenantSchemaTemplate(template: string): ParsedTemplate;
248
+ /** The schema a tenant's queries run against. Throws for unsafe tenant ids. */
249
+ declare function resolveTenantSchema(template: string, tenantId: string): string;
250
+ /**
251
+ * Rewrite every template-schema qualifier to the tenant's schema. Fails closed when
252
+ * another tenant's schema is still named anywhere (strings and comments included).
253
+ */
254
+ declare function rewriteTenantSchema(sql: string, template: string, tenantId: string): string;
255
+
233
256
  interface PostgresQueryResult {
234
257
  rows: Array<Record<string, unknown>>;
235
258
  fields: Array<{
@@ -257,6 +280,12 @@ interface PostgresAdapterOptions {
257
280
  * to these tables only.
258
281
  */
259
282
  allowedTables?: string[];
283
+ /**
284
+ * Schema-per-tenant connection: SQL has been rewritten to the caller's tenant
285
+ * schema, so queries may read any schema the template produces (or the tables
286
+ * `allowedTables` names in it) plus `sharedSchemas`, and nothing else.
287
+ */
288
+ tenantSchema?: TenantSchemaConfig;
260
289
  }
261
290
  /**
262
291
  * Simplified PostgreSQL adapter following IngestRequest format
@@ -270,6 +299,8 @@ declare class PostgresAdapter implements DatabaseAdapter {
270
299
  private readonly kind;
271
300
  private readonly dialect;
272
301
  private readonly allowedTables?;
302
+ /** Undefined when queries are unrestricted. */
303
+ private readonly allowsRelation?;
273
304
  constructor(clientFn: PostgresClientFn, options?: PostgresAdapterOptions);
274
305
  execute(sql: string, params?: Record<string, string | number | boolean | string[] | number[]>): Promise<DatabaseExecutionResult>;
275
306
  private validateQueryTables;
@@ -1327,6 +1358,16 @@ declare function mapGeneratedParams(params: Array<Record<string, unknown>>): Par
1327
1358
  */
1328
1359
  declare function normalizeSqlParamsInput(params: unknown[] | Record<string, unknown> | null | undefined): ParamRecord;
1329
1360
 
1361
+ /** Schema-per-tenant option for attachPostgres / attachRedshift (instead of tenantFieldName). */
1362
+ interface TenantSchemaAttachOptions {
1363
+ /**
1364
+ * Each tenant has its own schema, e.g. `{ template: "group_{tenant}_indexed" }`.
1365
+ * SQL naming any schema the template produces runs against the caller's tenant
1366
+ * schema, and only that schema (plus `sharedSchemas`) may be read.
1367
+ */
1368
+ tenantSchema?: TenantSchemaConfig;
1369
+ }
1370
+
1330
1371
  /** Options when constructing the SDK with HTTP (default) or with a custom API implementation. */
1331
1372
  interface QueryPanelSdkAPIOptions {
1332
1373
  defaultTenantId?: string;
@@ -1406,7 +1447,7 @@ declare class QueryPanelSdkAPI {
1406
1447
  tenantFieldName?: string;
1407
1448
  tenantFieldType?: string;
1408
1449
  enforceTenantIsolation?: boolean;
1409
- }): void;
1450
+ } & TenantSchemaAttachOptions): void;
1410
1451
  /**
1411
1452
  * Attach an Amazon Redshift database. Redshift speaks the Postgres wire
1412
1453
  * protocol, so `clientFn` can wrap a `pg` client; introspection uses
@@ -1418,7 +1459,7 @@ declare class QueryPanelSdkAPI {
1418
1459
  tenantFieldName?: string;
1419
1460
  tenantFieldType?: string;
1420
1461
  enforceTenantIsolation?: boolean;
1421
- }): void;
1462
+ } & TenantSchemaAttachOptions): void;
1422
1463
  attachBigQuery(name: string, clientFn: BigQueryClientFn, options: BigQueryAdapterOptions & {
1423
1464
  description?: string;
1424
1465
  tags?: string[];
@@ -2193,4 +2234,4 @@ declare class QueryPanelSdkAPI {
2193
2234
  }, signal?: AbortSignal): Promise<SdkDashboard[]>;
2194
2235
  }
2195
2236
 
2196
- export { ALL_VIZ_CHART_TYPES, type ActiveChartBulkGetResponse, type ActiveChartCreateInput, type ActiveChartListOptions, type ActiveChartUpdateInput, type AggregateOp, type AskOptions, type AskResponse, type AxisField, type AxisFieldInput, BigQueryAdapter, type BigQueryAdapterOptions, type BigQueryClientFn, type BigQueryQueryRequest, type BigQueryQueryResult, CallbackApiClient, type CatalogSchema, type CatalogTable, type CatalogTableType, type ChartBulkGetResponse, type ChartCreateInput, type ChartEncoding, type ChartEnvelope, type ChartListAllOptions, type ChartListOptions, type ChartModifyInput, type ChartModifyOptions, type ChartModifyResponse, type ChartSpec, type ChartType, type ChartUpdateInput, ClickHouseAdapter, type ClickHouseAdapterOptions, type ClickHouseClientFn, type ContextDocument, type DashboardCreateInput, type DashboardForkInput, type DashboardForkUpdateInput, type DashboardListOptions, type DashboardUpdateInput, type DatabaseAdapter, type DatabaseDialect, type DateRangeInput, type EncodingHints, type FieldRef, type FieldRefInput, type FieldType, type IQueryPanelApi, type IngestResponse, type IntentResult, type ListTablesOptions, type MapEncoding, type MapLocation, type MapLocationMode, type MapSpec, type MetricEncoding, type MetricField, type MetricSpec, type PaginatedResponse$1 as PaginatedResponse, type PaginationInfo$1 as PaginationInfo, type PaginationQuery$1 as PaginationQuery, type ParamRecord, type ParamValue, type PipelineTrace, PostgresAdapter, type PostgresAdapterOptions, type PostgresClientFn, type PostgresWireDialect, QueryErrorCode, QueryErrorCode as QueryErrorCodeType, QueryPanelSdkAPI, type QueryPanelSdkAPIOptions, QueryPipelineError, type RequestHandler, type RequestHandlerOptions, type RequestHandlerResult, type SchemaIngestBatchEmbedResponse, type SchemaIngestRequestOptions, type SchemaIntrospection, type SchemaSyncOptions, type SchemaSyncProgressEvent, SchemaTooLargeError, type SdkActiveChart, type SdkChart, type SdkDashboard, type SdkSession, type SdkSessionTurn, type SessionGetOptions, type SessionListOptions, type SessionUpdateInput, type SqlModifications, type StackingMode, type StageSchemaIngestOptions, type StageSchemaIngestResult, type SyncSchemaInBatchesOptions, type TableColumn, type TableEncoding, type TableSpec, type TimeUnit, type ValueFormat, type VizModifications, type VizSpec, type VizSpecGenerateInput, type VizSpecGenerateOptions, type VizSpecGeneratorInput, type VizSpecKind, type VizSpecResponse, type VizSpecResult, anonymizeResults, mapGeneratedParams, normalizeSqlParamsInput, resolveQueryPipeline };
2237
+ export { ALL_VIZ_CHART_TYPES, type ActiveChartBulkGetResponse, type ActiveChartCreateInput, type ActiveChartListOptions, type ActiveChartUpdateInput, type AggregateOp, type AskOptions, type AskResponse, type AxisField, type AxisFieldInput, BigQueryAdapter, type BigQueryAdapterOptions, type BigQueryClientFn, type BigQueryQueryRequest, type BigQueryQueryResult, CallbackApiClient, type CatalogSchema, type CatalogTable, type CatalogTableType, type ChartBulkGetResponse, type ChartCreateInput, type ChartEncoding, type ChartEnvelope, type ChartListAllOptions, type ChartListOptions, type ChartModifyInput, type ChartModifyOptions, type ChartModifyResponse, type ChartSpec, type ChartType, type ChartUpdateInput, ClickHouseAdapter, type ClickHouseAdapterOptions, type ClickHouseClientFn, type ContextDocument, type DashboardCreateInput, type DashboardForkInput, type DashboardForkUpdateInput, type DashboardListOptions, type DashboardUpdateInput, type DatabaseAdapter, type DatabaseDialect, type DateRangeInput, type EncodingHints, type FieldRef, type FieldRefInput, type FieldType, type IQueryPanelApi, type IngestResponse, type IntentResult, type ListTablesOptions, type MapEncoding, type MapLocation, type MapLocationMode, type MapSpec, type MetricEncoding, type MetricField, type MetricSpec, type PaginatedResponse$1 as PaginatedResponse, type PaginationInfo$1 as PaginationInfo, type PaginationQuery$1 as PaginationQuery, type ParamRecord, type ParamValue, type PipelineTrace, PostgresAdapter, type PostgresAdapterOptions, type PostgresClientFn, type PostgresWireDialect, QueryErrorCode, QueryErrorCode as QueryErrorCodeType, QueryPanelSdkAPI, type QueryPanelSdkAPIOptions, QueryPipelineError, type RequestHandler, type RequestHandlerOptions, type RequestHandlerResult, type SchemaIngestBatchEmbedResponse, type SchemaIngestRequestOptions, type SchemaIntrospection, type SchemaSyncOptions, type SchemaSyncProgressEvent, SchemaTooLargeError, type SdkActiveChart, type SdkChart, type SdkDashboard, type SdkSession, type SdkSessionTurn, type SessionGetOptions, type SessionListOptions, type SessionUpdateInput, type SqlModifications, type StackingMode, type StageSchemaIngestOptions, type StageSchemaIngestResult, type SyncSchemaInBatchesOptions, type TableColumn, type TableEncoding, type TableSpec, type TenantSchemaAttachOptions, type TenantSchemaConfig, TenantSchemaError, type TimeUnit, type ValueFormat, type VizModifications, type VizSpec, type VizSpecGenerateInput, type VizSpecGenerateOptions, type VizSpecGeneratorInput, type VizSpecKind, type VizSpecResponse, type VizSpecResult, anonymizeResults, mapGeneratedParams, normalizeSqlParamsInput, parseTenantSchemaTemplate, resolveQueryPipeline, resolveTenantSchema, rewriteTenantSchema };
package/dist/index.d.ts CHANGED
@@ -230,6 +230,29 @@ declare class ClickHouseAdapter implements DatabaseAdapter {
230
230
  private extractRows;
231
231
  }
232
232
 
233
+ /** Attach option for schema-per-tenant isolation. */
234
+ interface TenantSchemaConfig {
235
+ /** Schema name with one `{tenant}` placeholder, e.g. `group_{tenant}_indexed`. */
236
+ template: string;
237
+ /** Extra schemas every tenant may read (lookup tables etc.). */
238
+ sharedSchemas?: string[];
239
+ }
240
+ declare class TenantSchemaError extends Error {
241
+ constructor(message: string);
242
+ }
243
+ interface ParsedTemplate {
244
+ prefix: string;
245
+ suffix: string;
246
+ }
247
+ declare function parseTenantSchemaTemplate(template: string): ParsedTemplate;
248
+ /** The schema a tenant's queries run against. Throws for unsafe tenant ids. */
249
+ declare function resolveTenantSchema(template: string, tenantId: string): string;
250
+ /**
251
+ * Rewrite every template-schema qualifier to the tenant's schema. Fails closed when
252
+ * another tenant's schema is still named anywhere (strings and comments included).
253
+ */
254
+ declare function rewriteTenantSchema(sql: string, template: string, tenantId: string): string;
255
+
233
256
  interface PostgresQueryResult {
234
257
  rows: Array<Record<string, unknown>>;
235
258
  fields: Array<{
@@ -257,6 +280,12 @@ interface PostgresAdapterOptions {
257
280
  * to these tables only.
258
281
  */
259
282
  allowedTables?: string[];
283
+ /**
284
+ * Schema-per-tenant connection: SQL has been rewritten to the caller's tenant
285
+ * schema, so queries may read any schema the template produces (or the tables
286
+ * `allowedTables` names in it) plus `sharedSchemas`, and nothing else.
287
+ */
288
+ tenantSchema?: TenantSchemaConfig;
260
289
  }
261
290
  /**
262
291
  * Simplified PostgreSQL adapter following IngestRequest format
@@ -270,6 +299,8 @@ declare class PostgresAdapter implements DatabaseAdapter {
270
299
  private readonly kind;
271
300
  private readonly dialect;
272
301
  private readonly allowedTables?;
302
+ /** Undefined when queries are unrestricted. */
303
+ private readonly allowsRelation?;
273
304
  constructor(clientFn: PostgresClientFn, options?: PostgresAdapterOptions);
274
305
  execute(sql: string, params?: Record<string, string | number | boolean | string[] | number[]>): Promise<DatabaseExecutionResult>;
275
306
  private validateQueryTables;
@@ -1327,6 +1358,16 @@ declare function mapGeneratedParams(params: Array<Record<string, unknown>>): Par
1327
1358
  */
1328
1359
  declare function normalizeSqlParamsInput(params: unknown[] | Record<string, unknown> | null | undefined): ParamRecord;
1329
1360
 
1361
+ /** Schema-per-tenant option for attachPostgres / attachRedshift (instead of tenantFieldName). */
1362
+ interface TenantSchemaAttachOptions {
1363
+ /**
1364
+ * Each tenant has its own schema, e.g. `{ template: "group_{tenant}_indexed" }`.
1365
+ * SQL naming any schema the template produces runs against the caller's tenant
1366
+ * schema, and only that schema (plus `sharedSchemas`) may be read.
1367
+ */
1368
+ tenantSchema?: TenantSchemaConfig;
1369
+ }
1370
+
1330
1371
  /** Options when constructing the SDK with HTTP (default) or with a custom API implementation. */
1331
1372
  interface QueryPanelSdkAPIOptions {
1332
1373
  defaultTenantId?: string;
@@ -1406,7 +1447,7 @@ declare class QueryPanelSdkAPI {
1406
1447
  tenantFieldName?: string;
1407
1448
  tenantFieldType?: string;
1408
1449
  enforceTenantIsolation?: boolean;
1409
- }): void;
1450
+ } & TenantSchemaAttachOptions): void;
1410
1451
  /**
1411
1452
  * Attach an Amazon Redshift database. Redshift speaks the Postgres wire
1412
1453
  * protocol, so `clientFn` can wrap a `pg` client; introspection uses
@@ -1418,7 +1459,7 @@ declare class QueryPanelSdkAPI {
1418
1459
  tenantFieldName?: string;
1419
1460
  tenantFieldType?: string;
1420
1461
  enforceTenantIsolation?: boolean;
1421
- }): void;
1462
+ } & TenantSchemaAttachOptions): void;
1422
1463
  attachBigQuery(name: string, clientFn: BigQueryClientFn, options: BigQueryAdapterOptions & {
1423
1464
  description?: string;
1424
1465
  tags?: string[];
@@ -2193,4 +2234,4 @@ declare class QueryPanelSdkAPI {
2193
2234
  }, signal?: AbortSignal): Promise<SdkDashboard[]>;
2194
2235
  }
2195
2236
 
2196
- export { ALL_VIZ_CHART_TYPES, type ActiveChartBulkGetResponse, type ActiveChartCreateInput, type ActiveChartListOptions, type ActiveChartUpdateInput, type AggregateOp, type AskOptions, type AskResponse, type AxisField, type AxisFieldInput, BigQueryAdapter, type BigQueryAdapterOptions, type BigQueryClientFn, type BigQueryQueryRequest, type BigQueryQueryResult, CallbackApiClient, type CatalogSchema, type CatalogTable, type CatalogTableType, type ChartBulkGetResponse, type ChartCreateInput, type ChartEncoding, type ChartEnvelope, type ChartListAllOptions, type ChartListOptions, type ChartModifyInput, type ChartModifyOptions, type ChartModifyResponse, type ChartSpec, type ChartType, type ChartUpdateInput, ClickHouseAdapter, type ClickHouseAdapterOptions, type ClickHouseClientFn, type ContextDocument, type DashboardCreateInput, type DashboardForkInput, type DashboardForkUpdateInput, type DashboardListOptions, type DashboardUpdateInput, type DatabaseAdapter, type DatabaseDialect, type DateRangeInput, type EncodingHints, type FieldRef, type FieldRefInput, type FieldType, type IQueryPanelApi, type IngestResponse, type IntentResult, type ListTablesOptions, type MapEncoding, type MapLocation, type MapLocationMode, type MapSpec, type MetricEncoding, type MetricField, type MetricSpec, type PaginatedResponse$1 as PaginatedResponse, type PaginationInfo$1 as PaginationInfo, type PaginationQuery$1 as PaginationQuery, type ParamRecord, type ParamValue, type PipelineTrace, PostgresAdapter, type PostgresAdapterOptions, type PostgresClientFn, type PostgresWireDialect, QueryErrorCode, QueryErrorCode as QueryErrorCodeType, QueryPanelSdkAPI, type QueryPanelSdkAPIOptions, QueryPipelineError, type RequestHandler, type RequestHandlerOptions, type RequestHandlerResult, type SchemaIngestBatchEmbedResponse, type SchemaIngestRequestOptions, type SchemaIntrospection, type SchemaSyncOptions, type SchemaSyncProgressEvent, SchemaTooLargeError, type SdkActiveChart, type SdkChart, type SdkDashboard, type SdkSession, type SdkSessionTurn, type SessionGetOptions, type SessionListOptions, type SessionUpdateInput, type SqlModifications, type StackingMode, type StageSchemaIngestOptions, type StageSchemaIngestResult, type SyncSchemaInBatchesOptions, type TableColumn, type TableEncoding, type TableSpec, type TimeUnit, type ValueFormat, type VizModifications, type VizSpec, type VizSpecGenerateInput, type VizSpecGenerateOptions, type VizSpecGeneratorInput, type VizSpecKind, type VizSpecResponse, type VizSpecResult, anonymizeResults, mapGeneratedParams, normalizeSqlParamsInput, resolveQueryPipeline };
2237
+ export { ALL_VIZ_CHART_TYPES, type ActiveChartBulkGetResponse, type ActiveChartCreateInput, type ActiveChartListOptions, type ActiveChartUpdateInput, type AggregateOp, type AskOptions, type AskResponse, type AxisField, type AxisFieldInput, BigQueryAdapter, type BigQueryAdapterOptions, type BigQueryClientFn, type BigQueryQueryRequest, type BigQueryQueryResult, CallbackApiClient, type CatalogSchema, type CatalogTable, type CatalogTableType, type ChartBulkGetResponse, type ChartCreateInput, type ChartEncoding, type ChartEnvelope, type ChartListAllOptions, type ChartListOptions, type ChartModifyInput, type ChartModifyOptions, type ChartModifyResponse, type ChartSpec, type ChartType, type ChartUpdateInput, ClickHouseAdapter, type ClickHouseAdapterOptions, type ClickHouseClientFn, type ContextDocument, type DashboardCreateInput, type DashboardForkInput, type DashboardForkUpdateInput, type DashboardListOptions, type DashboardUpdateInput, type DatabaseAdapter, type DatabaseDialect, type DateRangeInput, type EncodingHints, type FieldRef, type FieldRefInput, type FieldType, type IQueryPanelApi, type IngestResponse, type IntentResult, type ListTablesOptions, type MapEncoding, type MapLocation, type MapLocationMode, type MapSpec, type MetricEncoding, type MetricField, type MetricSpec, type PaginatedResponse$1 as PaginatedResponse, type PaginationInfo$1 as PaginationInfo, type PaginationQuery$1 as PaginationQuery, type ParamRecord, type ParamValue, type PipelineTrace, PostgresAdapter, type PostgresAdapterOptions, type PostgresClientFn, type PostgresWireDialect, QueryErrorCode, QueryErrorCode as QueryErrorCodeType, QueryPanelSdkAPI, type QueryPanelSdkAPIOptions, QueryPipelineError, type RequestHandler, type RequestHandlerOptions, type RequestHandlerResult, type SchemaIngestBatchEmbedResponse, type SchemaIngestRequestOptions, type SchemaIntrospection, type SchemaSyncOptions, type SchemaSyncProgressEvent, SchemaTooLargeError, type SdkActiveChart, type SdkChart, type SdkDashboard, type SdkSession, type SdkSessionTurn, type SessionGetOptions, type SessionListOptions, type SessionUpdateInput, type SqlModifications, type StackingMode, type StageSchemaIngestOptions, type StageSchemaIngestResult, type SyncSchemaInBatchesOptions, type TableColumn, type TableEncoding, type TableSpec, type TenantSchemaAttachOptions, type TenantSchemaConfig, TenantSchemaError, type TimeUnit, type ValueFormat, type VizModifications, type VizSpec, type VizSpecGenerateInput, type VizSpecGenerateOptions, type VizSpecGeneratorInput, type VizSpecKind, type VizSpecResponse, type VizSpecResult, anonymizeResults, mapGeneratedParams, normalizeSqlParamsInput, parseTenantSchemaTemplate, resolveQueryPipeline, resolveTenantSchema, rewriteTenantSchema };
package/dist/index.js CHANGED
@@ -581,6 +581,248 @@ function toNumber(value) {
581
581
  return Number.isNaN(parsed) ? void 0 : parsed;
582
582
  }
583
583
 
584
+ // src/core/sql-relations.ts
585
+ function tokenize(sql) {
586
+ const tokens = [];
587
+ let i = 0;
588
+ while (i < sql.length) {
589
+ const ch = sql[i];
590
+ const next = sql[i + 1];
591
+ if (/\s/.test(ch)) {
592
+ i++;
593
+ } else if (ch === "-" && next === "-") {
594
+ while (i < sql.length && sql[i] !== "\n") i++;
595
+ } else if (ch === "/" && next === "*") {
596
+ const end = sql.indexOf("*/", i + 2);
597
+ i = end < 0 ? sql.length : end + 2;
598
+ } else if (ch === "'") {
599
+ i++;
600
+ while (i < sql.length) {
601
+ if (sql[i] === "\\") i += 2;
602
+ else if (sql[i] === "'" && sql[i + 1] === "'") i += 2;
603
+ else if (sql[i] === "'") break;
604
+ else i++;
605
+ }
606
+ i++;
607
+ } else if (ch === "$" && /[A-Za-z_$]/.test(next ?? "")) {
608
+ const tag = /^\$[A-Za-z_]*\$/.exec(sql.slice(i))?.[0];
609
+ if (tag) {
610
+ const end = sql.indexOf(tag, i + tag.length);
611
+ i = end < 0 ? sql.length : end + tag.length;
612
+ } else {
613
+ i++;
614
+ }
615
+ } else if (ch === '"') {
616
+ const start = i;
617
+ let text = "";
618
+ i++;
619
+ while (i < sql.length) {
620
+ if (sql[i] === '"' && sql[i + 1] === '"') {
621
+ text += '"';
622
+ i += 2;
623
+ } else if (sql[i] === '"') break;
624
+ else text += sql[i++];
625
+ }
626
+ i++;
627
+ tokens.push({ kind: "word", text, quoted: true, start, end: i });
628
+ } else if (/[A-Za-z_]/.test(ch)) {
629
+ const start = i;
630
+ while (i < sql.length && /[A-Za-z0-9_$]/.test(sql[i])) i++;
631
+ tokens.push({ kind: "word", text: sql.slice(start, i), quoted: false, start, end: i });
632
+ } else if (/[0-9]/.test(ch)) {
633
+ while (i < sql.length && /[0-9.eE]/.test(sql[i])) i++;
634
+ } else {
635
+ tokens.push({ kind: "punct", text: ch, start: i, end: i + 1 });
636
+ i++;
637
+ }
638
+ }
639
+ return tokens;
640
+ }
641
+ var identifier = (t) => t?.kind === "word" ? t.quoted ? t.text : t.text.toLowerCase() : void 0;
642
+ var isKeyword = (t, word) => t?.kind === "word" && !t.quoted && t.text.toLowerCase() === word;
643
+ var isPunct = (t, p) => t?.kind === "punct" && t.text === p;
644
+ var CLAUSE_WORDS = /* @__PURE__ */ new Set([
645
+ "where",
646
+ "group",
647
+ "order",
648
+ "limit",
649
+ "having",
650
+ "union",
651
+ "intersect",
652
+ "except",
653
+ "join",
654
+ "inner",
655
+ "left",
656
+ "right",
657
+ "full",
658
+ "cross",
659
+ "natural",
660
+ "on",
661
+ "using",
662
+ "window",
663
+ "qualify",
664
+ "offset",
665
+ "fetch",
666
+ "for",
667
+ "lateral",
668
+ "as"
669
+ ]);
670
+ var FROM_CLAUSE_END = /* @__PURE__ */ new Set([
671
+ "where",
672
+ "group",
673
+ "having",
674
+ "order",
675
+ "limit",
676
+ "offset",
677
+ "fetch",
678
+ "union",
679
+ "intersect",
680
+ "except",
681
+ "window",
682
+ "qualify",
683
+ "for",
684
+ "returning"
685
+ ]);
686
+ function cteNames(tokens) {
687
+ const names = /* @__PURE__ */ new Set();
688
+ for (let k = 0; k + 2 < tokens.length; k++) {
689
+ const value = identifier(tokens[k]);
690
+ if (!value) continue;
691
+ if (isKeyword(tokens[k + 1], "as") && isPunct(tokens[k + 2], "(")) names.add(value);
692
+ if (isPunct(tokens[k + 1], "(")) {
693
+ let j = k + 2;
694
+ while (j < tokens.length && !isPunct(tokens[j], ")")) j++;
695
+ if (isKeyword(tokens[j + 1], "as") && isPunct(tokens[j + 2], "(")) names.add(value);
696
+ }
697
+ }
698
+ return names;
699
+ }
700
+ function findRelations(sql) {
701
+ const tokens = tokenize(sql);
702
+ const ctes = cteNames(tokens);
703
+ const relations = [];
704
+ const readItem = (start) => {
705
+ let k = start;
706
+ if (isKeyword(tokens[k], "lateral")) k++;
707
+ const first = identifier(tokens[k]);
708
+ if (!first) return k;
709
+ const parts = [first];
710
+ k++;
711
+ while (isPunct(tokens[k], ".") && identifier(tokens[k + 1])) {
712
+ parts.push(identifier(tokens[k + 1]));
713
+ k += 2;
714
+ }
715
+ if (isPunct(tokens[k], "(")) return k;
716
+ if (!(parts.length === 1 && ctes.has(first))) relations.push({ parts });
717
+ if (isKeyword(tokens[k], "as")) k++;
718
+ const alias = tokens[k];
719
+ if (alias?.kind === "word" && (alias.quoted || !CLAUSE_WORDS.has(alias.text.toLowerCase()))) k++;
720
+ return k;
721
+ };
722
+ const selectAtDepth = [false];
723
+ const inFromAtDepth = [false];
724
+ const top = () => selectAtDepth.length - 1;
725
+ for (let k = 0; k < tokens.length; k++) {
726
+ const token = tokens[k];
727
+ if (isPunct(token, "(")) {
728
+ selectAtDepth.push(false);
729
+ inFromAtDepth.push(false);
730
+ } else if (isPunct(token, ")")) {
731
+ if (selectAtDepth.length > 1) {
732
+ selectAtDepth.pop();
733
+ inFromAtDepth.pop();
734
+ }
735
+ } else if (isKeyword(token, "select")) {
736
+ selectAtDepth[top()] = true;
737
+ inFromAtDepth[top()] = false;
738
+ } else if (isKeyword(token, "join")) {
739
+ k = readItem(k + 1) - 1;
740
+ } else if (isKeyword(token, "from") && selectAtDepth[top()] && !isKeyword(tokens[k - 1], "distinct")) {
741
+ inFromAtDepth[top()] = true;
742
+ k = readItem(k + 1) - 1;
743
+ } else if (isPunct(token, ",") && inFromAtDepth[top()]) {
744
+ k = readItem(k + 1) - 1;
745
+ } else if (token?.kind === "word" && !token.quoted && FROM_CLAUSE_END.has(token.text.toLowerCase())) {
746
+ inFromAtDepth[top()] = false;
747
+ }
748
+ }
749
+ return relations;
750
+ }
751
+
752
+ // src/core/tenant-schema.ts
753
+ var TenantSchemaError = class extends Error {
754
+ constructor(message) {
755
+ super(message);
756
+ this.name = "TenantSchemaError";
757
+ }
758
+ };
759
+ var PLACEHOLDER = "{tenant}";
760
+ var TEMPLATE_PART = /^[a-z0-9_]*$/;
761
+ var TENANT_ID = /^[A-Za-z0-9_]{1,64}$/;
762
+ function parseTenantSchemaTemplate(template) {
763
+ const value = template.trim().toLowerCase();
764
+ const index = value.indexOf(PLACEHOLDER);
765
+ if (index < 0 || value.indexOf(PLACEHOLDER, index + 1) >= 0) {
766
+ throw new TenantSchemaError(
767
+ `Tenant schema template must contain ${PLACEHOLDER} exactly once`
768
+ );
769
+ }
770
+ const prefix = value.slice(0, index);
771
+ const suffix = value.slice(index + PLACEHOLDER.length);
772
+ if (!TEMPLATE_PART.test(prefix) || !TEMPLATE_PART.test(suffix) || !(prefix || suffix)) {
773
+ throw new TenantSchemaError(
774
+ "Tenant schema template may only use a-z, 0-9 and _ around {tenant}"
775
+ );
776
+ }
777
+ return { prefix, suffix };
778
+ }
779
+ function matchesTemplate({ prefix, suffix }, schema) {
780
+ const value = schema.toLowerCase();
781
+ 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));
782
+ }
783
+ function resolveTenantSchema(template, tenantId) {
784
+ const { prefix, suffix } = parseTenantSchemaTemplate(template);
785
+ const id = tenantId.trim();
786
+ if (!TENANT_ID.test(id)) {
787
+ throw new TenantSchemaError(
788
+ "tenantId must be 1-64 characters of letters, digits or _ for schema-per-tenant isolation"
789
+ );
790
+ }
791
+ return `${prefix}${id.toLowerCase()}${suffix}`;
792
+ }
793
+ function rewriteTenantSchema(sql, template, tenantId) {
794
+ const parsed = parseTenantSchemaTemplate(template);
795
+ const tenantSchema = resolveTenantSchema(template, tenantId);
796
+ const tokens = tokenize(sql);
797
+ let rewritten = "";
798
+ let cursor = 0;
799
+ for (let k = 0; k < tokens.length; k++) {
800
+ const token = tokens[k];
801
+ const value = identifier(token);
802
+ if (!token || !value || !isPunct(tokens[k + 1], ".")) continue;
803
+ if (!matchesTemplate(parsed, value) || value.toLowerCase() === tenantSchema) continue;
804
+ const replacement = token.kind === "word" && token.quoted ? `"${tenantSchema}"` : tenantSchema;
805
+ rewritten += sql.slice(cursor, token.start) + replacement;
806
+ cursor = token.end;
807
+ }
808
+ rewritten += sql.slice(cursor);
809
+ const pattern = new RegExp(`${escapeRegex(parsed.prefix)}[a-z0-9_]+${escapeRegex(parsed.suffix)}`, "gi");
810
+ for (const match of rewritten.matchAll(pattern)) {
811
+ if (match[0].toLowerCase() !== tenantSchema) {
812
+ throw new TenantSchemaError("Query references another tenant's schema");
813
+ }
814
+ }
815
+ return rewritten;
816
+ }
817
+ var TENANT_SCHEMA_KEY = PLACEHOLDER;
818
+ function tenantSchemaKey(template) {
819
+ const parsed = parseTenantSchemaTemplate(template);
820
+ return (schema) => matchesTemplate(parsed, schema) ? TENANT_SCHEMA_KEY : schema.toLowerCase();
821
+ }
822
+ function escapeRegex(value) {
823
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
824
+ }
825
+
584
826
  // src/adapters/postgres.ts
585
827
  var WILDCARD = "*";
586
828
  function maxPostgresPlaceholderIndex(sql) {
@@ -627,6 +869,11 @@ var PostgresAdapter = class {
627
869
  this.defaultSchema
628
870
  );
629
871
  }
872
+ this.allowsRelation = buildRelationAllowlist(
873
+ this.allowedTables,
874
+ options.tenantSchema,
875
+ this.defaultSchema
876
+ );
630
877
  }
631
878
  clientFn;
632
879
  databaseName;
@@ -634,9 +881,11 @@ var PostgresAdapter = class {
634
881
  kind;
635
882
  dialect;
636
883
  allowedTables;
884
+ /** Undefined when queries are unrestricted. */
885
+ allowsRelation;
637
886
  async execute(sql, params) {
638
- if (this.allowedTables) {
639
- this.validateQueryTables(sql);
887
+ if (this.allowsRelation) {
888
+ this.validateQueryTables(sql, this.allowsRelation);
640
889
  }
641
890
  const boundParams = filterParamsForPostgresSql(sql, params);
642
891
  let paramArray;
@@ -647,29 +896,11 @@ var PostgresAdapter = class {
647
896
  const fields = result.fields.map((f) => f.name);
648
897
  return { fields, rows: result.rows };
649
898
  }
650
- validateQueryTables(sql) {
651
- if (!this.allowedTables || this.allowedTables.length === 0) {
652
- return;
653
- }
654
- const allowedSet = new Set(
655
- this.allowedTables.map((t) => tableKey2(t.schema, t.table))
656
- );
657
- const allowedSchemas = new Set(
658
- this.allowedTables.filter((t) => t.table === WILDCARD).map((t) => t.schema)
659
- );
660
- 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*/)");
661
- const tablePattern = /(?:FROM|JOIN)\s+(?:ONLY\s+)?(?:([a-zA-Z_][a-zA-Z0-9_]*)\.)?(["']?[a-zA-Z_][a-zA-Z0-9_]*["']?)/gi;
662
- const matches = neutralizedSql.matchAll(tablePattern);
663
- for (const match of matches) {
664
- const schema = match[1] ?? this.defaultSchema;
665
- const table = match[2]?.replace(/['"]/g, "");
666
- if (table) {
667
- const key = tableKey2(schema, table);
668
- if (!allowedSet.has(key) && !allowedSchemas.has(schema)) {
669
- throw new Error(
670
- `Query references table "${schema}.${table}" which is not in the allowed tables list`
671
- );
672
- }
899
+ validateQueryTables(sql, allows) {
900
+ for (const relation of findRelations(sql)) {
901
+ if (!allows(relation)) {
902
+ const name = relation.parts.length === 1 ? `${this.defaultSchema}.${relation.parts[0]}` : relation.parts.join(".");
903
+ throw new Error(`Query references table "${name}" which is not in the allowed tables list`);
673
904
  }
674
905
  }
675
906
  }
@@ -707,6 +938,9 @@ var PostgresAdapter = class {
707
938
  return positionalParams;
708
939
  }
709
940
  async validate(sql, params) {
941
+ if (this.allowsRelation) {
942
+ this.validateQueryTables(sql, this.allowsRelation);
943
+ }
710
944
  const boundParams = filterParamsForPostgresSql(sql, params);
711
945
  let paramArray;
712
946
  if (boundParams) {
@@ -996,6 +1230,27 @@ function buildListTablesQuery(dialect, options) {
996
1230
  LIMIT ${limit};`;
997
1231
  return { sql, params };
998
1232
  }
1233
+ function buildRelationAllowlist(allowedTables, tenantSchema, defaultSchema) {
1234
+ if (!allowedTables?.length && !tenantSchema) return void 0;
1235
+ const schemaKey = tenantSchema ? tenantSchemaKey(tenantSchema.template) : (schema) => schema.toLowerCase();
1236
+ const entries = [
1237
+ ...allowedTables?.length ? allowedTables : [{ schema: TENANT_SCHEMA_KEY, table: WILDCARD }],
1238
+ ...(tenantSchema?.sharedSchemas ?? []).map((schema) => ({ schema, table: WILDCARD }))
1239
+ ];
1240
+ const tables = /* @__PURE__ */ new Set();
1241
+ const wildcardSchemas = /* @__PURE__ */ new Set();
1242
+ for (const entry of entries) {
1243
+ const schema = entry.schema === TENANT_SCHEMA_KEY ? entry.schema : schemaKey(entry.schema);
1244
+ if (entry.table === WILDCARD) wildcardSchemas.add(schema);
1245
+ else tables.add(tableKey2(schema, entry.table.toLowerCase()));
1246
+ }
1247
+ return ({ parts }) => {
1248
+ if (parts.length > 2) return false;
1249
+ const table = parts[parts.length - 1].toLowerCase();
1250
+ const schema = schemaKey(parts.length === 2 ? parts[0] : defaultSchema);
1251
+ return wildcardSchemas.has(schema) || tables.has(tableKey2(schema, table));
1252
+ };
1253
+ }
999
1254
  function tableKey2(schema, table) {
1000
1255
  return `${schema}.${table}`;
1001
1256
  }
@@ -1600,7 +1855,9 @@ var QueryEngine = class {
1600
1855
  const adapter = this.getDatabase(databaseName);
1601
1856
  const metadata = this.getDatabaseMetadata(databaseName);
1602
1857
  let finalSql = sql;
1603
- if (metadata) {
1858
+ if (metadata?.tenantSchema) {
1859
+ finalSql = rewriteTenantSchema(sql, metadata.tenantSchema.template, tenantId);
1860
+ } else if (metadata) {
1604
1861
  finalSql = this.ensureTenantIsolation(sql, params, metadata, tenantId);
1605
1862
  }
1606
1863
  await adapter.validate(finalSql, params);
@@ -3349,6 +3606,9 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
3349
3606
  this.queryEngine.attachDatabase(name, adapter, metadata);
3350
3607
  }
3351
3608
  attachPostgres(name, clientFn, options) {
3609
+ if (options?.tenantSchema && options.tenantFieldName) {
3610
+ throw new Error("Use either tenantSchema or tenantFieldName for tenant isolation, not both");
3611
+ }
3352
3612
  const adapter = new PostgresAdapter(clientFn, options);
3353
3613
  const metadata = {
3354
3614
  name,
@@ -3360,7 +3620,8 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
3360
3620
  enforceTenantIsolation: resolveAttachEnforceTenantIsolation(
3361
3621
  options?.tenantFieldName,
3362
3622
  options?.enforceTenantIsolation
3363
- )
3623
+ ),
3624
+ tenantSchema: options?.tenantSchema
3364
3625
  };
3365
3626
  this.queryEngine.attachDatabase(name, adapter, metadata);
3366
3627
  }
@@ -4414,9 +4675,13 @@ export {
4414
4675
  QueryPanelSdkAPI,
4415
4676
  QueryPipelineError,
4416
4677
  SchemaTooLargeError,
4678
+ TenantSchemaError,
4417
4679
  anonymizeResults,
4418
4680
  mapGeneratedParams,
4419
4681
  normalizeSqlParamsInput,
4420
- resolveQueryPipeline
4682
+ parseTenantSchemaTemplate,
4683
+ resolveQueryPipeline,
4684
+ resolveTenantSchema,
4685
+ rewriteTenantSchema
4421
4686
  };
4422
4687
  //# sourceMappingURL=index.js.map