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