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