@querypanel/node-sdk 1.0.27 → 1.0.29

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,5 +1,12 @@
1
+ // src/index.ts
2
+ import { createHash, randomUUID } from "crypto";
3
+ import { importPKCS8, SignJWT } from "jose";
4
+
1
5
  // src/utils/clickhouse.ts
2
6
  var WRAPPER_REGEX = /^(Nullable|LowCardinality|SimpleAggregateFunction)\((.+)\)$/i;
7
+ function isNullableType(type) {
8
+ return /Nullable\s*\(/i.test(type);
9
+ }
3
10
  function unwrapTypeModifiers(type) {
4
11
  let current = type.trim();
5
12
  let match = WRAPPER_REGEX.exec(current);
@@ -13,6 +20,26 @@ function unwrapTypeModifiers(type) {
13
20
  }
14
21
  return current;
15
22
  }
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
+ }
16
43
  function parseKeyExpression(expression) {
17
44
  if (!expression) return [];
18
45
  let value = expression.trim();
@@ -100,10 +127,6 @@ var ClickHouseAdapter = class {
100
127
  getDialect() {
101
128
  return "clickhouse";
102
129
  }
103
- /**
104
- * Simplified introspection: only collect table/column metadata for IngestRequest
105
- * No indexes, constraints, or statistics
106
- */
107
130
  async introspect(options) {
108
131
  const tablesToIntrospect = options?.tables ? normalizeTableFilter(options.tables) : this.allowedTables;
109
132
  const allowTables = tablesToIntrospect ?? [];
@@ -116,7 +139,7 @@ var ClickHouseAdapter = class {
116
139
  }
117
140
  const filterClause = hasFilter ? " AND name IN {tables:Array(String)}" : "";
118
141
  const tables = await this.query(
119
- `SELECT name, engine, comment, primary_key
142
+ `SELECT name, engine, comment, total_rows, total_bytes, primary_key, sorting_key
120
143
  FROM system.tables
121
144
  WHERE database = {db:String}${filterClause}
122
145
  ORDER BY name`,
@@ -124,7 +147,7 @@ var ClickHouseAdapter = class {
124
147
  );
125
148
  const columnFilterClause = hasFilter ? " AND table IN {tables:Array(String)}" : "";
126
149
  const columns = await this.query(
127
- `SELECT table, name, type, position, comment, is_in_primary_key
150
+ `SELECT table, name, type, position, default_kind, default_expression, comment, is_in_primary_key
128
151
  FROM system.columns
129
152
  WHERE database = {db:String}${columnFilterClause}
130
153
  ORDER BY table, position`,
@@ -139,19 +162,44 @@ var ClickHouseAdapter = class {
139
162
  const tableSchemas = tables.map((table) => {
140
163
  const tableColumns = columnsByTable.get(table.name) ?? [];
141
164
  const primaryKeyColumns = parseKeyExpression(table.primary_key);
165
+ const totalRows = toNumber(table.total_rows);
166
+ const totalBytes = toNumber(table.total_bytes);
142
167
  for (const column of tableColumns) {
143
168
  column.isPrimaryKey = column.isPrimaryKey || primaryKeyColumns.includes(column.name);
144
169
  }
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
+ ] : [];
145
186
  const base = {
146
187
  name: table.name,
147
188
  schema: this.databaseName,
148
189
  type: asTableType(table.engine),
149
- columns: tableColumns
190
+ engine: table.engine,
191
+ columns: tableColumns,
192
+ indexes,
193
+ constraints
150
194
  };
151
195
  const comment = sanitize(table.comment);
152
196
  if (comment !== void 0) {
153
197
  base.comment = comment;
154
198
  }
199
+ const statistics = buildTableStatistics(totalRows, totalBytes);
200
+ if (statistics) {
201
+ base.statistics = statistics;
202
+ }
155
203
  return base;
156
204
  });
157
205
  return {
@@ -163,6 +211,10 @@ var ClickHouseAdapter = class {
163
211
  introspectedAt: (/* @__PURE__ */ new Date()).toISOString()
164
212
  };
165
213
  }
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
+ */
166
218
  validateQueryTables(sql) {
167
219
  if (!this.allowedTables || this.allowedTables.length === 0) {
168
220
  return;
@@ -240,15 +292,29 @@ function normalizeTableFilter(tables) {
240
292
  return normalized;
241
293
  }
242
294
  function transformColumnRow(row) {
295
+ const nullable = isNullableType(row.type);
243
296
  const unwrappedType = unwrapTypeModifiers(row.type);
297
+ const { precision, scale } = extractPrecisionScale(row.type);
298
+ const maxLength = extractFixedStringLength(row.type);
244
299
  const column = {
245
300
  name: row.name,
246
301
  type: unwrappedType,
247
302
  rawType: row.type,
248
- isPrimaryKey: Boolean(toNumber(row.is_in_primary_key))
303
+ nullable,
304
+ isPrimaryKey: Boolean(toNumber(row.is_in_primary_key)),
305
+ isForeignKey: false
249
306
  };
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
+ }
250
313
  const comment = sanitize(row.comment);
251
314
  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;
252
318
  return column;
253
319
  }
254
320
  function asTableType(engine) {
@@ -260,6 +326,13 @@ function asTableType(engine) {
260
326
  }
261
327
  return "table";
262
328
  }
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
+ }
263
336
  function sanitize(value) {
264
337
  if (value === null || value === void 0) return void 0;
265
338
  const trimmed = String(value).trim();
@@ -302,6 +375,10 @@ var PostgresAdapter = class {
302
375
  const fields = result.fields.map((f) => f.name);
303
376
  return { fields, rows: result.rows };
304
377
  }
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
+ */
305
382
  validateQueryTables(sql) {
306
383
  if (!this.allowedTables || this.allowedTables.length === 0) {
307
384
  return;
@@ -327,6 +404,11 @@ var PostgresAdapter = class {
327
404
  /**
328
405
  * Convert named params to positional array for PostgreSQL
329
406
  * 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
330
412
  */
331
413
  convertNamedToPositionalParams(params) {
332
414
  const numericKeys = Object.keys(params).filter((k) => /^\d+$/.test(k)).map((k) => Number.parseInt(k, 10)).sort((a, b) => a - b);
@@ -359,10 +441,6 @@ var PostgresAdapter = class {
359
441
  getDialect() {
360
442
  return "postgres";
361
443
  }
362
- /**
363
- * Simplified introspection: only collect table/column metadata for IngestRequest
364
- * No indexes, constraints, or statistics
365
- */
366
444
  async introspect(options) {
367
445
  const tablesToIntrospect = options?.tables ? normalizeTableFilter2(options.tables, this.defaultSchema) : this.allowedTables;
368
446
  const normalizedTables = tablesToIntrospect ?? [];
@@ -374,20 +452,39 @@ var PostgresAdapter = class {
374
452
  buildColumnsQuery(normalizedTables)
375
453
  );
376
454
  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;
377
463
  const tablesByKey = /* @__PURE__ */ new Map();
464
+ const columnsByKey = /* @__PURE__ */ new Map();
378
465
  for (const row of tableRows) {
379
466
  const key = tableKey(row.schema_name, row.table_name);
467
+ const statistics = buildTableStatistics2(
468
+ toNumber2(row.total_rows),
469
+ toNumber2(row.total_bytes)
470
+ );
380
471
  const table = {
381
472
  name: row.table_name,
382
473
  schema: row.schema_name,
383
474
  type: asTableType2(row.table_type),
384
- columns: []
475
+ columns: [],
476
+ indexes: [],
477
+ constraints: []
385
478
  };
386
479
  const comment = sanitize2(row.comment);
387
480
  if (comment !== void 0) {
388
481
  table.comment = comment;
389
482
  }
483
+ if (statistics) {
484
+ table.statistics = statistics;
485
+ }
390
486
  tablesByKey.set(key, table);
487
+ columnsByKey.set(key, /* @__PURE__ */ new Map());
391
488
  }
392
489
  for (const row of columnRows) {
393
490
  const key = tableKey(row.table_schema, row.table_name);
@@ -396,13 +493,80 @@ var PostgresAdapter = class {
396
493
  const column = {
397
494
  name: row.column_name,
398
495
  type: row.data_type,
399
- isPrimaryKey: row.is_primary_key
496
+ nullable: row.is_nullable.toUpperCase() === "YES",
497
+ isPrimaryKey: false,
498
+ isForeignKey: false
400
499
  };
401
500
  const rawType = row.udt_name ?? void 0;
402
501
  if (rawType !== void 0) column.rawType = rawType;
502
+ const defaultExpression = sanitize2(row.column_default);
503
+ if (defaultExpression !== void 0)
504
+ column.defaultExpression = defaultExpression;
403
505
  const comment = sanitize2(row.description);
404
506
  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;
405
513
  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);
406
570
  }
407
571
  const tables = Array.from(tablesByKey.values()).sort((a, b) => {
408
572
  if (a.schema === b.schema) {
@@ -420,6 +584,36 @@ var PostgresAdapter = class {
420
584
  };
421
585
  }
422
586
  };
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
+ }
423
617
  function normalizeTableFilter2(tables, defaultSchema) {
424
618
  if (!tables?.length) return [];
425
619
  const normalized = [];
@@ -452,7 +646,9 @@ function buildTablesQuery(tables) {
452
646
  WHEN 'm' THEN 'materialized_view'
453
647
  ELSE c.relkind::text
454
648
  END AS table_type,
455
- obj_description(c.oid) AS comment
649
+ obj_description(c.oid) AS comment,
650
+ c.reltuples AS total_rows,
651
+ pg_total_relation_size(c.oid) AS total_bytes
456
652
  FROM pg_class c
457
653
  JOIN pg_namespace n ON n.oid = c.relnamespace
458
654
  WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
@@ -472,18 +668,13 @@ function buildColumnsQuery(tables) {
472
668
  cols.column_name,
473
669
  cols.data_type,
474
670
  cols.udt_name,
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
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
487
678
  FROM information_schema.columns cols
488
679
  LEFT JOIN pg_catalog.pg_class c
489
680
  ON c.relname = cols.table_name
@@ -498,6 +689,50 @@ function buildColumnsQuery(tables) {
498
689
  ${filter}
499
690
  ORDER BY cols.table_schema, cols.table_name, cols.ordinal_position;`;
500
691
  }
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
+ }
501
736
  function buildFilterClause(tables, schemaExpr, tableExpr) {
502
737
  if (!tables.length) return "";
503
738
  const clauses = tables.map(({ schema, table }) => {
@@ -518,15 +753,38 @@ function asTableType2(value) {
518
753
  }
519
754
  return "table";
520
755
  }
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
+ }
521
763
  function sanitize2(value) {
522
764
  if (value === null || value === void 0) return void 0;
523
765
  const trimmed = String(value).trim();
524
766
  return trimmed.length ? trimmed : void 0;
525
767
  }
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
+ }
526
785
 
527
- // src/core/client.ts
528
- import { importPKCS8, SignJWT } from "jose";
529
- var ApiClient = class {
786
+ // src/index.ts
787
+ var QueryPanelSdkAPI = class {
530
788
  baseUrl;
531
789
  privateKey;
532
790
  organizationId;
@@ -534,6 +792,11 @@ var ApiClient = class {
534
792
  additionalHeaders;
535
793
  fetchImpl;
536
794
  cachedPrivateKey;
795
+ databases = /* @__PURE__ */ new Map();
796
+ databaseMetadata = /* @__PURE__ */ new Map();
797
+ defaultDatabase;
798
+ lastSyncHashes = /* @__PURE__ */ new Map();
799
+ syncedDatabases = /* @__PURE__ */ new Set();
537
800
  constructor(baseUrl, privateKey, organizationId, options) {
538
801
  if (!baseUrl) {
539
802
  throw new Error("Base URL is required");
@@ -556,703 +819,621 @@ var ApiClient = class {
556
819
  );
557
820
  }
558
821
  }
559
- getDefaultTenantId() {
560
- return this.defaultTenantId;
561
- }
562
- async get(path, tenantId, userId, scopes, signal, sessionId) {
563
- return await this.request(path, {
564
- method: "GET",
565
- headers: await this.buildHeaders(tenantId, userId, scopes, false, sessionId),
566
- signal
567
- });
568
- }
569
- async post(path, body, tenantId, userId, scopes, signal, sessionId) {
570
- return await this.request(path, {
571
- method: "POST",
572
- headers: await this.buildHeaders(tenantId, userId, scopes, true, sessionId),
573
- body: JSON.stringify(body ?? {}),
574
- signal
575
- });
576
- }
577
- async put(path, body, tenantId, userId, scopes, signal, sessionId) {
578
- return await this.request(path, {
579
- method: "PUT",
580
- headers: await this.buildHeaders(tenantId, userId, scopes, true, sessionId),
581
- body: JSON.stringify(body ?? {}),
582
- signal
583
- });
584
- }
585
- async delete(path, tenantId, userId, scopes, signal, sessionId) {
586
- return await this.request(path, {
587
- method: "DELETE",
588
- headers: await this.buildHeaders(tenantId, userId, scopes, false, sessionId),
589
- signal
590
- });
591
- }
592
- async request(path, init) {
593
- const response = await this.fetchImpl(`${this.baseUrl}${path}`, init);
594
- const text = await response.text();
595
- let json;
596
- try {
597
- json = text ? JSON.parse(text) : void 0;
598
- } catch {
599
- json = void 0;
600
- }
601
- if (!response.ok) {
602
- const error = new Error(
603
- json?.error || response.statusText || "Request failed"
604
- );
605
- error.status = response.status;
606
- if (json?.details) error.details = json.details;
607
- throw error;
608
- }
609
- return json;
610
- }
611
- async buildHeaders(tenantId, userId, scopes, includeJson = true, sessionId) {
612
- const token = await this.generateJWT(tenantId, userId, scopes);
613
- const headers = {
614
- Authorization: `Bearer ${token}`,
615
- Accept: "application/json"
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
616
833
  };
617
- if (includeJson) {
618
- headers["Content-Type"] = "application/json";
619
- }
620
- if (sessionId) {
621
- headers["x-session-id"] = sessionId;
622
- }
623
- if (this.additionalHeaders) {
624
- Object.assign(headers, this.additionalHeaders);
625
- }
626
- return headers;
834
+ this.databaseMetadata.set(name, metadata);
627
835
  }
628
- async generateJWT(tenantId, userId, scopes) {
629
- if (!this.cachedPrivateKey) {
630
- this.cachedPrivateKey = await importPKCS8(this.privateKey, "RS256");
631
- }
632
- const payload = {
633
- organizationId: this.organizationId,
634
- tenantId
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
635
846
  };
636
- if (userId) payload.userId = userId;
637
- if (scopes?.length) payload.scopes = scopes;
638
- return await new SignJWT(payload).setProtectedHeader({ alg: "RS256" }).setIssuedAt().setExpirationTime("1h").sign(this.cachedPrivateKey);
847
+ this.databaseMetadata.set(name, metadata);
639
848
  }
640
- };
641
-
642
- // src/core/query-engine.ts
643
- var QueryEngine = class {
644
- databases = /* @__PURE__ */ new Map();
645
- databaseMetadata = /* @__PURE__ */ new Map();
646
- defaultDatabase;
647
- attachDatabase(name, adapter, metadata) {
849
+ attachDatabase(name, adapter) {
648
850
  this.databases.set(name, adapter);
649
- this.databaseMetadata.set(name, metadata);
650
851
  if (!this.defaultDatabase) {
651
852
  this.defaultDatabase = name;
652
853
  }
653
854
  }
654
- getDatabase(name) {
655
- const dbName = name ?? this.defaultDatabase;
656
- if (!dbName) {
657
- throw new Error("No database attached.");
658
- }
659
- const adapter = this.databases.get(dbName);
660
- if (!adapter) {
661
- throw new Error(
662
- `Database '${dbName}' not found. Attached: ${Array.from(
663
- this.databases.keys()
664
- ).join(", ")}`
665
- );
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
+ };
666
877
  }
667
- return adapter;
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;
668
891
  }
669
- getDatabaseMetadata(name) {
670
- const dbName = name ?? this.defaultDatabase;
671
- if (!dbName) return void 0;
672
- return this.databaseMetadata.get(dbName);
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
+ );
673
910
  }
674
- getDefaultDatabase() {
675
- return this.defaultDatabase;
911
+ async introspect(databaseName, tables) {
912
+ const adapter = this.getDatabase(databaseName);
913
+ return await adapter.introspect(tables ? { tables } : void 0);
676
914
  }
677
- async validateAndExecute(sql, params, databaseName, tenantId) {
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) {
940
+ throw new Error(
941
+ "No database attached. Call attachPostgres/attachClickhouse first."
942
+ );
943
+ }
678
944
  const adapter = this.getDatabase(databaseName);
679
- const metadata = this.getDatabaseMetadata(databaseName);
680
- let finalSql = sql;
945
+ const paramMetadata = Array.isArray(queryResponse.params) ? queryResponse.params : [];
946
+ const paramValues = this.mapGeneratedParams(paramMetadata);
947
+ const metadata = this.databaseMetadata.get(databaseName);
681
948
  if (metadata) {
682
- finalSql = this.ensureTenantIsolation(sql, params, metadata, tenantId);
949
+ queryResponse.sql = this.ensureTenantIsolation(
950
+ queryResponse.sql,
951
+ paramValues,
952
+ metadata,
953
+ tenantId
954
+ );
683
955
  }
684
- await adapter.validate(finalSql, params);
685
- const result = await adapter.execute(finalSql, params);
686
- return {
687
- rows: result.rows,
688
- fields: result.fields
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
689
962
  };
690
- }
691
- async execute(sql, params, databaseName) {
692
- try {
693
- const adapter = this.getDatabase(databaseName);
694
- const result = await adapter.execute(sql, params);
695
- return result.rows;
696
- } catch (error) {
697
- console.warn(
698
- `Failed to execute SQL locally for database '${databaseName}':`,
699
- error
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
700
980
  );
701
- return [];
981
+ chart = {
982
+ vegaLiteSpec: chartResponse.chart ? {
983
+ ...chartResponse.chart,
984
+ data: { values: rows }
985
+ } : null,
986
+ notes: chartResponse.notes
987
+ };
702
988
  }
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
+ };
703
1001
  }
704
- mapGeneratedParams(params) {
705
- const record = {};
706
- params.forEach((param, index) => {
707
- const value = param.value;
708
- if (value === void 0) {
709
- return;
710
- }
711
- const nameCandidate = typeof param.name === "string" && param.name.trim() || typeof param.placeholder === "string" && param.placeholder.trim() || typeof param.position === "number" && String(param.position) || String(index + 1);
712
- const key = nameCandidate.replace(/[{}:$]/g, "").trim();
713
- record[key] = value;
714
- });
715
- return record;
716
- }
717
- ensureTenantIsolation(sql, params, metadata, tenantId) {
718
- if (!metadata.tenantFieldName || metadata.enforceTenantIsolation === false) {
719
- return sql;
720
- }
721
- const tenantField = metadata.tenantFieldName;
722
- const paramKey = tenantField;
723
- params[paramKey] = tenantId;
724
- const normalizedSql = sql.toLowerCase();
725
- if (normalizedSql.includes(tenantField.toLowerCase())) {
726
- return sql;
727
- }
728
- const tenantPredicate = metadata.dialect === "clickhouse" ? `${tenantField} = {${tenantField}:${metadata.tenantFieldType ?? "String"}}` : `${tenantField} = '${tenantId}'`;
729
- if (/\bwhere\b/i.test(sql)) {
730
- return sql.replace(
731
- /\bwhere\b/i,
732
- (match) => `${match} ${tenantPredicate} AND `
733
- );
734
- }
735
- return `${sql} WHERE ${tenantPredicate}`;
736
- }
737
- };
738
-
739
- // src/routes/charts.ts
740
- async function createChart(client, body, options, signal) {
741
- const tenantId = resolveTenantId(client, options?.tenantId);
742
- return await client.post(
743
- "/charts",
744
- body,
745
- tenantId,
746
- options?.userId,
747
- options?.scopes,
748
- signal
749
- );
750
- }
751
- async function listCharts(client, queryEngine, options, signal) {
752
- const tenantId = resolveTenantId(client, options?.tenantId);
753
- const params = new URLSearchParams();
754
- if (options?.pagination?.page)
755
- params.set("page", `${options.pagination.page}`);
756
- if (options?.pagination?.limit)
757
- params.set("limit", `${options.pagination.limit}`);
758
- if (options?.sortBy) params.set("sort_by", options.sortBy);
759
- if (options?.sortDir) params.set("sort_dir", options.sortDir);
760
- if (options?.title) params.set("title", options.title);
761
- if (options?.userFilter) params.set("user_id", options.userFilter);
762
- if (options?.createdFrom) params.set("created_from", options.createdFrom);
763
- if (options?.createdTo) params.set("created_to", options.createdTo);
764
- if (options?.updatedFrom) params.set("updated_from", options.updatedFrom);
765
- if (options?.updatedTo) params.set("updated_to", options.updatedTo);
766
- const response = await client.get(
767
- `/charts${params.toString() ? `?${params.toString()}` : ""}`,
768
- tenantId,
769
- options?.userId,
770
- options?.scopes,
771
- signal
772
- );
773
- if (options?.includeData) {
774
- response.data = await Promise.all(
775
- response.data.map(async (chart) => ({
776
- ...chart,
777
- vega_lite_spec: {
778
- ...chart.vega_lite_spec,
779
- data: {
780
- values: await queryEngine.execute(
781
- chart.sql,
782
- chart.sql_params ?? void 0,
783
- chart.target_db ?? void 0
784
- )
785
- }
786
- }
787
- }))
788
- );
789
- }
790
- return response;
791
- }
792
- async function getChart(client, queryEngine, id, options, signal) {
793
- const tenantId = resolveTenantId(client, options?.tenantId);
794
- const chart = await client.get(
795
- `/charts/${encodeURIComponent(id)}`,
796
- tenantId,
797
- options?.userId,
798
- options?.scopes,
799
- signal
800
- );
801
- return {
802
- ...chart,
803
- vega_lite_spec: {
804
- ...chart.vega_lite_spec,
805
- data: {
806
- values: await queryEngine.execute(
807
- chart.sql,
808
- chart.sql_params ?? void 0,
809
- chart.target_db ?? void 0
810
- )
811
- }
812
- }
813
- };
814
- }
815
- async function updateChart(client, id, body, options, signal) {
816
- const tenantId = resolveTenantId(client, options?.tenantId);
817
- return await client.put(
818
- `/charts/${encodeURIComponent(id)}`,
819
- body,
820
- tenantId,
821
- options?.userId,
822
- options?.scopes,
823
- signal
824
- );
825
- }
826
- async function deleteChart(client, id, options, signal) {
827
- const tenantId = resolveTenantId(client, options?.tenantId);
828
- await client.delete(
829
- `/charts/${encodeURIComponent(id)}`,
830
- tenantId,
831
- options?.userId,
832
- options?.scopes,
833
- signal
834
- );
835
- }
836
- function resolveTenantId(client, tenantId) {
837
- const resolved = tenantId ?? client.getDefaultTenantId();
838
- if (!resolved) {
839
- throw new Error(
840
- "tenantId is required. Provide it per request or via defaultTenantId option."
841
- );
842
- }
843
- return resolved;
844
- }
845
-
846
- // src/routes/active-charts.ts
847
- async function createActiveChart(client, body, options, signal) {
848
- const tenantId = resolveTenantId2(client, options?.tenantId);
849
- return await client.post(
850
- "/active-charts",
851
- body,
852
- tenantId,
853
- options?.userId,
854
- options?.scopes,
855
- signal
856
- );
857
- }
858
- async function listActiveCharts(client, queryEngine, options, signal) {
859
- const tenantId = resolveTenantId2(client, options?.tenantId);
860
- const params = new URLSearchParams();
861
- if (options?.pagination?.page)
862
- params.set("page", `${options.pagination.page}`);
863
- if (options?.pagination?.limit)
864
- params.set("limit", `${options.pagination.limit}`);
865
- if (options?.sortBy) params.set("sort_by", options.sortBy);
866
- if (options?.sortDir) params.set("sort_dir", options.sortDir);
867
- if (options?.title) params.set("name", options.title);
868
- if (options?.userFilter) params.set("user_id", options.userFilter);
869
- if (options?.createdFrom) params.set("created_from", options.createdFrom);
870
- if (options?.createdTo) params.set("created_to", options.createdTo);
871
- if (options?.updatedFrom) params.set("updated_from", options.updatedFrom);
872
- if (options?.updatedTo) params.set("updated_to", options.updatedTo);
873
- const response = await client.get(
874
- `/active-charts${params.toString() ? `?${params.toString()}` : ""}`,
875
- tenantId,
876
- options?.userId,
877
- options?.scopes,
878
- signal
879
- );
880
- if (options?.withData) {
881
- response.data = await Promise.all(
882
- response.data.map(async (active) => ({
883
- ...active,
884
- chart: active.chart ? await getChart(
885
- client,
886
- queryEngine,
887
- active.chart_id,
888
- options,
889
- signal
890
- ) : null
891
- }))
892
- );
893
- }
894
- return response;
895
- }
896
- async function getActiveChart(client, queryEngine, id, options, signal) {
897
- const tenantId = resolveTenantId2(client, options?.tenantId);
898
- const active = await client.get(
899
- `/active-charts/${encodeURIComponent(id)}`,
900
- tenantId,
901
- options?.userId,
902
- options?.scopes,
903
- signal
904
- );
905
- if (options?.withData && active.chart_id) {
906
- return {
907
- ...active,
908
- chart: await getChart(
909
- client,
910
- queryEngine,
911
- active.chart_id,
912
- options,
913
- signal
914
- )
915
- };
916
- }
917
- return active;
918
- }
919
- async function updateActiveChart(client, id, body, options, signal) {
920
- const tenantId = resolveTenantId2(client, options?.tenantId);
921
- return await client.put(
922
- `/active-charts/${encodeURIComponent(id)}`,
923
- body,
924
- tenantId,
925
- options?.userId,
926
- options?.scopes,
927
- signal
928
- );
929
- }
930
- async function deleteActiveChart(client, id, options, signal) {
931
- const tenantId = resolveTenantId2(client, options?.tenantId);
932
- await client.delete(
933
- `/active-charts/${encodeURIComponent(id)}`,
934
- tenantId,
935
- options?.userId,
936
- options?.scopes,
937
- signal
938
- );
939
- }
940
- function resolveTenantId2(client, tenantId) {
941
- const resolved = tenantId ?? client.getDefaultTenantId();
942
- if (!resolved) {
943
- throw new Error(
944
- "tenantId is required. Provide it per request or via defaultTenantId option."
945
- );
946
- }
947
- return resolved;
948
- }
949
-
950
- // src/routes/ingest.ts
951
- import { randomUUID } from "crypto";
952
- async function syncSchema(client, queryEngine, databaseName, options, signal) {
953
- const tenantId = resolveTenantId3(client, options.tenantId);
954
- const adapter = queryEngine.getDatabase(databaseName);
955
- const introspection = await adapter.introspect(
956
- options.tables ? { tables: options.tables } : void 0
957
- );
958
- const payload = buildSchemaRequest(databaseName, adapter, introspection);
959
- const sessionId = randomUUID();
960
- const response = await client.post(
961
- "/ingest",
962
- payload,
963
- tenantId,
964
- options.userId,
965
- options.scopes,
966
- signal,
967
- sessionId
968
- );
969
- return response;
970
- }
971
- function resolveTenantId3(client, tenantId) {
972
- const resolved = tenantId ?? client.getDefaultTenantId();
973
- if (!resolved) {
974
- throw new Error(
975
- "tenantId is required. Provide it per request or via defaultTenantId option."
976
- );
977
- }
978
- return resolved;
979
- }
980
- function buildSchemaRequest(databaseName, adapter, introspection) {
981
- const dialect = adapter.getDialect();
982
- const tables = introspection.tables.map((table) => ({
983
- table_name: table.name,
984
- description: table.comment ?? `Table ${table.name}`,
985
- columns: table.columns.map((column) => ({
986
- name: column.name,
987
- data_type: column.rawType ?? column.type,
988
- is_primary_key: Boolean(column.isPrimaryKey),
989
- description: column.comment ?? ""
990
- }))
991
- }));
992
- return {
993
- database: databaseName,
994
- dialect,
995
- tables
996
- };
997
- }
998
-
999
- // src/routes/query.ts
1000
- import { randomUUID as randomUUID2 } from "crypto";
1001
- async function ask(client, queryEngine, question, options, signal) {
1002
- const tenantId = resolveTenantId4(client, options.tenantId);
1003
- const sessionId = randomUUID2();
1004
- const maxRetry = options.maxRetry ?? 0;
1005
- let attempt = 0;
1006
- let lastError = options.lastError;
1007
- let previousSql = options.previousSql;
1008
- while (attempt <= maxRetry) {
1009
- console.log({ lastError, previousSql });
1010
- const queryResponse = await client.post(
1011
- "/query",
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",
1012
1006
  {
1013
- question,
1014
- ...lastError ? { last_error: lastError } : {},
1015
- ...previousSql ? { previous_sql: previousSql } : {},
1016
- ...options.maxRetry ? { max_retry: options.maxRetry } : {}
1007
+ organization_id: this.organizationId,
1008
+ tenant_id: tenantId,
1009
+ target_identifier: input.targetIdentifier,
1010
+ content: input.content,
1011
+ user_id: input.userId
1017
1012
  },
1018
1013
  tenantId,
1019
- options.userId,
1020
- options.scopes,
1021
- signal,
1022
- sessionId
1014
+ options?.userId,
1015
+ options?.scopes,
1016
+ signal
1023
1017
  );
1024
- const databaseName = queryResponse.database ?? options.database ?? queryEngine.getDefaultDatabase();
1025
- if (!databaseName) {
1026
- throw new Error(
1027
- "No database attached. Call attachPostgres/attachClickhouse first."
1028
- );
1029
- }
1030
- const paramMetadata = Array.isArray(queryResponse.params) ? queryResponse.params : [];
1031
- const paramValues = queryEngine.mapGeneratedParams(paramMetadata);
1032
- try {
1033
- const execution = await queryEngine.validateAndExecute(
1034
- queryResponse.sql,
1035
- paramValues,
1036
- databaseName,
1037
- tenantId
1038
- );
1039
- const rows = execution.rows ?? [];
1040
- let chart = {
1041
- vegaLiteSpec: null,
1042
- notes: rows.length === 0 ? "Query returned no rows." : null
1043
- };
1044
- if (rows.length > 0) {
1045
- const chartResponse = await client.post(
1046
- "/chart",
1047
- {
1048
- question,
1049
- sql: queryResponse.sql,
1050
- rationale: queryResponse.rationale,
1051
- fields: execution.fields,
1052
- rows: anonymizeResults(rows),
1053
- max_retries: options.chartMaxRetries ?? 3,
1054
- query_id: queryResponse.queryId
1055
- },
1056
- tenantId,
1057
- options.userId,
1058
- options.scopes,
1059
- signal,
1060
- sessionId
1061
- );
1062
- chart = {
1063
- vegaLiteSpec: chartResponse.chart ? {
1064
- ...chartResponse.chart,
1065
- data: { values: rows }
1066
- } : null,
1067
- notes: chartResponse.notes
1068
- };
1069
- }
1070
- return {
1071
- sql: queryResponse.sql,
1072
- params: paramValues,
1073
- paramMetadata,
1074
- rationale: queryResponse.rationale,
1075
- dialect: queryResponse.dialect,
1076
- queryId: queryResponse.queryId,
1077
- rows,
1078
- fields: execution.fields,
1079
- chart,
1080
- context: queryResponse.context,
1081
- attempts: attempt + 1
1082
- };
1083
- } catch (error) {
1084
- attempt++;
1085
- if (attempt > maxRetry) {
1086
- throw error;
1087
- }
1088
- lastError = error instanceof Error ? error.message : String(error);
1089
- previousSql = queryResponse.sql;
1090
- console.warn(
1091
- `SQL execution failed (attempt ${attempt}/${maxRetry + 1}): ${lastError}. Retrying...`
1092
- );
1093
- }
1018
+ return response.annotation;
1094
1019
  }
1095
- throw new Error("Unexpected error in ask retry loop");
1096
- }
1097
- function resolveTenantId4(client, tenantId) {
1098
- const resolved = tenantId ?? client.getDefaultTenantId();
1099
- if (!resolved) {
1100
- throw new Error(
1101
- "tenantId is required. Provide it per request or via defaultTenantId option."
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
1102
1028
  );
1029
+ return response.annotations;
1103
1030
  }
1104
- return resolved;
1105
- }
1106
- function anonymizeResults(rows) {
1107
- if (!rows?.length) return [];
1108
- return rows.map((row) => {
1109
- const masked = {};
1110
- Object.entries(row).forEach(([key, value]) => {
1111
- if (value === null) masked[key] = "null";
1112
- else if (Array.isArray(value)) masked[key] = "array";
1113
- else masked[key] = typeof value;
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;
1114
1044
  });
1115
- return masked;
1116
- });
1117
- }
1118
-
1119
- // src/index.ts
1120
- var QueryPanelSdkAPI = class {
1121
- client;
1122
- queryEngine;
1123
- constructor(baseUrl, privateKey, organizationId, options) {
1124
- this.client = new ApiClient(baseUrl, privateKey, organizationId, options);
1125
- this.queryEngine = new QueryEngine();
1045
+ return response.annotation ?? null;
1126
1046
  }
1127
- // Database attachment methods
1128
- attachClickhouse(name, clientFn, options) {
1129
- const adapter = new ClickHouseAdapter(clientFn, options);
1130
- const metadata = {
1131
- name,
1132
- dialect: "clickhouse",
1133
- description: options?.description,
1134
- tags: options?.tags,
1135
- tenantFieldName: options?.tenantFieldName,
1136
- tenantFieldType: options?.tenantFieldType ?? "String",
1137
- enforceTenantIsolation: options?.tenantFieldName ? options?.enforceTenantIsolation ?? true : void 0
1138
- };
1139
- this.queryEngine.attachDatabase(name, adapter, metadata);
1140
- }
1141
- attachPostgres(name, clientFn, options) {
1142
- const adapter = new PostgresAdapter(clientFn, options);
1143
- const metadata = {
1144
- name,
1145
- dialect: "postgres",
1146
- description: options?.description,
1147
- tags: options?.tags,
1148
- tenantFieldName: options?.tenantFieldName,
1149
- enforceTenantIsolation: options?.tenantFieldName ? options?.enforceTenantIsolation ?? true : void 0
1150
- };
1151
- this.queryEngine.attachDatabase(name, adapter, metadata);
1152
- }
1153
- attachDatabase(name, adapter) {
1154
- const metadata = {
1155
- name,
1156
- dialect: adapter.getDialect()
1157
- };
1158
- this.queryEngine.attachDatabase(name, adapter, metadata);
1159
- }
1160
- // Schema introspection and sync
1161
- async introspect(databaseName, tables) {
1162
- const adapter = this.queryEngine.getDatabase(databaseName);
1163
- return await adapter.introspect(tables ? { tables } : void 0);
1164
- }
1165
- async syncSchema(databaseName, options, signal) {
1166
- return await syncSchema(
1167
- this.client,
1168
- this.queryEngine,
1169
- databaseName,
1170
- options,
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,
1171
1054
  signal
1172
1055
  );
1173
1056
  }
1174
- // Natural language query
1175
- async ask(question, options, signal) {
1176
- return await ask(
1177
- this.client,
1178
- this.queryEngine,
1179
- question,
1180
- options,
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,
1181
1065
  signal
1182
1066
  );
1183
1067
  }
1184
- // Chart CRUD operations
1185
- async createChart(body, options, signal) {
1186
- return await createChart(this.client, body, options, signal);
1187
- }
1188
1068
  async listCharts(options, signal) {
1189
- return await listCharts(
1190
- this.client,
1191
- this.queryEngine,
1192
- options,
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,
1193
1088
  signal
1194
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;
1195
1108
  }
1196
1109
  async getChart(id, options, signal) {
1197
- return await getChart(
1198
- this.client,
1199
- this.queryEngine,
1200
- id,
1201
- options,
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,
1202
1116
  signal
1203
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
+ };
1204
1131
  }
1205
1132
  async updateChart(id, body, options, signal) {
1206
- return await updateChart(
1207
- this.client,
1208
- id,
1133
+ const tenantId = this.resolveTenantId(options?.tenantId);
1134
+ return await this.put(
1135
+ `/charts/${encodeURIComponent(id)}`,
1209
1136
  body,
1210
- options,
1137
+ tenantId,
1138
+ options?.userId,
1139
+ options?.scopes,
1211
1140
  signal
1212
1141
  );
1213
1142
  }
1214
1143
  async deleteChart(id, options, signal) {
1215
- await deleteChart(this.client, 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
+ );
1216
1152
  }
1217
- // Active Chart CRUD operations
1218
1153
  async createActiveChart(body, options, signal) {
1219
- return await createActiveChart(
1220
- this.client,
1154
+ const tenantId = this.resolveTenantId(options?.tenantId);
1155
+ return await this.post(
1156
+ "/active-charts",
1221
1157
  body,
1222
- options,
1158
+ tenantId,
1159
+ options?.userId,
1160
+ options?.scopes,
1223
1161
  signal
1224
1162
  );
1225
1163
  }
1226
1164
  async listActiveCharts(options, signal) {
1227
- return await listActiveCharts(
1228
- this.client,
1229
- this.queryEngine,
1230
- options,
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,
1231
1184
  signal
1232
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;
1233
1195
  }
1234
1196
  async getActiveChart(id, options, signal) {
1235
- return await getActiveChart(
1236
- this.client,
1237
- this.queryEngine,
1238
- id,
1239
- options,
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,
1240
1203
  signal
1241
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;
1242
1212
  }
1243
1213
  async updateActiveChart(id, body, options, signal) {
1244
- return await updateActiveChart(
1245
- this.client,
1246
- id,
1214
+ const tenantId = this.resolveTenantId(options?.tenantId);
1215
+ return await this.put(
1216
+ `/active-charts/${encodeURIComponent(id)}`,
1247
1217
  body,
1248
- options,
1218
+ tenantId,
1219
+ options?.userId,
1220
+ options?.scopes,
1249
1221
  signal
1250
1222
  );
1251
1223
  }
1252
1224
  async deleteActiveChart(id, options, signal) {
1253
- await deleteActiveChart(this.client, 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
+ }
1378
+ mapGeneratedParams(params) {
1379
+ const record = {};
1380
+ params.forEach((param, index) => {
1381
+ const value = param.value;
1382
+ if (value === void 0) {
1383
+ return;
1384
+ }
1385
+ const nameCandidate = typeof param.name === "string" && param.name.trim() || typeof param.placeholder === "string" && param.placeholder.trim() || typeof param.position === "number" && String(param.position) || String(index + 1);
1386
+ const key = nameCandidate.replace(/[{}:$]/g, "").trim();
1387
+ record[key] = value;
1388
+ });
1389
+ return record;
1390
+ }
1391
+ ensureTenantIsolation(sql, params, metadata, tenantId) {
1392
+ if (!metadata.tenantFieldName || metadata.enforceTenantIsolation === false) {
1393
+ return sql;
1394
+ }
1395
+ const tenantField = metadata.tenantFieldName;
1396
+ const paramKey = tenantField;
1397
+ params[paramKey] = tenantId;
1398
+ const normalizedSql = sql.toLowerCase();
1399
+ if (normalizedSql.includes(tenantField.toLowerCase())) {
1400
+ return sql;
1401
+ }
1402
+ const tenantPredicate = metadata.dialect === "clickhouse" ? `${tenantField} = {${tenantField}:${metadata.tenantFieldType ?? "String"}}` : `${tenantField} = '${tenantId}'`;
1403
+ if (/\bwhere\b/i.test(sql)) {
1404
+ return sql.replace(
1405
+ /\bwhere\b/i,
1406
+ (match) => `${match} ${tenantPredicate} AND `
1407
+ );
1408
+ }
1409
+ return `${sql} WHERE ${tenantPredicate}`;
1410
+ }
1411
+ async runSafeQueryOnClient(sql, database, params) {
1412
+ try {
1413
+ const adapter = this.getDatabase(database);
1414
+ const result = await adapter.execute(sql, params);
1415
+ return result.rows;
1416
+ } catch (error) {
1417
+ console.warn(
1418
+ `Failed to execute SQL locally for database '${database}':`,
1419
+ error
1420
+ );
1421
+ return [];
1422
+ }
1254
1423
  }
1255
1424
  };
1425
+ function anonymizeResults(rows) {
1426
+ if (!rows?.length) return [];
1427
+ return rows.map((row) => {
1428
+ const masked = {};
1429
+ Object.entries(row).forEach(([key, value]) => {
1430
+ if (value === null) masked[key] = "null";
1431
+ else if (Array.isArray(value)) masked[key] = "array";
1432
+ else masked[key] = typeof value;
1433
+ });
1434
+ return masked;
1435
+ });
1436
+ }
1256
1437
  export {
1257
1438
  ClickHouseAdapter,
1258
1439
  PostgresAdapter,