@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 +835 -656
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +218 -138
- package/dist/index.d.ts +218 -138
- package/dist/index.js +837 -656
- package/dist/index.js.map +1 -1
- package/package.json +73 -73
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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/
|
|
528
|
-
|
|
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
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
-
|
|
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
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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
|
-
|
|
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
|
-
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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
|
-
|
|
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
|
-
|
|
670
|
-
const
|
|
671
|
-
|
|
672
|
-
|
|
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
|
-
|
|
675
|
-
|
|
911
|
+
async introspect(databaseName, tables) {
|
|
912
|
+
const adapter = this.getDatabase(databaseName);
|
|
913
|
+
return await adapter.introspect(tables ? { tables } : void 0);
|
|
676
914
|
}
|
|
677
|
-
async
|
|
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
|
|
680
|
-
|
|
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
|
-
|
|
949
|
+
queryResponse.sql = this.ensureTenantIsolation(
|
|
950
|
+
queryResponse.sql,
|
|
951
|
+
paramValues,
|
|
952
|
+
metadata,
|
|
953
|
+
tenantId
|
|
954
|
+
);
|
|
683
955
|
}
|
|
684
|
-
await adapter.validate(
|
|
685
|
-
const
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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
|
-
|
|
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
|
-
|
|
705
|
-
const
|
|
706
|
-
|
|
707
|
-
|
|
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
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
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
|
|
1020
|
-
options
|
|
1021
|
-
signal
|
|
1022
|
-
sessionId
|
|
1014
|
+
options?.userId,
|
|
1015
|
+
options?.scopes,
|
|
1016
|
+
signal
|
|
1023
1017
|
);
|
|
1024
|
-
|
|
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
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
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
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
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
|
|
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
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
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
|
-
|
|
1175
|
-
|
|
1176
|
-
return await
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
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
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
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
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
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
|
-
|
|
1207
|
-
|
|
1208
|
-
id
|
|
1133
|
+
const tenantId = this.resolveTenantId(options?.tenantId);
|
|
1134
|
+
return await this.put(
|
|
1135
|
+
`/charts/${encodeURIComponent(id)}`,
|
|
1209
1136
|
body,
|
|
1210
|
-
|
|
1137
|
+
tenantId,
|
|
1138
|
+
options?.userId,
|
|
1139
|
+
options?.scopes,
|
|
1211
1140
|
signal
|
|
1212
1141
|
);
|
|
1213
1142
|
}
|
|
1214
1143
|
async deleteChart(id, options, signal) {
|
|
1215
|
-
|
|
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
|
-
|
|
1220
|
-
|
|
1154
|
+
const tenantId = this.resolveTenantId(options?.tenantId);
|
|
1155
|
+
return await this.post(
|
|
1156
|
+
"/active-charts",
|
|
1221
1157
|
body,
|
|
1222
|
-
|
|
1158
|
+
tenantId,
|
|
1159
|
+
options?.userId,
|
|
1160
|
+
options?.scopes,
|
|
1223
1161
|
signal
|
|
1224
1162
|
);
|
|
1225
1163
|
}
|
|
1226
1164
|
async listActiveCharts(options, signal) {
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
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
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
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
|
-
|
|
1245
|
-
|
|
1246
|
-
id
|
|
1214
|
+
const tenantId = this.resolveTenantId(options?.tenantId);
|
|
1215
|
+
return await this.put(
|
|
1216
|
+
`/active-charts/${encodeURIComponent(id)}`,
|
|
1247
1217
|
body,
|
|
1248
|
-
|
|
1218
|
+
tenantId,
|
|
1219
|
+
options?.userId,
|
|
1220
|
+
options?.scopes,
|
|
1249
1221
|
signal
|
|
1250
1222
|
);
|
|
1251
1223
|
}
|
|
1252
1224
|
async deleteActiveChart(id, options, signal) {
|
|
1253
|
-
|
|
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,
|