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