@querypanel/node-sdk 1.0.57 → 1.0.58

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.js CHANGED
@@ -1,3 +1,183 @@
1
+ // src/adapters/bigquery.ts
2
+ var BigQueryAdapter = class {
3
+ constructor(clientFn, options) {
4
+ this.clientFn = clientFn;
5
+ this.projectId = options.projectId;
6
+ this.datasetProjectId = options.datasetProjectId ?? options.projectId;
7
+ this.defaultDataset = options.dataset;
8
+ this.databaseName = options.database ?? options.dataset;
9
+ this.kind = options.kind ?? "bigquery";
10
+ if (options.allowedTables) {
11
+ this.allowedTables = normalizeTableFilter(
12
+ options.allowedTables,
13
+ this.defaultDataset
14
+ );
15
+ }
16
+ }
17
+ projectId;
18
+ datasetProjectId;
19
+ defaultDataset;
20
+ databaseName;
21
+ kind;
22
+ allowedTables;
23
+ async execute(sql, params) {
24
+ if (this.allowedTables) {
25
+ this.validateQueryTables(sql);
26
+ }
27
+ const result = await this.clientFn({
28
+ query: sql,
29
+ params,
30
+ dryRun: false
31
+ });
32
+ return { fields: result.fields, rows: result.rows };
33
+ }
34
+ async validate(sql, params) {
35
+ await this.clientFn({
36
+ query: sql,
37
+ params,
38
+ dryRun: true
39
+ });
40
+ }
41
+ getDialect() {
42
+ return "bigquery";
43
+ }
44
+ /**
45
+ * Introspect using BigQuery INFORMATION_SCHEMA (TABLES and COLUMNS).
46
+ * Restricts to the default dataset (or allowedTables) and returns tables/columns only.
47
+ */
48
+ async introspect(options) {
49
+ const tablesToIntrospect = options?.tables ? normalizeTableFilter(options.tables, this.defaultDataset) : this.allowedTables;
50
+ const normalizedTables = tablesToIntrospect ?? [];
51
+ const tablesSql = buildTablesQuery(
52
+ this.datasetProjectId,
53
+ this.defaultDataset,
54
+ normalizedTables
55
+ );
56
+ const tablesResult = await this.clientFn({
57
+ query: tablesSql,
58
+ dryRun: false
59
+ });
60
+ const tableRows = tablesResult.rows;
61
+ const columnsSql = buildColumnsQuery(
62
+ this.datasetProjectId,
63
+ this.defaultDataset,
64
+ normalizedTables
65
+ );
66
+ const columnsResult = await this.clientFn({
67
+ query: columnsSql,
68
+ dryRun: false
69
+ });
70
+ const columnRows = columnsResult.rows;
71
+ const tablesByKey = /* @__PURE__ */ new Map();
72
+ for (const row of tableRows) {
73
+ const key = tableKey(row.table_schema, row.table_name);
74
+ const table = {
75
+ name: row.table_name,
76
+ schema: row.table_schema,
77
+ type: asTableType(row.table_type),
78
+ columns: []
79
+ };
80
+ tablesByKey.set(key, table);
81
+ }
82
+ for (const row of columnRows) {
83
+ const key = tableKey(row.table_schema, row.table_name);
84
+ const table = tablesByKey.get(key);
85
+ if (!table) continue;
86
+ const column = {
87
+ name: row.column_name,
88
+ type: row.data_type,
89
+ isPrimaryKey: false
90
+ };
91
+ table.columns.push(column);
92
+ }
93
+ const tables = Array.from(tablesByKey.values()).sort((a, b) => {
94
+ if (a.schema === b.schema) {
95
+ return a.name.localeCompare(b.name);
96
+ }
97
+ return a.schema.localeCompare(b.schema);
98
+ });
99
+ return {
100
+ db: {
101
+ kind: this.kind,
102
+ name: this.databaseName
103
+ },
104
+ tables,
105
+ introspectedAt: (/* @__PURE__ */ new Date()).toISOString()
106
+ };
107
+ }
108
+ validateQueryTables(sql) {
109
+ if (!this.allowedTables || this.allowedTables.length === 0) {
110
+ return;
111
+ }
112
+ const allowedSet = new Set(
113
+ this.allowedTables.map((t) => tableKey(t.dataset, t.table))
114
+ );
115
+ const tablePattern = /(?:FROM|JOIN)\s+`?(?:[\w-]+\.)?([\w-]+)\.([\w-]+)`?|(?:FROM|JOIN)\s+`?([\w-]+)`?/gi;
116
+ const matches = sql.matchAll(tablePattern);
117
+ for (const match of matches) {
118
+ const dataset = match[1] ?? match[3] ?? this.defaultDataset;
119
+ const table = match[2] ?? match[3];
120
+ if (table) {
121
+ const key = tableKey(dataset, table);
122
+ if (!allowedSet.has(key)) {
123
+ throw new Error(
124
+ `Query references table "${dataset}.${table}" which is not in the allowed tables list`
125
+ );
126
+ }
127
+ }
128
+ }
129
+ }
130
+ };
131
+ function normalizeTableFilter(tables, defaultDataset) {
132
+ if (!tables?.length) return [];
133
+ const normalized = [];
134
+ const seen = /* @__PURE__ */ new Set();
135
+ for (const raw of tables) {
136
+ if (!raw?.trim()) continue;
137
+ const trimmed = raw.trim();
138
+ const parts = trimmed.split(".");
139
+ const table = parts.pop() ?? "";
140
+ const dataset = parts.pop() ?? defaultDataset;
141
+ const key = tableKey(dataset, table);
142
+ if (seen.has(key)) continue;
143
+ seen.add(key);
144
+ normalized.push({ dataset, table });
145
+ }
146
+ return normalized;
147
+ }
148
+ function tableKey(dataset, table) {
149
+ return `${dataset}.${table}`;
150
+ }
151
+ function buildTablesQuery(datasetProjectId, dataset, tables) {
152
+ const qualifier = `\`${datasetProjectId}.${dataset}.INFORMATION_SCHEMA.TABLES\``;
153
+ const filter = tables.length > 0 ? ` AND table_name IN (${tables.map((t) => `'${escapeSql(t.table)}'`).join(", ")})` : "";
154
+ return `SELECT table_name, table_schema, table_type
155
+ FROM ${qualifier}
156
+ WHERE table_schema = '${escapeSql(dataset)}'${filter}
157
+ ORDER BY table_schema, table_name`;
158
+ }
159
+ function buildColumnsQuery(datasetProjectId, dataset, tables) {
160
+ const qualifier = `\`${datasetProjectId}.${dataset}.INFORMATION_SCHEMA.COLUMNS\``;
161
+ const filter = tables.length > 0 ? ` AND table_name IN (${tables.map((t) => `'${escapeSql(t.table)}'`).join(", ")})` : "";
162
+ return `SELECT table_name, table_schema, column_name, data_type, ordinal_position, is_nullable
163
+ FROM ${qualifier}
164
+ WHERE table_schema = '${escapeSql(dataset)}'${filter}
165
+ ORDER BY table_schema, table_name, ordinal_position`;
166
+ }
167
+ function escapeSql(value) {
168
+ return value.replace(/\\/g, "\\\\").replace(/'/g, "''");
169
+ }
170
+ function asTableType(tableType) {
171
+ const normalized = tableType?.toLowerCase() ?? "";
172
+ if (normalized.includes("view")) {
173
+ return normalized.includes("materialized") ? "materialized_view" : "view";
174
+ }
175
+ if (normalized.includes("external") || normalized.includes("snapshot") || normalized.includes("clone")) {
176
+ return "table";
177
+ }
178
+ return "table";
179
+ }
180
+
1
181
  // src/utils/clickhouse.ts
2
182
  var WRAPPER_REGEX = /^(Nullable|LowCardinality|SimpleAggregateFunction)\((.+)\)$/i;
3
183
  function unwrapTypeModifiers(type) {
@@ -67,7 +247,7 @@ var ClickHouseAdapter = class {
67
247
  this.defaultFormat = options.defaultFormat ?? "JSONEachRow";
68
248
  this.kind = options.kind ?? "clickhouse";
69
249
  if (options.allowedTables) {
70
- this.allowedTables = normalizeTableFilter(options.allowedTables);
250
+ this.allowedTables = normalizeTableFilter2(options.allowedTables);
71
251
  }
72
252
  }
73
253
  databaseName;
@@ -105,7 +285,7 @@ var ClickHouseAdapter = class {
105
285
  * No indexes, constraints, or statistics
106
286
  */
107
287
  async introspect(options) {
108
- const tablesToIntrospect = options?.tables ? normalizeTableFilter(options.tables) : this.allowedTables;
288
+ const tablesToIntrospect = options?.tables ? normalizeTableFilter2(options.tables) : this.allowedTables;
109
289
  const allowTables = tablesToIntrospect ?? [];
110
290
  const hasFilter = allowTables.length > 0;
111
291
  const queryParams = {
@@ -145,7 +325,7 @@ var ClickHouseAdapter = class {
145
325
  const base = {
146
326
  name: table.name,
147
327
  schema: this.databaseName,
148
- type: asTableType(table.engine),
328
+ type: asTableType2(table.engine),
149
329
  columns: tableColumns
150
330
  };
151
331
  const comment = sanitize(table.comment);
@@ -223,7 +403,7 @@ function normalizePayload(payload) {
223
403
  }
224
404
  return [];
225
405
  }
226
- function normalizeTableFilter(tables) {
406
+ function normalizeTableFilter2(tables) {
227
407
  if (!tables?.length) return [];
228
408
  const seen = /* @__PURE__ */ new Set();
229
409
  const normalized = [];
@@ -251,7 +431,7 @@ function transformColumnRow(row) {
251
431
  if (comment !== void 0) column.comment = comment;
252
432
  return column;
253
433
  }
254
- function asTableType(engine) {
434
+ function asTableType2(engine) {
255
435
  if (typeof engine === "string") {
256
436
  const normalized = engine.toLowerCase();
257
437
  if (normalized.includes("view")) {
@@ -280,7 +460,7 @@ var PostgresAdapter = class {
280
460
  this.defaultSchema = options.defaultSchema ?? "public";
281
461
  this.kind = options.kind ?? "postgres";
282
462
  if (options.allowedTables) {
283
- this.allowedTables = normalizeTableFilter2(
463
+ this.allowedTables = normalizeTableFilter3(
284
464
  options.allowedTables,
285
465
  this.defaultSchema
286
466
  );
@@ -307,7 +487,7 @@ var PostgresAdapter = class {
307
487
  return;
308
488
  }
309
489
  const allowedSet = new Set(
310
- this.allowedTables.map((t) => tableKey(t.schema, t.table))
490
+ this.allowedTables.map((t) => tableKey2(t.schema, t.table))
311
491
  );
312
492
  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*/)");
313
493
  const tablePattern = /(?:FROM|JOIN)\s+(?:ONLY\s+)?(?:([a-zA-Z_][a-zA-Z0-9_]*)\.)?(["']?[a-zA-Z_][a-zA-Z0-9_]*["']?)/gi;
@@ -316,7 +496,7 @@ var PostgresAdapter = class {
316
496
  const schema = match[1] ?? this.defaultSchema;
317
497
  const table = match[2]?.replace(/['"]/g, "");
318
498
  if (table) {
319
- const key = tableKey(schema, table);
499
+ const key = tableKey2(schema, table);
320
500
  if (!allowedSet.has(key)) {
321
501
  throw new Error(
322
502
  `Query references table "${schema}.${table}" which is not in the allowed tables list`
@@ -367,23 +547,23 @@ var PostgresAdapter = class {
367
547
  * No indexes, constraints, or statistics
368
548
  */
369
549
  async introspect(options) {
370
- const tablesToIntrospect = options?.tables ? normalizeTableFilter2(options.tables, this.defaultSchema) : this.allowedTables;
550
+ const tablesToIntrospect = options?.tables ? normalizeTableFilter3(options.tables, this.defaultSchema) : this.allowedTables;
371
551
  const normalizedTables = tablesToIntrospect ?? [];
372
552
  const tablesResult = await this.clientFn(
373
- buildTablesQuery(normalizedTables)
553
+ buildTablesQuery2(normalizedTables)
374
554
  );
375
555
  const tableRows = tablesResult.rows;
376
556
  const columnsResult = await this.clientFn(
377
- buildColumnsQuery(normalizedTables)
557
+ buildColumnsQuery2(normalizedTables)
378
558
  );
379
559
  const columnRows = columnsResult.rows;
380
560
  const tablesByKey = /* @__PURE__ */ new Map();
381
561
  for (const row of tableRows) {
382
- const key = tableKey(row.schema_name, row.table_name);
562
+ const key = tableKey2(row.schema_name, row.table_name);
383
563
  const table = {
384
564
  name: row.table_name,
385
565
  schema: row.schema_name,
386
- type: asTableType2(row.table_type),
566
+ type: asTableType3(row.table_type),
387
567
  columns: []
388
568
  };
389
569
  const comment = sanitize2(row.comment);
@@ -393,7 +573,7 @@ var PostgresAdapter = class {
393
573
  tablesByKey.set(key, table);
394
574
  }
395
575
  for (const row of columnRows) {
396
- const key = tableKey(row.table_schema, row.table_name);
576
+ const key = tableKey2(row.table_schema, row.table_name);
397
577
  const table = tablesByKey.get(key);
398
578
  if (!table) continue;
399
579
  const column = {
@@ -423,7 +603,7 @@ var PostgresAdapter = class {
423
603
  };
424
604
  }
425
605
  };
426
- function normalizeTableFilter2(tables, defaultSchema) {
606
+ function normalizeTableFilter3(tables, defaultSchema) {
427
607
  if (!tables?.length) return [];
428
608
  const normalized = [];
429
609
  const seen = /* @__PURE__ */ new Set();
@@ -437,14 +617,14 @@ function normalizeTableFilter2(tables, defaultSchema) {
437
617
  if (!isSafeIdentifier(schema) || !isSafeIdentifier(table)) {
438
618
  continue;
439
619
  }
440
- const key = tableKey(schema, table);
620
+ const key = tableKey2(schema, table);
441
621
  if (seen.has(key)) continue;
442
622
  seen.add(key);
443
623
  normalized.push({ schema, table });
444
624
  }
445
625
  return normalized;
446
626
  }
447
- function buildTablesQuery(tables) {
627
+ function buildTablesQuery2(tables) {
448
628
  const filter = buildFilterClause(tables, "n.nspname", "c.relname");
449
629
  return `SELECT
450
630
  c.relname AS table_name,
@@ -463,7 +643,7 @@ function buildTablesQuery(tables) {
463
643
  ${filter}
464
644
  ORDER BY n.nspname, c.relname;`;
465
645
  }
466
- function buildColumnsQuery(tables) {
646
+ function buildColumnsQuery2(tables) {
467
647
  const filter = buildFilterClause(
468
648
  tables,
469
649
  "cols.table_schema",
@@ -508,13 +688,13 @@ function buildFilterClause(tables, schemaExpr, tableExpr) {
508
688
  });
509
689
  return `AND (${clauses.join(" OR ")})`;
510
690
  }
511
- function tableKey(schema, table) {
691
+ function tableKey2(schema, table) {
512
692
  return `${schema}.${table}`;
513
693
  }
514
694
  function isSafeIdentifier(value) {
515
695
  return /^[A-Za-z_][A-Za-z0-9_]*$/.test(value);
516
696
  }
517
- function asTableType2(value) {
697
+ function asTableType3(value) {
518
698
  const normalized = value.toLowerCase();
519
699
  if (normalized.includes("view")) {
520
700
  return normalized.includes("materialized") ? "materialized_view" : "view";
@@ -982,6 +1162,9 @@ var QueryEngine = class {
982
1162
  const paramKey = tenantField;
983
1163
  params[paramKey] = tenantId;
984
1164
  tenantPredicate = `${tenantField} = {${tenantField}:${metadata.tenantFieldType ?? "String"}}`;
1165
+ } else if (metadata.dialect === "bigquery") {
1166
+ params[tenantField] = tenantId;
1167
+ tenantPredicate = `${tenantField} = @${tenantField}`;
985
1168
  } else {
986
1169
  const escapedId = tenantId.replace(/'/g, "''");
987
1170
  tenantPredicate = `${tenantField} = '${escapedId}'`;
@@ -1538,7 +1721,8 @@ async function ask(client, queryEngine, question, options, signal) {
1538
1721
  ...options.maxRetry ? { max_retry: options.maxRetry } : {},
1539
1722
  ...tenantSettings ? { tenant_settings: tenantSettings } : {},
1540
1723
  ...databaseName ? { database: databaseName } : {},
1541
- ...metadata?.dialect ? { dialect: metadata.dialect } : {}
1724
+ ...metadata?.dialect ? { dialect: metadata.dialect } : {},
1725
+ ...options.model?.trim() ? { model: options.model.trim() } : {}
1542
1726
  },
1543
1727
  tenantId,
1544
1728
  options.userId,
@@ -1562,7 +1746,7 @@ async function ask(client, queryEngine, question, options, signal) {
1562
1746
  const dbName = queryResponse.data.database ?? options.database ?? queryEngine.getDefaultDatabase();
1563
1747
  if (!dbName) {
1564
1748
  throw new Error(
1565
- "No database attached. Call attachPostgres/attachClickhouse first."
1749
+ "No database attached. Call attachPostgres, attachClickhouse, or attachBigQuery first."
1566
1750
  );
1567
1751
  }
1568
1752
  const paramMetadata = Array.isArray(queryResponse.data.params) ? queryResponse.data.params : [];
@@ -2103,20 +2287,22 @@ function resolveTenantId8(client, tenantId) {
2103
2287
  var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
2104
2288
  client;
2105
2289
  queryEngine;
2106
- constructor(baseUrl, privateKey, organizationId, options) {
2290
+ /**
2291
+ * @param workspaceId - Workspace UUID (same value as your org in the dashboard). Passed to the API as JWT claim `organizationId`.
2292
+ */
2293
+ constructor(baseUrl, privateKey, workspaceId, options) {
2107
2294
  if (options?.api) {
2108
2295
  this.client = options.api;
2109
2296
  } else {
2110
2297
  if (!baseUrl) throw new Error("Base URL is required");
2111
2298
  if (!privateKey) throw new Error("Private key is required");
2112
- if (!organizationId) throw new Error("Organization ID is required");
2113
- this.client = new ApiClient(baseUrl, privateKey, organizationId, {
2299
+ if (!workspaceId) throw new Error("Workspace ID is required");
2300
+ this.client = new ApiClient(baseUrl, privateKey, workspaceId, {
2114
2301
  defaultTenantId: options?.defaultTenantId,
2115
2302
  additionalHeaders: options?.additionalHeaders,
2116
2303
  fetch: options?.fetch
2117
2304
  });
2118
2305
  }
2119
- if (!organizationId) throw new Error("Organization ID is required");
2120
2306
  this.queryEngine = new QueryEngine();
2121
2307
  }
2122
2308
  /**
@@ -2125,7 +2311,7 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
2125
2311
  * are handled in-process (e.g. by invoking querypanel-sdk services directly)
2126
2312
  * and do not cause recursion.
2127
2313
  *
2128
- * @param organizationId - Organization identifier
2314
+ * @param workspaceId - Workspace identifier (sent as `organizationId` in JWTs and API auth)
2129
2315
  * @param requestHandler - Callback invoked for each logical API request (method, path, body, tenantId, etc.)
2130
2316
  * @param options - Optional defaultTenantId
2131
2317
  * @returns QueryPanelSdkAPI instance; attach databases and call ask(), syncSchema(), etc. as usual
@@ -2133,7 +2319,7 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
2133
2319
  * @example
2134
2320
  * ```ts
2135
2321
  * const qp = QueryPanelSdkAPI.withCallbacks(
2136
- * process.env.ORGANIZATION_ID!,
2322
+ * process.env.QUERYPANEL_WORKSPACE_ID!,
2137
2323
  * async (opts) => {
2138
2324
  * // Call your in-process services (e.g. querypanel-sdk logic) instead of HTTP
2139
2325
  * if (opts.path === '/query' || opts.path === '/v2/query') {
@@ -2150,11 +2336,11 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
2150
2336
  * const result = await qp.ask('Top 10 orders', { tenantId: 't1', database: 'db' });
2151
2337
  * ```
2152
2338
  */
2153
- static withCallbacks(organizationId, requestHandler, options) {
2339
+ static withCallbacks(workspaceId, requestHandler, options) {
2154
2340
  const api = new CallbackApiClient(requestHandler, {
2155
2341
  defaultTenantId: options?.defaultTenantId
2156
2342
  });
2157
- return new _QueryPanelSdkAPI("", "", organizationId, { api });
2343
+ return new _QueryPanelSdkAPI("", "", workspaceId, { api });
2158
2344
  }
2159
2345
  // Database attachment methods
2160
2346
  attachClickhouse(name, clientFn, options) {
@@ -2183,6 +2369,19 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
2183
2369
  };
2184
2370
  this.queryEngine.attachDatabase(name, adapter, metadata);
2185
2371
  }
2372
+ attachBigQuery(name, clientFn, options) {
2373
+ const adapter = new BigQueryAdapter(clientFn, options);
2374
+ const metadata = {
2375
+ name,
2376
+ dialect: "bigquery",
2377
+ description: options?.description,
2378
+ tags: options?.tags,
2379
+ tenantFieldName: options?.tenantFieldName,
2380
+ tenantFieldType: options?.tenantFieldType ?? "String",
2381
+ enforceTenantIsolation: options?.tenantFieldName ? options?.enforceTenantIsolation ?? true : void 0
2382
+ };
2383
+ this.queryEngine.attachDatabase(name, adapter, metadata);
2384
+ }
2186
2385
  attachDatabase(name, adapter) {
2187
2386
  const metadata = {
2188
2387
  name,
@@ -3043,6 +3242,7 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
3043
3242
  }
3044
3243
  };
3045
3244
  export {
3245
+ BigQueryAdapter,
3046
3246
  CallbackApiClient,
3047
3247
  ClickHouseAdapter,
3048
3248
  PostgresAdapter,