@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/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.
|
|
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
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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
|
-
|
|
4682
|
+
parseTenantSchemaTemplate,
|
|
4683
|
+
resolveQueryPipeline,
|
|
4684
|
+
resolveTenantSchema,
|
|
4685
|
+
rewriteTenantSchema
|
|
4421
4686
|
};
|
|
4422
4687
|
//# sourceMappingURL=index.js.map
|