@querypanel/node-sdk 1.0.57 → 1.2.0

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}'`;
@@ -1562,7 +1745,7 @@ async function ask(client, queryEngine, question, options, signal) {
1562
1745
  const dbName = queryResponse.data.database ?? options.database ?? queryEngine.getDefaultDatabase();
1563
1746
  if (!dbName) {
1564
1747
  throw new Error(
1565
- "No database attached. Call attachPostgres/attachClickhouse first."
1748
+ "No database attached. Call attachPostgres, attachClickhouse, or attachBigQuery first."
1566
1749
  );
1567
1750
  }
1568
1751
  const paramMetadata = Array.isArray(queryResponse.data.params) ? queryResponse.data.params : [];
@@ -2103,20 +2286,22 @@ function resolveTenantId8(client, tenantId) {
2103
2286
  var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
2104
2287
  client;
2105
2288
  queryEngine;
2106
- constructor(baseUrl, privateKey, organizationId, options) {
2289
+ /**
2290
+ * @param workspaceId - Workspace UUID (same value as your org in the dashboard). Passed to the API as JWT claim `organizationId`.
2291
+ */
2292
+ constructor(baseUrl, privateKey, workspaceId, options) {
2107
2293
  if (options?.api) {
2108
2294
  this.client = options.api;
2109
2295
  } else {
2110
2296
  if (!baseUrl) throw new Error("Base URL is required");
2111
2297
  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, {
2298
+ if (!workspaceId) throw new Error("Workspace ID is required");
2299
+ this.client = new ApiClient(baseUrl, privateKey, workspaceId, {
2114
2300
  defaultTenantId: options?.defaultTenantId,
2115
2301
  additionalHeaders: options?.additionalHeaders,
2116
2302
  fetch: options?.fetch
2117
2303
  });
2118
2304
  }
2119
- if (!organizationId) throw new Error("Organization ID is required");
2120
2305
  this.queryEngine = new QueryEngine();
2121
2306
  }
2122
2307
  /**
@@ -2125,7 +2310,7 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
2125
2310
  * are handled in-process (e.g. by invoking querypanel-sdk services directly)
2126
2311
  * and do not cause recursion.
2127
2312
  *
2128
- * @param organizationId - Organization identifier
2313
+ * @param workspaceId - Workspace identifier (sent as `organizationId` in JWTs and API auth)
2129
2314
  * @param requestHandler - Callback invoked for each logical API request (method, path, body, tenantId, etc.)
2130
2315
  * @param options - Optional defaultTenantId
2131
2316
  * @returns QueryPanelSdkAPI instance; attach databases and call ask(), syncSchema(), etc. as usual
@@ -2133,7 +2318,7 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
2133
2318
  * @example
2134
2319
  * ```ts
2135
2320
  * const qp = QueryPanelSdkAPI.withCallbacks(
2136
- * process.env.ORGANIZATION_ID!,
2321
+ * process.env.QUERYPANEL_WORKSPACE_ID!,
2137
2322
  * async (opts) => {
2138
2323
  * // Call your in-process services (e.g. querypanel-sdk logic) instead of HTTP
2139
2324
  * if (opts.path === '/query' || opts.path === '/v2/query') {
@@ -2150,11 +2335,11 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
2150
2335
  * const result = await qp.ask('Top 10 orders', { tenantId: 't1', database: 'db' });
2151
2336
  * ```
2152
2337
  */
2153
- static withCallbacks(organizationId, requestHandler, options) {
2338
+ static withCallbacks(workspaceId, requestHandler, options) {
2154
2339
  const api = new CallbackApiClient(requestHandler, {
2155
2340
  defaultTenantId: options?.defaultTenantId
2156
2341
  });
2157
- return new _QueryPanelSdkAPI("", "", organizationId, { api });
2342
+ return new _QueryPanelSdkAPI("", "", workspaceId, { api });
2158
2343
  }
2159
2344
  // Database attachment methods
2160
2345
  attachClickhouse(name, clientFn, options) {
@@ -2183,6 +2368,19 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
2183
2368
  };
2184
2369
  this.queryEngine.attachDatabase(name, adapter, metadata);
2185
2370
  }
2371
+ attachBigQuery(name, clientFn, options) {
2372
+ const adapter = new BigQueryAdapter(clientFn, options);
2373
+ const metadata = {
2374
+ name,
2375
+ dialect: "bigquery",
2376
+ description: options?.description,
2377
+ tags: options?.tags,
2378
+ tenantFieldName: options?.tenantFieldName,
2379
+ tenantFieldType: options?.tenantFieldType ?? "String",
2380
+ enforceTenantIsolation: options?.tenantFieldName ? options?.enforceTenantIsolation ?? true : void 0
2381
+ };
2382
+ this.queryEngine.attachDatabase(name, adapter, metadata);
2383
+ }
2186
2384
  attachDatabase(name, adapter) {
2187
2385
  const metadata = {
2188
2386
  name,
@@ -3043,6 +3241,7 @@ var QueryPanelSdkAPI = class _QueryPanelSdkAPI {
3043
3241
  }
3044
3242
  };
3045
3243
  export {
3244
+ BigQueryAdapter,
3046
3245
  CallbackApiClient,
3047
3246
  ClickHouseAdapter,
3048
3247
  PostgresAdapter,