@querypanel/node-sdk 1.0.41 → 1.0.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -42,18 +42,26 @@ const createPostgresClient = () => async (sql: string, params?: unknown[]) => {
42
42
  }
43
43
  };
44
44
 
45
- qp.attachPostgres("analytics", createPostgresClient(), {
46
- description: "Primary analytics warehouse",
47
- tenantFieldName: "tenant_id",
48
- });
45
+ // Attach PostgreSQL database using the SDK's PostgresAdapter
46
+ // The SDK will automatically handle tenant isolation when tenantFieldName is provided
47
+ qp.attachPostgres(
48
+ "pg_demo", // a uniq identifier for QueryPanel
49
+ createPostgresClientFn(),
50
+ {
51
+ database: "pg_demo", // database name
52
+ description: "PostgreSQL demo database", // some description that QueryPanel can use
53
+ tenantFieldName: "tenant_id", // SDK will automatically filter by tenant_id
54
+ enforceTenantIsolation: true, // Ensures all queries include tenant_id filter
55
+ allowedTables: ["orders"], // Only sync 'orders' table - 'users' will be excluded
56
+ });
49
57
 
50
58
  qp.attachClickhouse(
51
- "clicks",
59
+ "clicks", // uniq identifier for QueryPanel
52
60
  (params) => clickhouse.query(params),
53
61
  {
54
- database: "analytics",
55
- tenantFieldName: "customer_id",
56
- tenantFieldType: "String",
62
+ database: "analytics", // database name
63
+ tenantFieldName: "customer_id", // SDK will automatically filter by tenant_id
64
+ tenantFieldType: "String", // SDK will use it in the clickhouse query as {customer_id::String}
57
65
  },
58
66
  );
59
67
 
package/dist/index.cjs CHANGED
@@ -348,8 +348,9 @@ var PostgresAdapter = class {
348
348
  const allowedSet = new Set(
349
349
  this.allowedTables.map((t) => tableKey(t.schema, t.table))
350
350
  );
351
+ 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*/)");
351
352
  const tablePattern = /(?:FROM|JOIN)\s+(?:ONLY\s+)?(?:([a-zA-Z_][a-zA-Z0-9_]*)\.)?(["']?[a-zA-Z_][a-zA-Z0-9_]*["']?)/gi;
352
- const matches = sql.matchAll(tablePattern);
353
+ const matches = neutralizedSql.matchAll(tablePattern);
353
354
  for (const match of matches) {
354
355
  const schema = match[1] ?? this.defaultSchema;
355
356
  const table = match[2]?.replace(/['"]/g, "");
@@ -1174,13 +1175,26 @@ async function ask(client, queryEngine, question, options, signal) {
1174
1175
  let previousSql = options.previousSql;
1175
1176
  while (attempt <= maxRetry) {
1176
1177
  console.log({ lastError, previousSql });
1178
+ const databaseName = options.database ?? queryEngine.getDefaultDatabase();
1179
+ const metadata = databaseName ? queryEngine.getDatabaseMetadata(databaseName) : void 0;
1180
+ let tenantSettings;
1181
+ if (metadata?.tenantFieldName) {
1182
+ tenantSettings = {
1183
+ tenantFieldName: metadata.tenantFieldName,
1184
+ tenantFieldType: metadata.tenantFieldType,
1185
+ enforceTenantIsolation: metadata.enforceTenantIsolation
1186
+ };
1187
+ }
1177
1188
  const queryResponse = await client.post(
1178
1189
  "/query",
1179
1190
  {
1180
1191
  question,
1181
1192
  ...lastError ? { last_error: lastError } : {},
1182
1193
  ...previousSql ? { previous_sql: previousSql } : {},
1183
- ...options.maxRetry ? { max_retry: options.maxRetry } : {}
1194
+ ...options.maxRetry ? { max_retry: options.maxRetry } : {},
1195
+ ...tenantSettings ? { tenant_settings: tenantSettings } : {},
1196
+ ...databaseName ? { database: databaseName } : {},
1197
+ ...metadata?.dialect ? { dialect: metadata.dialect } : {}
1184
1198
  },
1185
1199
  tenantId,
1186
1200
  options.userId,
@@ -1188,8 +1202,8 @@ async function ask(client, queryEngine, question, options, signal) {
1188
1202
  signal,
1189
1203
  sessionId
1190
1204
  );
1191
- const databaseName = queryResponse.database ?? options.database ?? queryEngine.getDefaultDatabase();
1192
- if (!databaseName) {
1205
+ const dbName = queryResponse.database ?? options.database ?? queryEngine.getDefaultDatabase();
1206
+ if (!dbName) {
1193
1207
  throw new Error(
1194
1208
  "No database attached. Call attachPostgres/attachClickhouse first."
1195
1209
  );
@@ -1200,7 +1214,7 @@ async function ask(client, queryEngine, question, options, signal) {
1200
1214
  const execution = await queryEngine.validateAndExecute(
1201
1215
  queryResponse.sql,
1202
1216
  paramValues,
1203
- databaseName,
1217
+ dbName,
1204
1218
  tenantId
1205
1219
  );
1206
1220
  const rows = execution.rows ?? [];
@@ -1273,7 +1287,7 @@ async function ask(client, queryEngine, question, options, signal) {
1273
1287
  chart,
1274
1288
  context: queryResponse.context,
1275
1289
  attempts: attempt + 1,
1276
- target_db: databaseName
1290
+ target_db: dbName
1277
1291
  };
1278
1292
  } catch (error) {
1279
1293
  attempt++;
@@ -1375,6 +1389,7 @@ var QueryPanelSdkAPI = class {
1375
1389
  description: options?.description,
1376
1390
  tags: options?.tags,
1377
1391
  tenantFieldName: options?.tenantFieldName,
1392
+ tenantFieldType: options?.tenantFieldType ?? "String",
1378
1393
  enforceTenantIsolation: options?.tenantFieldName ? options?.enforceTenantIsolation ?? true : void 0
1379
1394
  };
1380
1395
  this.queryEngine.attachDatabase(name, adapter, metadata);