@querypanel/node-sdk 1.0.29 → 1.0.30

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,12 +1,5 @@
1
- // src/index.ts
2
- import { createHash, randomUUID } from "crypto";
3
- import { importPKCS8, SignJWT } from "jose";
4
-
5
1
  // src/utils/clickhouse.ts
6
2
  var WRAPPER_REGEX = /^(Nullable|LowCardinality|SimpleAggregateFunction)\((.+)\)$/i;
7
- function isNullableType(type) {
8
- return /Nullable\s*\(/i.test(type);
9
- }
10
3
  function unwrapTypeModifiers(type) {
11
4
  let current = type.trim();
12
5
  let match = WRAPPER_REGEX.exec(current);
@@ -20,26 +13,6 @@ function unwrapTypeModifiers(type) {
20
13
  }
21
14
  return current;
22
15
  }
23
- function extractPrecisionScale(type) {
24
- const unwrapped = unwrapTypeModifiers(type);
25
- const decimalMatch = unwrapped.match(/Decimal(?:\d+)?\((\d+)\s*,\s*(\d+)\)/i);
26
- if (!decimalMatch) return {};
27
- const precision = decimalMatch[1];
28
- const scale = decimalMatch[2];
29
- if (!precision || !scale) return {};
30
- return {
31
- precision: Number.parseInt(precision, 10),
32
- scale: Number.parseInt(scale, 10)
33
- };
34
- }
35
- function extractFixedStringLength(type) {
36
- const unwrapped = unwrapTypeModifiers(type);
37
- const match = unwrapped.match(/^(?:FixedString|StringFixed)\((\d+)\)$/i);
38
- if (!match) return void 0;
39
- const length = match[1];
40
- if (!length) return void 0;
41
- return Number.parseInt(length, 10);
42
- }
43
16
  function parseKeyExpression(expression) {
44
17
  if (!expression) return [];
45
18
  let value = expression.trim();
@@ -127,6 +100,10 @@ var ClickHouseAdapter = class {
127
100
  getDialect() {
128
101
  return "clickhouse";
129
102
  }
103
+ /**
104
+ * Simplified introspection: only collect table/column metadata for IngestRequest
105
+ * No indexes, constraints, or statistics
106
+ */
130
107
  async introspect(options) {
131
108
  const tablesToIntrospect = options?.tables ? normalizeTableFilter(options.tables) : this.allowedTables;
132
109
  const allowTables = tablesToIntrospect ?? [];
@@ -139,7 +116,7 @@ var ClickHouseAdapter = class {
139
116
  }
140
117
  const filterClause = hasFilter ? " AND name IN {tables:Array(String)}" : "";
141
118
  const tables = await this.query(
142
- `SELECT name, engine, comment, total_rows, total_bytes, primary_key, sorting_key
119
+ `SELECT name, engine, comment, primary_key
143
120
  FROM system.tables
144
121
  WHERE database = {db:String}${filterClause}
145
122
  ORDER BY name`,
@@ -147,7 +124,7 @@ var ClickHouseAdapter = class {
147
124
  );
148
125
  const columnFilterClause = hasFilter ? " AND table IN {tables:Array(String)}" : "";
149
126
  const columns = await this.query(
150
- `SELECT table, name, type, position, default_kind, default_expression, comment, is_in_primary_key
127
+ `SELECT table, name, type, position, comment, is_in_primary_key
151
128
  FROM system.columns
152
129
  WHERE database = {db:String}${columnFilterClause}
153
130
  ORDER BY table, position`,
@@ -162,44 +139,19 @@ var ClickHouseAdapter = class {
162
139
  const tableSchemas = tables.map((table) => {
163
140
  const tableColumns = columnsByTable.get(table.name) ?? [];
164
141
  const primaryKeyColumns = parseKeyExpression(table.primary_key);
165
- const totalRows = toNumber(table.total_rows);
166
- const totalBytes = toNumber(table.total_bytes);
167
142
  for (const column of tableColumns) {
168
143
  column.isPrimaryKey = column.isPrimaryKey || primaryKeyColumns.includes(column.name);
169
144
  }
170
- const indexes = primaryKeyColumns.length ? [
171
- {
172
- name: "primary_key",
173
- columns: primaryKeyColumns,
174
- unique: true,
175
- type: "PRIMARY KEY",
176
- ...table.primary_key ? { definition: table.primary_key } : {}
177
- }
178
- ] : [];
179
- const constraints = primaryKeyColumns.length ? [
180
- {
181
- name: "primary_key",
182
- type: "PRIMARY KEY",
183
- columns: primaryKeyColumns
184
- }
185
- ] : [];
186
145
  const base = {
187
146
  name: table.name,
188
147
  schema: this.databaseName,
189
148
  type: asTableType(table.engine),
190
- engine: table.engine,
191
- columns: tableColumns,
192
- indexes,
193
- constraints
149
+ columns: tableColumns
194
150
  };
195
151
  const comment = sanitize(table.comment);
196
152
  if (comment !== void 0) {
197
153
  base.comment = comment;
198
154
  }
199
- const statistics = buildTableStatistics(totalRows, totalBytes);
200
- if (statistics) {
201
- base.statistics = statistics;
202
- }
203
155
  return base;
204
156
  });
205
157
  return {
@@ -211,10 +163,6 @@ var ClickHouseAdapter = class {
211
163
  introspectedAt: (/* @__PURE__ */ new Date()).toISOString()
212
164
  };
213
165
  }
214
- /**
215
- * Validate that the SQL query only references allowed tables.
216
- * This is a basic validation that extracts table-like patterns from the query.
217
- */
218
166
  validateQueryTables(sql) {
219
167
  if (!this.allowedTables || this.allowedTables.length === 0) {
220
168
  return;
@@ -292,29 +240,15 @@ function normalizeTableFilter(tables) {
292
240
  return normalized;
293
241
  }
294
242
  function transformColumnRow(row) {
295
- const nullable = isNullableType(row.type);
296
243
  const unwrappedType = unwrapTypeModifiers(row.type);
297
- const { precision, scale } = extractPrecisionScale(row.type);
298
- const maxLength = extractFixedStringLength(row.type);
299
244
  const column = {
300
245
  name: row.name,
301
246
  type: unwrappedType,
302
247
  rawType: row.type,
303
- nullable,
304
- isPrimaryKey: Boolean(toNumber(row.is_in_primary_key)),
305
- isForeignKey: false
248
+ isPrimaryKey: Boolean(toNumber(row.is_in_primary_key))
306
249
  };
307
- const defaultKind = sanitize(row.default_kind);
308
- if (defaultKind !== void 0) column.defaultKind = defaultKind;
309
- const defaultExpression = sanitize(row.default_expression);
310
- if (defaultExpression !== void 0) {
311
- column.defaultExpression = defaultExpression;
312
- }
313
250
  const comment = sanitize(row.comment);
314
251
  if (comment !== void 0) column.comment = comment;
315
- if (maxLength !== void 0) column.maxLength = maxLength;
316
- if (precision !== void 0) column.precision = precision;
317
- if (scale !== void 0) column.scale = scale;
318
252
  return column;
319
253
  }
320
254
  function asTableType(engine) {
@@ -326,13 +260,6 @@ function asTableType(engine) {
326
260
  }
327
261
  return "table";
328
262
  }
329
- function buildTableStatistics(totalRows, totalBytes) {
330
- if (totalRows === void 0 && totalBytes === void 0) return void 0;
331
- const stats = {};
332
- if (totalRows !== void 0) stats.totalRows = totalRows;
333
- if (totalBytes !== void 0) stats.totalBytes = totalBytes;
334
- return stats;
335
- }
336
263
  function sanitize(value) {
337
264
  if (value === null || value === void 0) return void 0;
338
265
  const trimmed = String(value).trim();
@@ -375,10 +302,6 @@ var PostgresAdapter = class {
375
302
  const fields = result.fields.map((f) => f.name);
376
303
  return { fields, rows: result.rows };
377
304
  }
378
- /**
379
- * Validate that the SQL query only references allowed tables.
380
- * This is a basic validation that extracts table-like patterns from the query.
381
- */
382
305
  validateQueryTables(sql) {
383
306
  if (!this.allowedTables || this.allowedTables.length === 0) {
384
307
  return;
@@ -404,11 +327,6 @@ var PostgresAdapter = class {
404
327
  /**
405
328
  * Convert named params to positional array for PostgreSQL
406
329
  * PostgreSQL expects $1, $2, $3 in SQL and an array of values [val1, val2, val3]
407
- *
408
- * Supports two formats:
409
- * 1. Numeric keys: { '1': 'value1', '2': 'value2' } - maps directly to $1, $2
410
- * 2. Named keys: { 'tenant_id': 'value' } - values extracted in alphabetical order
411
- * 3. Mixed: { '1': 'value1', 'tenant_id': 'value' } - numeric keys first, then named keys
412
330
  */
413
331
  convertNamedToPositionalParams(params) {
414
332
  const numericKeys = Object.keys(params).filter((k) => /^\d+$/.test(k)).map((k) => Number.parseInt(k, 10)).sort((a, b) => a - b);
@@ -441,6 +359,10 @@ var PostgresAdapter = class {
441
359
  getDialect() {
442
360
  return "postgres";
443
361
  }
362
+ /**
363
+ * Simplified introspection: only collect table/column metadata for IngestRequest
364
+ * No indexes, constraints, or statistics
365
+ */
444
366
  async introspect(options) {
445
367
  const tablesToIntrospect = options?.tables ? normalizeTableFilter2(options.tables, this.defaultSchema) : this.allowedTables;
446
368
  const normalizedTables = tablesToIntrospect ?? [];
@@ -452,39 +374,20 @@ var PostgresAdapter = class {
452
374
  buildColumnsQuery(normalizedTables)
453
375
  );
454
376
  const columnRows = columnsResult.rows;
455
- const constraintsResult = await this.clientFn(
456
- buildConstraintsQuery(normalizedTables)
457
- );
458
- const constraintRows = constraintsResult.rows;
459
- const indexesResult = await this.clientFn(
460
- buildIndexesQuery(normalizedTables)
461
- );
462
- const indexRows = indexesResult.rows;
463
377
  const tablesByKey = /* @__PURE__ */ new Map();
464
- const columnsByKey = /* @__PURE__ */ new Map();
465
378
  for (const row of tableRows) {
466
379
  const key = tableKey(row.schema_name, row.table_name);
467
- const statistics = buildTableStatistics2(
468
- toNumber2(row.total_rows),
469
- toNumber2(row.total_bytes)
470
- );
471
380
  const table = {
472
381
  name: row.table_name,
473
382
  schema: row.schema_name,
474
383
  type: asTableType2(row.table_type),
475
- columns: [],
476
- indexes: [],
477
- constraints: []
384
+ columns: []
478
385
  };
479
386
  const comment = sanitize2(row.comment);
480
387
  if (comment !== void 0) {
481
388
  table.comment = comment;
482
389
  }
483
- if (statistics) {
484
- table.statistics = statistics;
485
- }
486
390
  tablesByKey.set(key, table);
487
- columnsByKey.set(key, /* @__PURE__ */ new Map());
488
391
  }
489
392
  for (const row of columnRows) {
490
393
  const key = tableKey(row.table_schema, row.table_name);
@@ -493,80 +396,13 @@ var PostgresAdapter = class {
493
396
  const column = {
494
397
  name: row.column_name,
495
398
  type: row.data_type,
496
- nullable: row.is_nullable.toUpperCase() === "YES",
497
- isPrimaryKey: false,
498
- isForeignKey: false
399
+ isPrimaryKey: row.is_primary_key
499
400
  };
500
401
  const rawType = row.udt_name ?? void 0;
501
402
  if (rawType !== void 0) column.rawType = rawType;
502
- const defaultExpression = sanitize2(row.column_default);
503
- if (defaultExpression !== void 0)
504
- column.defaultExpression = defaultExpression;
505
403
  const comment = sanitize2(row.description);
506
404
  if (comment !== void 0) column.comment = comment;
507
- const maxLength = row.character_maximum_length ?? void 0;
508
- if (maxLength !== void 0) column.maxLength = maxLength;
509
- const precision = row.numeric_precision ?? void 0;
510
- if (precision !== void 0) column.precision = precision;
511
- const scale = row.numeric_scale ?? void 0;
512
- if (scale !== void 0) column.scale = scale;
513
405
  table.columns.push(column);
514
- columnsByKey.get(key)?.set(row.column_name, column);
515
- }
516
- const constraintGroups = groupConstraints(constraintRows);
517
- for (const group of constraintGroups) {
518
- const key = tableKey(group.table_schema, group.table_name);
519
- const table = tablesByKey.get(key);
520
- if (!table) continue;
521
- const constraint = {
522
- name: group.constraint_name,
523
- type: group.constraint_type,
524
- columns: [...group.columns]
525
- };
526
- if (group.type === "FOREIGN KEY") {
527
- if (group.foreign_table_name) {
528
- const referencedTable = group.foreign_table_schema ? `${group.foreign_table_schema}.${group.foreign_table_name}` : group.foreign_table_name;
529
- constraint.referencedTable = referencedTable;
530
- }
531
- if (group.foreign_columns.length) {
532
- constraint.referencedColumns = [...group.foreign_columns];
533
- }
534
- }
535
- table.constraints.push(constraint);
536
- for (let index = 0; index < group.columns.length; index += 1) {
537
- const columnName = group.columns[index];
538
- if (!columnName) continue;
539
- const column = columnsByKey.get(key)?.get(columnName);
540
- if (!column) continue;
541
- if (group.type === "PRIMARY KEY") {
542
- column.isPrimaryKey = true;
543
- }
544
- if (group.type === "FOREIGN KEY") {
545
- column.isForeignKey = true;
546
- if (group.foreign_table_name) {
547
- column.foreignKeyTable = group.foreign_table_schema ? `${group.foreign_table_schema}.${group.foreign_table_name}` : group.foreign_table_name;
548
- }
549
- const referencedColumn = group.foreign_columns[index];
550
- if (referencedColumn) {
551
- column.foreignKeyColumn = referencedColumn;
552
- }
553
- }
554
- }
555
- }
556
- for (const row of indexRows) {
557
- const key = tableKey(row.schema_name, row.table_name);
558
- const table = tablesByKey.get(key);
559
- if (!table) continue;
560
- const columns = coerceStringArray(row.column_names).map((c) => c.trim()).filter(Boolean);
561
- const index = {
562
- name: row.index_name,
563
- columns,
564
- unique: Boolean(row.indisunique),
565
- type: columns.length === 1 ? "INDEX" : "COMPOSITE INDEX"
566
- };
567
- const definition = sanitize2(row.definition);
568
- if (definition !== void 0) index.definition = definition;
569
- table.indexes.push(index);
570
406
  }
571
407
  const tables = Array.from(tablesByKey.values()).sort((a, b) => {
572
408
  if (a.schema === b.schema) {
@@ -584,36 +420,6 @@ var PostgresAdapter = class {
584
420
  };
585
421
  }
586
422
  };
587
- function groupConstraints(rows) {
588
- const groups = /* @__PURE__ */ new Map();
589
- for (const row of rows) {
590
- const key = `${row.table_schema}.${row.table_name}.${row.constraint_name}`;
591
- let group = groups.get(key);
592
- if (!group) {
593
- group = {
594
- table_schema: row.table_schema,
595
- table_name: row.table_name,
596
- constraint_name: row.constraint_name,
597
- constraint_type: row.constraint_type,
598
- columns: [],
599
- foreign_columns: [],
600
- type: row.constraint_type
601
- };
602
- groups.set(key, group);
603
- }
604
- if (row.column_name) {
605
- group.columns.push(row.column_name);
606
- }
607
- if (row.constraint_type === "FOREIGN KEY") {
608
- group.foreign_table_schema = row.foreign_table_schema;
609
- group.foreign_table_name = row.foreign_table_name;
610
- if (row.foreign_column_name) {
611
- group.foreign_columns.push(row.foreign_column_name);
612
- }
613
- }
614
- }
615
- return Array.from(groups.values());
616
- }
617
423
  function normalizeTableFilter2(tables, defaultSchema) {
618
424
  if (!tables?.length) return [];
619
425
  const normalized = [];
@@ -646,9 +452,7 @@ function buildTablesQuery(tables) {
646
452
  WHEN 'm' THEN 'materialized_view'
647
453
  ELSE c.relkind::text
648
454
  END AS table_type,
649
- obj_description(c.oid) AS comment,
650
- c.reltuples AS total_rows,
651
- pg_total_relation_size(c.oid) AS total_bytes
455
+ obj_description(c.oid) AS comment
652
456
  FROM pg_class c
653
457
  JOIN pg_namespace n ON n.oid = c.relnamespace
654
458
  WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
@@ -668,13 +472,18 @@ function buildColumnsQuery(tables) {
668
472
  cols.column_name,
669
473
  cols.data_type,
670
474
  cols.udt_name,
671
- cols.is_nullable,
672
- cols.column_default,
673
- cols.character_maximum_length,
674
- cols.numeric_precision,
675
- cols.numeric_scale,
676
- cols.ordinal_position,
677
- pgd.description
475
+ pgd.description,
476
+ EXISTS(
477
+ SELECT 1
478
+ FROM information_schema.table_constraints tc
479
+ JOIN information_schema.key_column_usage kcu
480
+ ON tc.constraint_name = kcu.constraint_name
481
+ AND tc.table_schema = kcu.table_schema
482
+ WHERE tc.constraint_type = 'PRIMARY KEY'
483
+ AND tc.table_schema = cols.table_schema
484
+ AND tc.table_name = cols.table_name
485
+ AND kcu.column_name = cols.column_name
486
+ ) AS is_primary_key
678
487
  FROM information_schema.columns cols
679
488
  LEFT JOIN pg_catalog.pg_class c
680
489
  ON c.relname = cols.table_name
@@ -689,50 +498,6 @@ function buildColumnsQuery(tables) {
689
498
  ${filter}
690
499
  ORDER BY cols.table_schema, cols.table_name, cols.ordinal_position;`;
691
500
  }
692
- function buildConstraintsQuery(tables) {
693
- const filter = buildFilterClause(tables, "tc.table_schema", "tc.table_name");
694
- return `SELECT
695
- tc.table_schema,
696
- tc.table_name,
697
- tc.constraint_name,
698
- tc.constraint_type,
699
- kcu.column_name,
700
- ccu.table_schema AS foreign_table_schema,
701
- ccu.table_name AS foreign_table_name,
702
- ccu.column_name AS foreign_column_name
703
- FROM information_schema.table_constraints tc
704
- LEFT JOIN information_schema.key_column_usage kcu
705
- ON tc.constraint_name = kcu.constraint_name
706
- AND tc.table_schema = kcu.table_schema
707
- LEFT JOIN information_schema.constraint_column_usage ccu
708
- ON ccu.constraint_name = tc.constraint_name
709
- AND ccu.table_schema = tc.table_schema
710
- WHERE tc.constraint_type IN ('PRIMARY KEY', 'UNIQUE', 'FOREIGN KEY')
711
- AND tc.table_schema NOT IN ('pg_catalog', 'information_schema')
712
- ${filter}
713
- ORDER BY tc.table_schema, tc.table_name, tc.constraint_name, kcu.ordinal_position;`;
714
- }
715
- function buildIndexesQuery(tables) {
716
- const filter = buildFilterClause(tables, "n.nspname", "c.relname");
717
- return `SELECT
718
- n.nspname AS schema_name,
719
- c.relname AS table_name,
720
- ci.relname AS index_name,
721
- idx.indisunique,
722
- array_remove(
723
- array_agg(pg_get_indexdef(idx.indexrelid, g.k, true) ORDER BY g.k),
724
- NULL
725
- ) AS column_names,
726
- pg_get_indexdef(idx.indexrelid) AS definition
727
- FROM pg_class c
728
- JOIN pg_namespace n ON n.oid = c.relnamespace
729
- JOIN pg_index idx ON idx.indrelid = c.oid
730
- JOIN pg_class ci ON ci.oid = idx.indexrelid
731
- JOIN LATERAL generate_subscripts(idx.indkey, 1) AS g(k) ON true
732
- WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
733
- ${filter}
734
- GROUP BY n.nspname, c.relname, ci.relname, idx.indisunique, idx.indexrelid;`;
735
- }
736
501
  function buildFilterClause(tables, schemaExpr, tableExpr) {
737
502
  if (!tables.length) return "";
738
503
  const clauses = tables.map(({ schema, table }) => {
@@ -753,50 +518,21 @@ function asTableType2(value) {
753
518
  }
754
519
  return "table";
755
520
  }
756
- function buildTableStatistics2(totalRows, totalBytes) {
757
- if (totalRows === void 0 && totalBytes === void 0) return void 0;
758
- const stats = {};
759
- if (totalRows !== void 0) stats.totalRows = totalRows;
760
- if (totalBytes !== void 0) stats.totalBytes = totalBytes;
761
- return stats;
762
- }
763
521
  function sanitize2(value) {
764
522
  if (value === null || value === void 0) return void 0;
765
523
  const trimmed = String(value).trim();
766
524
  return trimmed.length ? trimmed : void 0;
767
525
  }
768
- function toNumber2(value) {
769
- if (value === null || value === void 0) return void 0;
770
- if (typeof value === "number") return value;
771
- const parsed = Number.parseFloat(String(value));
772
- return Number.isNaN(parsed) ? void 0 : parsed;
773
- }
774
- function coerceStringArray(value) {
775
- if (!value) return [];
776
- if (Array.isArray(value)) {
777
- return value.map((entry) => String(entry));
778
- }
779
- const text = String(value).trim();
780
- if (!text) return [];
781
- const withoutBraces = text.startsWith("{") && text.endsWith("}") ? text.slice(1, -1) : text;
782
- if (!withoutBraces) return [];
783
- return withoutBraces.split(",").map((part) => part.trim().replace(/^"(.+)"$/, "$1")).filter(Boolean);
784
- }
785
526
 
786
- // src/index.ts
787
- var QueryPanelSdkAPI = class {
527
+ // src/core/client.ts
528
+ import { createSign } from "crypto";
529
+ var ApiClient = class {
788
530
  baseUrl;
789
531
  privateKey;
790
532
  organizationId;
791
533
  defaultTenantId;
792
534
  additionalHeaders;
793
535
  fetchImpl;
794
- cachedPrivateKey;
795
- databases = /* @__PURE__ */ new Map();
796
- databaseMetadata = /* @__PURE__ */ new Map();
797
- defaultDatabase;
798
- lastSyncHashes = /* @__PURE__ */ new Map();
799
- syncedDatabases = /* @__PURE__ */ new Set();
800
536
  constructor(baseUrl, privateKey, organizationId, options) {
801
537
  if (!baseUrl) {
802
538
  throw new Error("Base URL is required");
@@ -819,561 +555,188 @@ var QueryPanelSdkAPI = class {
819
555
  );
820
556
  }
821
557
  }
822
- attachClickhouse(name, clientFn, options) {
823
- const adapter = new ClickHouseAdapter(clientFn, options);
824
- this.attachDatabase(name, adapter);
825
- const metadata = {
826
- name,
827
- dialect: "clickhouse",
828
- description: options?.description,
829
- tags: options?.tags,
830
- tenantFieldName: options?.tenantFieldName,
831
- tenantFieldType: options?.tenantFieldType ?? "String",
832
- enforceTenantIsolation: options?.tenantFieldName ? options?.enforceTenantIsolation ?? true : void 0
558
+ getDefaultTenantId() {
559
+ return this.defaultTenantId;
560
+ }
561
+ async get(path, tenantId, userId, scopes, signal, sessionId) {
562
+ return await this.request(path, {
563
+ method: "GET",
564
+ headers: await this.buildHeaders(
565
+ tenantId,
566
+ userId,
567
+ scopes,
568
+ false,
569
+ sessionId
570
+ ),
571
+ signal
572
+ });
573
+ }
574
+ async post(path, body, tenantId, userId, scopes, signal, sessionId) {
575
+ return await this.request(path, {
576
+ method: "POST",
577
+ headers: await this.buildHeaders(
578
+ tenantId,
579
+ userId,
580
+ scopes,
581
+ true,
582
+ sessionId
583
+ ),
584
+ body: JSON.stringify(body ?? {}),
585
+ signal
586
+ });
587
+ }
588
+ async put(path, body, tenantId, userId, scopes, signal, sessionId) {
589
+ return await this.request(path, {
590
+ method: "PUT",
591
+ headers: await this.buildHeaders(
592
+ tenantId,
593
+ userId,
594
+ scopes,
595
+ true,
596
+ sessionId
597
+ ),
598
+ body: JSON.stringify(body ?? {}),
599
+ signal
600
+ });
601
+ }
602
+ async delete(path, tenantId, userId, scopes, signal, sessionId) {
603
+ return await this.request(path, {
604
+ method: "DELETE",
605
+ headers: await this.buildHeaders(
606
+ tenantId,
607
+ userId,
608
+ scopes,
609
+ false,
610
+ sessionId
611
+ ),
612
+ signal
613
+ });
614
+ }
615
+ async request(path, init) {
616
+ const response = await this.fetchImpl(`${this.baseUrl}${path}`, init);
617
+ const text = await response.text();
618
+ let json;
619
+ try {
620
+ json = text ? JSON.parse(text) : void 0;
621
+ } catch {
622
+ json = void 0;
623
+ }
624
+ if (!response.ok) {
625
+ const error = new Error(
626
+ json?.error || response.statusText || "Request failed"
627
+ );
628
+ error.status = response.status;
629
+ if (json?.details) error.details = json.details;
630
+ throw error;
631
+ }
632
+ return json;
633
+ }
634
+ async buildHeaders(tenantId, userId, scopes, includeJson = true, sessionId) {
635
+ const token = await this.generateJWT(tenantId, userId, scopes);
636
+ const headers = {
637
+ Authorization: `Bearer ${token}`,
638
+ Accept: "application/json"
833
639
  };
834
- this.databaseMetadata.set(name, metadata);
640
+ if (includeJson) {
641
+ headers["Content-Type"] = "application/json";
642
+ }
643
+ if (sessionId) {
644
+ headers["x-session-id"] = sessionId;
645
+ }
646
+ if (this.additionalHeaders) {
647
+ Object.assign(headers, this.additionalHeaders);
648
+ }
649
+ return headers;
835
650
  }
836
- attachPostgres(name, clientFn, options) {
837
- const adapter = new PostgresAdapter(clientFn, options);
838
- this.attachDatabase(name, adapter);
839
- const metadata = {
840
- name,
841
- dialect: "postgres",
842
- description: options?.description,
843
- tags: options?.tags,
844
- tenantFieldName: options?.tenantFieldName,
845
- enforceTenantIsolation: options?.tenantFieldName ? options?.enforceTenantIsolation ?? true : void 0
651
+ async generateJWT(tenantId, userId, scopes) {
652
+ const header = {
653
+ alg: "RS256",
654
+ typ: "JWT"
846
655
  };
847
- this.databaseMetadata.set(name, metadata);
656
+ const payload = {
657
+ organizationId: this.organizationId,
658
+ tenantId
659
+ };
660
+ if (userId) payload.userId = userId;
661
+ if (scopes?.length) payload.scopes = scopes;
662
+ const encodeJson = (obj) => {
663
+ const json = JSON.stringify(obj);
664
+ const base64 = Buffer.from(json).toString("base64");
665
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
666
+ };
667
+ const encodedHeader = encodeJson(header);
668
+ const encodedPayload = encodeJson(payload);
669
+ const data = `${encodedHeader}.${encodedPayload}`;
670
+ const signer = createSign("RSA-SHA256");
671
+ signer.update(data);
672
+ signer.end();
673
+ const signature = signer.sign(this.privateKey);
674
+ const encodedSignature = signature.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
675
+ return `${data}.${encodedSignature}`;
848
676
  }
849
- attachDatabase(name, adapter) {
677
+ };
678
+
679
+ // src/core/query-engine.ts
680
+ var QueryEngine = class {
681
+ databases = /* @__PURE__ */ new Map();
682
+ databaseMetadata = /* @__PURE__ */ new Map();
683
+ defaultDatabase;
684
+ attachDatabase(name, adapter, metadata) {
850
685
  this.databases.set(name, adapter);
686
+ this.databaseMetadata.set(name, metadata);
851
687
  if (!this.defaultDatabase) {
852
688
  this.defaultDatabase = name;
853
689
  }
854
690
  }
855
- async syncSchema(databaseName, options, signal) {
856
- const tenantId = this.resolveTenantId(options.tenantId);
857
- const adapter = this.getDatabase(databaseName);
858
- const introspection = await adapter.introspect(
859
- options.tables ? { tables: options.tables } : void 0
860
- );
861
- const payload = this.buildSchemaRequest(
862
- databaseName,
863
- adapter,
864
- introspection
865
- );
866
- const hash = this.hashSchemaRequest(payload);
867
- const previousHash = this.lastSyncHashes.get(databaseName);
868
- if (!options.force && previousHash === hash) {
869
- return {
870
- success: true,
871
- message: "Schema unchanged; skipping ingestion",
872
- chunks: 0,
873
- chunks_with_annotations: 0,
874
- schema_hash: hash,
875
- skipped: true
876
- };
691
+ getDatabase(name) {
692
+ const dbName = name ?? this.defaultDatabase;
693
+ if (!dbName) {
694
+ throw new Error("No database attached.");
877
695
  }
878
- const sessionId = randomUUID();
879
- const response = await this.post(
880
- "/ingest",
881
- payload,
882
- tenantId,
883
- options.userId,
884
- options.scopes,
885
- signal,
886
- sessionId
887
- );
888
- this.lastSyncHashes.set(databaseName, hash);
889
- this.syncedDatabases.add(databaseName);
890
- return response;
891
- }
892
- async ingestKnowledgeBaseChunks(payload, options, signal) {
893
- const tenantId = this.resolveTenantId(
894
- payload.tenantId ?? options?.tenantId
895
- );
896
- return await this.post(
897
- "/knowledge-base/chunks",
898
- {
899
- organization_id: this.organizationId,
900
- tenant_id: tenantId,
901
- database: payload.database,
902
- dialect: payload.dialect,
903
- tables: payload.tables
904
- },
905
- tenantId,
906
- options?.userId,
907
- options?.scopes,
908
- signal
909
- );
910
- }
911
- async introspect(databaseName, tables) {
912
- const adapter = this.getDatabase(databaseName);
913
- return await adapter.introspect(tables ? { tables } : void 0);
914
- }
915
- async ask(question, options, signal) {
916
- const tenantId = this.resolveTenantId(options.tenantId);
917
- await this.ensureSchemasSynced(
918
- tenantId,
919
- options.userId,
920
- options.scopes,
921
- options.disableAutoSync
922
- );
923
- const sessionId = randomUUID();
924
- const queryResponse = await this.post(
925
- "/query",
926
- {
927
- question,
928
- ...options.lastError ? { last_error: options.lastError } : {},
929
- ...options.previousSql ? { previous_sql: options.previousSql } : {},
930
- ...options.maxRetry ? { max_retry: options.maxRetry } : {}
931
- },
932
- tenantId,
933
- options.userId,
934
- options.scopes,
935
- signal,
936
- sessionId
937
- );
938
- const databaseName = options.database ?? this.defaultDatabase;
939
- if (!databaseName) {
696
+ const adapter = this.databases.get(dbName);
697
+ if (!adapter) {
940
698
  throw new Error(
941
- "No database attached. Call attachPostgres/attachClickhouse first."
699
+ `Database '${dbName}' not found. Attached: ${Array.from(
700
+ this.databases.keys()
701
+ ).join(", ")}`
942
702
  );
943
703
  }
704
+ return adapter;
705
+ }
706
+ getDatabaseMetadata(name) {
707
+ const dbName = name ?? this.defaultDatabase;
708
+ if (!dbName) return void 0;
709
+ return this.databaseMetadata.get(dbName);
710
+ }
711
+ getDefaultDatabase() {
712
+ return this.defaultDatabase;
713
+ }
714
+ async validateAndExecute(sql, params, databaseName, tenantId) {
944
715
  const adapter = this.getDatabase(databaseName);
945
- const paramMetadata = Array.isArray(queryResponse.params) ? queryResponse.params : [];
946
- const paramValues = this.mapGeneratedParams(paramMetadata);
947
- const metadata = this.databaseMetadata.get(databaseName);
716
+ const metadata = this.getDatabaseMetadata(databaseName);
717
+ let finalSql = sql;
948
718
  if (metadata) {
949
- queryResponse.sql = this.ensureTenantIsolation(
950
- queryResponse.sql,
951
- paramValues,
952
- metadata,
953
- tenantId
954
- );
719
+ finalSql = this.ensureTenantIsolation(sql, params, metadata, tenantId);
955
720
  }
956
- await adapter.validate(queryResponse.sql, paramValues);
957
- const execution = await adapter.execute(queryResponse.sql, paramValues);
958
- const rows = execution.rows ?? [];
959
- let chart = {
960
- vegaLiteSpec: null,
961
- notes: rows.length === 0 ? "Query returned no rows." : null
721
+ await adapter.validate(finalSql, params);
722
+ const result = await adapter.execute(finalSql, params);
723
+ return {
724
+ rows: result.rows,
725
+ fields: result.fields
962
726
  };
963
- if (rows.length > 0) {
964
- const chartResponse = await this.post(
965
- "/chart",
966
- {
967
- question,
968
- sql: queryResponse.sql,
969
- rationale: queryResponse.rationale,
970
- fields: execution.fields,
971
- rows: anonymizeResults(rows),
972
- max_retries: options.chartMaxRetries ?? 3,
973
- query_id: queryResponse.queryId
974
- },
975
- tenantId,
976
- options.userId,
977
- options.scopes,
978
- signal,
979
- sessionId
727
+ }
728
+ async execute(sql, params, databaseName) {
729
+ try {
730
+ const adapter = this.getDatabase(databaseName);
731
+ const result = await adapter.execute(sql, params);
732
+ return result.rows;
733
+ } catch (error) {
734
+ console.warn(
735
+ `Failed to execute SQL locally for database '${databaseName}':`,
736
+ error
980
737
  );
981
- chart = {
982
- vegaLiteSpec: chartResponse.chart ? {
983
- ...chartResponse.chart,
984
- data: { values: rows }
985
- } : null,
986
- notes: chartResponse.notes
987
- };
738
+ return [];
988
739
  }
989
- return {
990
- sql: queryResponse.sql,
991
- params: paramValues,
992
- paramMetadata,
993
- rationale: queryResponse.rationale,
994
- dialect: queryResponse.dialect,
995
- queryId: queryResponse.queryId,
996
- rows,
997
- fields: execution.fields,
998
- chart,
999
- context: queryResponse.context
1000
- };
1001
- }
1002
- async upsertAnnotation(input, options, signal) {
1003
- const tenantId = this.resolveTenantId(input.tenantId ?? options?.tenantId);
1004
- const response = await this.post(
1005
- "/knowledge-base/annotations",
1006
- {
1007
- organization_id: this.organizationId,
1008
- tenant_id: tenantId,
1009
- target_identifier: input.targetIdentifier,
1010
- content: input.content,
1011
- user_id: input.userId
1012
- },
1013
- tenantId,
1014
- options?.userId,
1015
- options?.scopes,
1016
- signal
1017
- );
1018
- return response.annotation;
1019
- }
1020
- async listAnnotations(options, signal) {
1021
- const tenantId = this.resolveTenantId(options?.tenantId);
1022
- const response = await this.get(
1023
- "/knowledge-base/annotations",
1024
- tenantId,
1025
- options?.userId,
1026
- options?.scopes,
1027
- signal
1028
- );
1029
- return response.annotations;
1030
- }
1031
- async getAnnotation(targetIdentifier, options, signal) {
1032
- const tenantId = this.resolveTenantId(options?.tenantId);
1033
- const response = await this.get(
1034
- `/knowledge-base/annotations/${encodeURIComponent(targetIdentifier)}`,
1035
- tenantId,
1036
- options?.userId,
1037
- options?.scopes,
1038
- signal
1039
- ).catch((error) => {
1040
- if (error?.status === 404) {
1041
- return { success: false, annotation: null };
1042
- }
1043
- throw error;
1044
- });
1045
- return response.annotation ?? null;
1046
- }
1047
- async deleteAnnotation(targetIdentifier, options, signal) {
1048
- const tenantId = this.resolveTenantId(options?.tenantId);
1049
- await this.delete(
1050
- `/knowledge-base/annotations/${encodeURIComponent(targetIdentifier)}`,
1051
- tenantId,
1052
- options?.userId,
1053
- options?.scopes,
1054
- signal
1055
- );
1056
- }
1057
- async createChart(body, options, signal) {
1058
- const tenantId = this.resolveTenantId(options?.tenantId);
1059
- return await this.post(
1060
- "/charts",
1061
- body,
1062
- tenantId,
1063
- options?.userId,
1064
- options?.scopes,
1065
- signal
1066
- );
1067
- }
1068
- async listCharts(options, signal) {
1069
- const tenantId = this.resolveTenantId(options?.tenantId);
1070
- const params = new URLSearchParams();
1071
- if (options?.pagination?.page)
1072
- params.set("page", `${options.pagination.page}`);
1073
- if (options?.pagination?.limit)
1074
- params.set("limit", `${options.pagination.limit}`);
1075
- if (options?.sortBy) params.set("sort_by", options.sortBy);
1076
- if (options?.sortDir) params.set("sort_dir", options.sortDir);
1077
- if (options?.title) params.set("title", options.title);
1078
- if (options?.userFilter) params.set("user_id", options.userFilter);
1079
- if (options?.createdFrom) params.set("created_from", options.createdFrom);
1080
- if (options?.createdTo) params.set("created_to", options.createdTo);
1081
- if (options?.updatedFrom) params.set("updated_from", options.updatedFrom);
1082
- if (options?.updatedTo) params.set("updated_to", options.updatedTo);
1083
- const response = await this.get(
1084
- `/charts${params.toString() ? `?${params.toString()}` : ""}`,
1085
- tenantId,
1086
- options?.userId,
1087
- options?.scopes,
1088
- signal
1089
- );
1090
- if (options?.includeData) {
1091
- response.data = await Promise.all(
1092
- response.data.map(async (chart) => ({
1093
- ...chart,
1094
- vega_lite_spec: {
1095
- ...chart.vega_lite_spec,
1096
- data: {
1097
- values: await this.runSafeQueryOnClient(
1098
- chart.sql,
1099
- chart.database ?? void 0,
1100
- chart.sql_params ?? void 0
1101
- )
1102
- }
1103
- }
1104
- }))
1105
- );
1106
- }
1107
- return response;
1108
- }
1109
- async getChart(id, options, signal) {
1110
- const tenantId = this.resolveTenantId(options?.tenantId);
1111
- const chart = await this.get(
1112
- `/charts/${encodeURIComponent(id)}`,
1113
- tenantId,
1114
- options?.userId,
1115
- options?.scopes,
1116
- signal
1117
- );
1118
- return {
1119
- ...chart,
1120
- vega_lite_spec: {
1121
- ...chart.vega_lite_spec,
1122
- data: {
1123
- values: await this.runSafeQueryOnClient(
1124
- chart.sql,
1125
- chart.database ?? void 0,
1126
- chart.sql_params ?? void 0
1127
- )
1128
- }
1129
- }
1130
- };
1131
- }
1132
- async updateChart(id, body, options, signal) {
1133
- const tenantId = this.resolveTenantId(options?.tenantId);
1134
- return await this.put(
1135
- `/charts/${encodeURIComponent(id)}`,
1136
- body,
1137
- tenantId,
1138
- options?.userId,
1139
- options?.scopes,
1140
- signal
1141
- );
1142
- }
1143
- async deleteChart(id, options, signal) {
1144
- const tenantId = this.resolveTenantId(options?.tenantId);
1145
- await this.delete(
1146
- `/charts/${encodeURIComponent(id)}`,
1147
- tenantId,
1148
- options?.userId,
1149
- options?.scopes,
1150
- signal
1151
- );
1152
- }
1153
- async createActiveChart(body, options, signal) {
1154
- const tenantId = this.resolveTenantId(options?.tenantId);
1155
- return await this.post(
1156
- "/active-charts",
1157
- body,
1158
- tenantId,
1159
- options?.userId,
1160
- options?.scopes,
1161
- signal
1162
- );
1163
- }
1164
- async listActiveCharts(options, signal) {
1165
- const tenantId = this.resolveTenantId(options?.tenantId);
1166
- const params = new URLSearchParams();
1167
- if (options?.pagination?.page)
1168
- params.set("page", `${options.pagination.page}`);
1169
- if (options?.pagination?.limit)
1170
- params.set("limit", `${options.pagination.limit}`);
1171
- if (options?.sortBy) params.set("sort_by", options.sortBy);
1172
- if (options?.sortDir) params.set("sort_dir", options.sortDir);
1173
- if (options?.title) params.set("name", options.title);
1174
- if (options?.userFilter) params.set("user_id", options.userFilter);
1175
- if (options?.createdFrom) params.set("created_from", options.createdFrom);
1176
- if (options?.createdTo) params.set("created_to", options.createdTo);
1177
- if (options?.updatedFrom) params.set("updated_from", options.updatedFrom);
1178
- if (options?.updatedTo) params.set("updated_to", options.updatedTo);
1179
- const response = await this.get(
1180
- `/active-charts${params.toString() ? `?${params.toString()}` : ""}`,
1181
- tenantId,
1182
- options?.userId,
1183
- options?.scopes,
1184
- signal
1185
- );
1186
- if (options?.withData) {
1187
- response.data = await Promise.all(
1188
- response.data.map(async (active) => ({
1189
- ...active,
1190
- chart: active.chart ? await this.getChart(active.chart_id, options, signal) : null
1191
- }))
1192
- );
1193
- }
1194
- return response;
1195
- }
1196
- async getActiveChart(id, options, signal) {
1197
- const tenantId = this.resolveTenantId(options?.tenantId);
1198
- const active = await this.get(
1199
- `/active-charts/${encodeURIComponent(id)}`,
1200
- tenantId,
1201
- options?.userId,
1202
- options?.scopes,
1203
- signal
1204
- );
1205
- if (options?.withData && active.chart_id) {
1206
- return {
1207
- ...active,
1208
- chart: await this.getChart(active.chart_id, options, signal)
1209
- };
1210
- }
1211
- return active;
1212
- }
1213
- async updateActiveChart(id, body, options, signal) {
1214
- const tenantId = this.resolveTenantId(options?.tenantId);
1215
- return await this.put(
1216
- `/active-charts/${encodeURIComponent(id)}`,
1217
- body,
1218
- tenantId,
1219
- options?.userId,
1220
- options?.scopes,
1221
- signal
1222
- );
1223
- }
1224
- async deleteActiveChart(id, options, signal) {
1225
- const tenantId = this.resolveTenantId(options?.tenantId);
1226
- await this.delete(
1227
- `/active-charts/${encodeURIComponent(id)}`,
1228
- tenantId,
1229
- options?.userId,
1230
- options?.scopes,
1231
- signal
1232
- );
1233
- }
1234
- getDatabase(name) {
1235
- const dbName = name ?? this.defaultDatabase;
1236
- if (!dbName) {
1237
- throw new Error("No database attached.");
1238
- }
1239
- const adapter = this.databases.get(dbName);
1240
- if (!adapter) {
1241
- throw new Error(
1242
- `Database '${dbName}' not found. Attached: ${Array.from(
1243
- this.databases.keys()
1244
- ).join(", ")}`
1245
- );
1246
- }
1247
- return adapter;
1248
- }
1249
- async ensureSchemasSynced(tenantId, userId, scopes, disableAutoSync) {
1250
- if (disableAutoSync) return;
1251
- const unsynced = Array.from(this.databases.keys()).filter(
1252
- (name) => !this.syncedDatabases.has(name)
1253
- );
1254
- await Promise.all(
1255
- unsynced.map(
1256
- (name) => this.syncSchema(name, { tenantId, userId, scopes }).catch((error) => {
1257
- console.warn(`Failed to sync schema for ${name}:`, error);
1258
- })
1259
- )
1260
- );
1261
- }
1262
- resolveTenantId(tenantId) {
1263
- const resolved = tenantId ?? this.defaultTenantId;
1264
- if (!resolved) {
1265
- throw new Error(
1266
- "tenantId is required. Provide it per request or via defaultTenantId option."
1267
- );
1268
- }
1269
- return resolved;
1270
- }
1271
- async headers(tenantId, userId, scopes, includeJson = true, sessionId) {
1272
- const token = await this.generateJWT(tenantId, userId, scopes);
1273
- const headers = {
1274
- Authorization: `Bearer ${token}`,
1275
- Accept: "application/json"
1276
- };
1277
- if (includeJson) {
1278
- headers["Content-Type"] = "application/json";
1279
- }
1280
- if (sessionId) {
1281
- headers["x-session-id"] = sessionId;
1282
- }
1283
- if (this.additionalHeaders) {
1284
- Object.assign(headers, this.additionalHeaders);
1285
- }
1286
- return headers;
1287
- }
1288
- async request(path, init) {
1289
- const response = await this.fetchImpl(`${this.baseUrl}${path}`, init);
1290
- const text = await response.text();
1291
- let json;
1292
- try {
1293
- json = text ? JSON.parse(text) : void 0;
1294
- } catch {
1295
- json = void 0;
1296
- }
1297
- if (!response.ok) {
1298
- const error = new Error(
1299
- json?.error || response.statusText || "Request failed"
1300
- );
1301
- error.status = response.status;
1302
- if (json?.details) error.details = json.details;
1303
- throw error;
1304
- }
1305
- return json;
1306
- }
1307
- async get(path, tenantId, userId, scopes, signal, sessionId) {
1308
- return await this.request(path, {
1309
- method: "GET",
1310
- headers: await this.headers(tenantId, userId, scopes, false, sessionId),
1311
- signal
1312
- });
1313
- }
1314
- async post(path, body, tenantId, userId, scopes, signal, sessionId) {
1315
- return await this.request(path, {
1316
- method: "POST",
1317
- headers: await this.headers(tenantId, userId, scopes, true, sessionId),
1318
- body: JSON.stringify(body ?? {}),
1319
- signal
1320
- });
1321
- }
1322
- async put(path, body, tenantId, userId, scopes, signal, sessionId) {
1323
- return await this.request(path, {
1324
- method: "PUT",
1325
- headers: await this.headers(tenantId, userId, scopes, true, sessionId),
1326
- body: JSON.stringify(body ?? {}),
1327
- signal
1328
- });
1329
- }
1330
- async delete(path, tenantId, userId, scopes, signal, sessionId) {
1331
- return await this.request(path, {
1332
- method: "DELETE",
1333
- headers: await this.headers(tenantId, userId, scopes, false, sessionId),
1334
- signal
1335
- });
1336
- }
1337
- async generateJWT(tenantId, userId, scopes) {
1338
- if (!this.cachedPrivateKey) {
1339
- this.cachedPrivateKey = await importPKCS8(this.privateKey, "RS256");
1340
- }
1341
- const payload = {
1342
- organizationId: this.organizationId,
1343
- tenantId
1344
- };
1345
- if (userId) payload.userId = userId;
1346
- if (scopes?.length) payload.scopes = scopes;
1347
- return await new SignJWT(payload).setProtectedHeader({ alg: "RS256" }).setIssuedAt().setExpirationTime("1h").sign(this.cachedPrivateKey);
1348
- }
1349
- buildSchemaRequest(databaseName, adapter, introspection) {
1350
- const dialect = adapter.getDialect();
1351
- const tables = introspection.tables.map((table) => ({
1352
- table_name: table.name,
1353
- description: table.comment ?? `Table ${table.name}`,
1354
- columns: table.columns.map((column) => ({
1355
- name: column.name,
1356
- data_type: column.rawType ?? column.type,
1357
- is_primary_key: Boolean(column.isPrimaryKey),
1358
- description: column.comment ?? ""
1359
- }))
1360
- }));
1361
- return {
1362
- database: databaseName,
1363
- dialect,
1364
- tables
1365
- };
1366
- }
1367
- hashSchemaRequest(payload) {
1368
- const normalized = payload.tables.map((table) => ({
1369
- name: table.table_name,
1370
- columns: table.columns.map((column) => ({
1371
- name: column.name,
1372
- type: column.data_type,
1373
- primary: column.is_primary_key
1374
- }))
1375
- }));
1376
- return createHash("sha256").update(JSON.stringify(normalized)).digest("hex");
1377
740
  }
1378
741
  mapGeneratedParams(params) {
1379
742
  const record = {};
@@ -1408,20 +771,375 @@ var QueryPanelSdkAPI = class {
1408
771
  }
1409
772
  return `${sql} WHERE ${tenantPredicate}`;
1410
773
  }
1411
- async runSafeQueryOnClient(sql, database, params) {
774
+ };
775
+
776
+ // src/routes/charts.ts
777
+ async function createChart(client, body, options, signal) {
778
+ const tenantId = resolveTenantId(client, options?.tenantId);
779
+ return await client.post(
780
+ "/charts",
781
+ body,
782
+ tenantId,
783
+ options?.userId,
784
+ options?.scopes,
785
+ signal
786
+ );
787
+ }
788
+ async function listCharts(client, queryEngine, options, signal) {
789
+ const tenantId = resolveTenantId(client, options?.tenantId);
790
+ const params = new URLSearchParams();
791
+ if (options?.pagination?.page)
792
+ params.set("page", `${options.pagination.page}`);
793
+ if (options?.pagination?.limit)
794
+ params.set("limit", `${options.pagination.limit}`);
795
+ if (options?.sortBy) params.set("sort_by", options.sortBy);
796
+ if (options?.sortDir) params.set("sort_dir", options.sortDir);
797
+ if (options?.title) params.set("title", options.title);
798
+ if (options?.userFilter) params.set("user_id", options.userFilter);
799
+ if (options?.createdFrom) params.set("created_from", options.createdFrom);
800
+ if (options?.createdTo) params.set("created_to", options.createdTo);
801
+ if (options?.updatedFrom) params.set("updated_from", options.updatedFrom);
802
+ if (options?.updatedTo) params.set("updated_to", options.updatedTo);
803
+ const response = await client.get(
804
+ `/charts${params.toString() ? `?${params.toString()}` : ""}`,
805
+ tenantId,
806
+ options?.userId,
807
+ options?.scopes,
808
+ signal
809
+ );
810
+ if (options?.includeData) {
811
+ response.data = await Promise.all(
812
+ response.data.map(async (chart) => ({
813
+ ...chart,
814
+ vega_lite_spec: {
815
+ ...chart.vega_lite_spec,
816
+ data: {
817
+ values: await queryEngine.execute(
818
+ chart.sql,
819
+ chart.sql_params ?? void 0,
820
+ chart.target_db ?? void 0
821
+ )
822
+ }
823
+ }
824
+ }))
825
+ );
826
+ }
827
+ return response;
828
+ }
829
+ async function getChart(client, queryEngine, id, options, signal) {
830
+ const tenantId = resolveTenantId(client, options?.tenantId);
831
+ const chart = await client.get(
832
+ `/charts/${encodeURIComponent(id)}`,
833
+ tenantId,
834
+ options?.userId,
835
+ options?.scopes,
836
+ signal
837
+ );
838
+ return {
839
+ ...chart,
840
+ vega_lite_spec: {
841
+ ...chart.vega_lite_spec,
842
+ data: {
843
+ values: await queryEngine.execute(
844
+ chart.sql,
845
+ chart.sql_params ?? void 0,
846
+ chart.target_db ?? void 0
847
+ )
848
+ }
849
+ }
850
+ };
851
+ }
852
+ async function updateChart(client, id, body, options, signal) {
853
+ const tenantId = resolveTenantId(client, options?.tenantId);
854
+ return await client.put(
855
+ `/charts/${encodeURIComponent(id)}`,
856
+ body,
857
+ tenantId,
858
+ options?.userId,
859
+ options?.scopes,
860
+ signal
861
+ );
862
+ }
863
+ async function deleteChart(client, id, options, signal) {
864
+ const tenantId = resolveTenantId(client, options?.tenantId);
865
+ await client.delete(
866
+ `/charts/${encodeURIComponent(id)}`,
867
+ tenantId,
868
+ options?.userId,
869
+ options?.scopes,
870
+ signal
871
+ );
872
+ }
873
+ function resolveTenantId(client, tenantId) {
874
+ const resolved = tenantId ?? client.getDefaultTenantId();
875
+ if (!resolved) {
876
+ throw new Error(
877
+ "tenantId is required. Provide it per request or via defaultTenantId option."
878
+ );
879
+ }
880
+ return resolved;
881
+ }
882
+
883
+ // src/routes/active-charts.ts
884
+ async function createActiveChart(client, body, options, signal) {
885
+ const tenantId = resolveTenantId2(client, options?.tenantId);
886
+ return await client.post(
887
+ "/active-charts",
888
+ body,
889
+ tenantId,
890
+ options?.userId,
891
+ options?.scopes,
892
+ signal
893
+ );
894
+ }
895
+ async function listActiveCharts(client, queryEngine, options, signal) {
896
+ const tenantId = resolveTenantId2(client, options?.tenantId);
897
+ const params = new URLSearchParams();
898
+ if (options?.pagination?.page)
899
+ params.set("page", `${options.pagination.page}`);
900
+ if (options?.pagination?.limit)
901
+ params.set("limit", `${options.pagination.limit}`);
902
+ if (options?.sortBy) params.set("sort_by", options.sortBy);
903
+ if (options?.sortDir) params.set("sort_dir", options.sortDir);
904
+ if (options?.title) params.set("name", options.title);
905
+ if (options?.userFilter) params.set("user_id", options.userFilter);
906
+ if (options?.createdFrom) params.set("created_from", options.createdFrom);
907
+ if (options?.createdTo) params.set("created_to", options.createdTo);
908
+ if (options?.updatedFrom) params.set("updated_from", options.updatedFrom);
909
+ if (options?.updatedTo) params.set("updated_to", options.updatedTo);
910
+ const response = await client.get(
911
+ `/active-charts${params.toString() ? `?${params.toString()}` : ""}`,
912
+ tenantId,
913
+ options?.userId,
914
+ options?.scopes,
915
+ signal
916
+ );
917
+ if (options?.withData) {
918
+ response.data = await Promise.all(
919
+ response.data.map(async (active) => ({
920
+ ...active,
921
+ chart: active.chart ? await getChart(
922
+ client,
923
+ queryEngine,
924
+ active.chart_id,
925
+ options,
926
+ signal
927
+ ) : null
928
+ }))
929
+ );
930
+ }
931
+ return response;
932
+ }
933
+ async function getActiveChart(client, queryEngine, id, options, signal) {
934
+ const tenantId = resolveTenantId2(client, options?.tenantId);
935
+ const active = await client.get(
936
+ `/active-charts/${encodeURIComponent(id)}`,
937
+ tenantId,
938
+ options?.userId,
939
+ options?.scopes,
940
+ signal
941
+ );
942
+ if (options?.withData && active.chart_id) {
943
+ return {
944
+ ...active,
945
+ chart: await getChart(
946
+ client,
947
+ queryEngine,
948
+ active.chart_id,
949
+ options,
950
+ signal
951
+ )
952
+ };
953
+ }
954
+ return active;
955
+ }
956
+ async function updateActiveChart(client, id, body, options, signal) {
957
+ const tenantId = resolveTenantId2(client, options?.tenantId);
958
+ return await client.put(
959
+ `/active-charts/${encodeURIComponent(id)}`,
960
+ body,
961
+ tenantId,
962
+ options?.userId,
963
+ options?.scopes,
964
+ signal
965
+ );
966
+ }
967
+ async function deleteActiveChart(client, id, options, signal) {
968
+ const tenantId = resolveTenantId2(client, options?.tenantId);
969
+ await client.delete(
970
+ `/active-charts/${encodeURIComponent(id)}`,
971
+ tenantId,
972
+ options?.userId,
973
+ options?.scopes,
974
+ signal
975
+ );
976
+ }
977
+ function resolveTenantId2(client, tenantId) {
978
+ const resolved = tenantId ?? client.getDefaultTenantId();
979
+ if (!resolved) {
980
+ throw new Error(
981
+ "tenantId is required. Provide it per request or via defaultTenantId option."
982
+ );
983
+ }
984
+ return resolved;
985
+ }
986
+
987
+ // src/routes/ingest.ts
988
+ import { randomUUID } from "crypto";
989
+ async function syncSchema(client, queryEngine, databaseName, options, signal) {
990
+ const tenantId = resolveTenantId3(client, options.tenantId);
991
+ const adapter = queryEngine.getDatabase(databaseName);
992
+ const introspection = await adapter.introspect(
993
+ options.tables ? { tables: options.tables } : void 0
994
+ );
995
+ const payload = buildSchemaRequest(databaseName, adapter, introspection);
996
+ const sessionId = randomUUID();
997
+ const response = await client.post(
998
+ "/ingest",
999
+ payload,
1000
+ tenantId,
1001
+ options.userId,
1002
+ options.scopes,
1003
+ signal,
1004
+ sessionId
1005
+ );
1006
+ return response;
1007
+ }
1008
+ function resolveTenantId3(client, tenantId) {
1009
+ const resolved = tenantId ?? client.getDefaultTenantId();
1010
+ if (!resolved) {
1011
+ throw new Error(
1012
+ "tenantId is required. Provide it per request or via defaultTenantId option."
1013
+ );
1014
+ }
1015
+ return resolved;
1016
+ }
1017
+ function buildSchemaRequest(databaseName, adapter, introspection) {
1018
+ const dialect = adapter.getDialect();
1019
+ const tables = introspection.tables.map((table) => ({
1020
+ table_name: table.name,
1021
+ description: table.comment ?? `Table ${table.name}`,
1022
+ columns: table.columns.map((column) => ({
1023
+ name: column.name,
1024
+ data_type: column.rawType ?? column.type,
1025
+ is_primary_key: Boolean(column.isPrimaryKey),
1026
+ description: column.comment ?? ""
1027
+ }))
1028
+ }));
1029
+ return {
1030
+ database: databaseName,
1031
+ dialect,
1032
+ tables
1033
+ };
1034
+ }
1035
+
1036
+ // src/routes/query.ts
1037
+ import { randomUUID as randomUUID2 } from "crypto";
1038
+ async function ask(client, queryEngine, question, options, signal) {
1039
+ const tenantId = resolveTenantId4(client, options.tenantId);
1040
+ const sessionId = randomUUID2();
1041
+ const maxRetry = options.maxRetry ?? 0;
1042
+ let attempt = 0;
1043
+ let lastError = options.lastError;
1044
+ let previousSql = options.previousSql;
1045
+ while (attempt <= maxRetry) {
1046
+ console.log({ lastError, previousSql });
1047
+ const queryResponse = await client.post(
1048
+ "/query",
1049
+ {
1050
+ question,
1051
+ ...lastError ? { last_error: lastError } : {},
1052
+ ...previousSql ? { previous_sql: previousSql } : {},
1053
+ ...options.maxRetry ? { max_retry: options.maxRetry } : {}
1054
+ },
1055
+ tenantId,
1056
+ options.userId,
1057
+ options.scopes,
1058
+ signal,
1059
+ sessionId
1060
+ );
1061
+ const databaseName = queryResponse.database ?? options.database ?? queryEngine.getDefaultDatabase();
1062
+ if (!databaseName) {
1063
+ throw new Error(
1064
+ "No database attached. Call attachPostgres/attachClickhouse first."
1065
+ );
1066
+ }
1067
+ const paramMetadata = Array.isArray(queryResponse.params) ? queryResponse.params : [];
1068
+ const paramValues = queryEngine.mapGeneratedParams(paramMetadata);
1412
1069
  try {
1413
- const adapter = this.getDatabase(database);
1414
- const result = await adapter.execute(sql, params);
1415
- return result.rows;
1070
+ const execution = await queryEngine.validateAndExecute(
1071
+ queryResponse.sql,
1072
+ paramValues,
1073
+ databaseName,
1074
+ tenantId
1075
+ );
1076
+ const rows = execution.rows ?? [];
1077
+ let chart = {
1078
+ vegaLiteSpec: null,
1079
+ notes: rows.length === 0 ? "Query returned no rows." : null
1080
+ };
1081
+ if (rows.length > 0) {
1082
+ const chartResponse = await client.post(
1083
+ "/chart",
1084
+ {
1085
+ question,
1086
+ sql: queryResponse.sql,
1087
+ rationale: queryResponse.rationale,
1088
+ fields: execution.fields,
1089
+ rows: anonymizeResults(rows),
1090
+ max_retries: options.chartMaxRetries ?? 3,
1091
+ query_id: queryResponse.queryId
1092
+ },
1093
+ tenantId,
1094
+ options.userId,
1095
+ options.scopes,
1096
+ signal,
1097
+ sessionId
1098
+ );
1099
+ chart = {
1100
+ vegaLiteSpec: chartResponse.chart ? {
1101
+ ...chartResponse.chart,
1102
+ data: { values: rows }
1103
+ } : null,
1104
+ notes: chartResponse.notes
1105
+ };
1106
+ }
1107
+ return {
1108
+ sql: queryResponse.sql,
1109
+ params: paramValues,
1110
+ paramMetadata,
1111
+ rationale: queryResponse.rationale,
1112
+ dialect: queryResponse.dialect,
1113
+ queryId: queryResponse.queryId,
1114
+ rows,
1115
+ fields: execution.fields,
1116
+ chart,
1117
+ context: queryResponse.context,
1118
+ attempts: attempt + 1
1119
+ };
1416
1120
  } catch (error) {
1121
+ attempt++;
1122
+ if (attempt > maxRetry) {
1123
+ throw error;
1124
+ }
1125
+ lastError = error instanceof Error ? error.message : String(error);
1126
+ previousSql = queryResponse.sql;
1417
1127
  console.warn(
1418
- `Failed to execute SQL locally for database '${database}':`,
1419
- error
1128
+ `SQL execution failed (attempt ${attempt}/${maxRetry + 1}): ${lastError}. Retrying...`
1420
1129
  );
1421
- return [];
1422
1130
  }
1423
1131
  }
1424
- };
1132
+ throw new Error("Unexpected error in ask retry loop");
1133
+ }
1134
+ function resolveTenantId4(client, tenantId) {
1135
+ const resolved = tenantId ?? client.getDefaultTenantId();
1136
+ if (!resolved) {
1137
+ throw new Error(
1138
+ "tenantId is required. Provide it per request or via defaultTenantId option."
1139
+ );
1140
+ }
1141
+ return resolved;
1142
+ }
1425
1143
  function anonymizeResults(rows) {
1426
1144
  if (!rows?.length) return [];
1427
1145
  return rows.map((row) => {
@@ -1434,6 +1152,144 @@ function anonymizeResults(rows) {
1434
1152
  return masked;
1435
1153
  });
1436
1154
  }
1155
+
1156
+ // src/index.ts
1157
+ var QueryPanelSdkAPI = class {
1158
+ client;
1159
+ queryEngine;
1160
+ constructor(baseUrl, privateKey, organizationId, options) {
1161
+ this.client = new ApiClient(baseUrl, privateKey, organizationId, options);
1162
+ this.queryEngine = new QueryEngine();
1163
+ }
1164
+ // Database attachment methods
1165
+ attachClickhouse(name, clientFn, options) {
1166
+ const adapter = new ClickHouseAdapter(clientFn, options);
1167
+ const metadata = {
1168
+ name,
1169
+ dialect: "clickhouse",
1170
+ description: options?.description,
1171
+ tags: options?.tags,
1172
+ tenantFieldName: options?.tenantFieldName,
1173
+ tenantFieldType: options?.tenantFieldType ?? "String",
1174
+ enforceTenantIsolation: options?.tenantFieldName ? options?.enforceTenantIsolation ?? true : void 0
1175
+ };
1176
+ this.queryEngine.attachDatabase(name, adapter, metadata);
1177
+ }
1178
+ attachPostgres(name, clientFn, options) {
1179
+ const adapter = new PostgresAdapter(clientFn, options);
1180
+ const metadata = {
1181
+ name,
1182
+ dialect: "postgres",
1183
+ description: options?.description,
1184
+ tags: options?.tags,
1185
+ tenantFieldName: options?.tenantFieldName,
1186
+ enforceTenantIsolation: options?.tenantFieldName ? options?.enforceTenantIsolation ?? true : void 0
1187
+ };
1188
+ this.queryEngine.attachDatabase(name, adapter, metadata);
1189
+ }
1190
+ attachDatabase(name, adapter) {
1191
+ const metadata = {
1192
+ name,
1193
+ dialect: adapter.getDialect()
1194
+ };
1195
+ this.queryEngine.attachDatabase(name, adapter, metadata);
1196
+ }
1197
+ // Schema introspection and sync
1198
+ async introspect(databaseName, tables) {
1199
+ const adapter = this.queryEngine.getDatabase(databaseName);
1200
+ return await adapter.introspect(tables ? { tables } : void 0);
1201
+ }
1202
+ async syncSchema(databaseName, options, signal) {
1203
+ return await syncSchema(
1204
+ this.client,
1205
+ this.queryEngine,
1206
+ databaseName,
1207
+ options,
1208
+ signal
1209
+ );
1210
+ }
1211
+ // Natural language query
1212
+ async ask(question, options, signal) {
1213
+ return await ask(
1214
+ this.client,
1215
+ this.queryEngine,
1216
+ question,
1217
+ options,
1218
+ signal
1219
+ );
1220
+ }
1221
+ // Chart CRUD operations
1222
+ async createChart(body, options, signal) {
1223
+ return await createChart(this.client, body, options, signal);
1224
+ }
1225
+ async listCharts(options, signal) {
1226
+ return await listCharts(
1227
+ this.client,
1228
+ this.queryEngine,
1229
+ options,
1230
+ signal
1231
+ );
1232
+ }
1233
+ async getChart(id, options, signal) {
1234
+ return await getChart(
1235
+ this.client,
1236
+ this.queryEngine,
1237
+ id,
1238
+ options,
1239
+ signal
1240
+ );
1241
+ }
1242
+ async updateChart(id, body, options, signal) {
1243
+ return await updateChart(
1244
+ this.client,
1245
+ id,
1246
+ body,
1247
+ options,
1248
+ signal
1249
+ );
1250
+ }
1251
+ async deleteChart(id, options, signal) {
1252
+ await deleteChart(this.client, id, options, signal);
1253
+ }
1254
+ // Active Chart CRUD operations
1255
+ async createActiveChart(body, options, signal) {
1256
+ return await createActiveChart(
1257
+ this.client,
1258
+ body,
1259
+ options,
1260
+ signal
1261
+ );
1262
+ }
1263
+ async listActiveCharts(options, signal) {
1264
+ return await listActiveCharts(
1265
+ this.client,
1266
+ this.queryEngine,
1267
+ options,
1268
+ signal
1269
+ );
1270
+ }
1271
+ async getActiveChart(id, options, signal) {
1272
+ return await getActiveChart(
1273
+ this.client,
1274
+ this.queryEngine,
1275
+ id,
1276
+ options,
1277
+ signal
1278
+ );
1279
+ }
1280
+ async updateActiveChart(id, body, options, signal) {
1281
+ return await updateActiveChart(
1282
+ this.client,
1283
+ id,
1284
+ body,
1285
+ options,
1286
+ signal
1287
+ );
1288
+ }
1289
+ async deleteActiveChart(id, options, signal) {
1290
+ await deleteActiveChart(this.client, id, options, signal);
1291
+ }
1292
+ };
1437
1293
  export {
1438
1294
  ClickHouseAdapter,
1439
1295
  PostgresAdapter,