@querypanel/node-sdk 1.0.24 → 1.0.25

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.
Files changed (137) hide show
  1. package/README.md +46 -274
  2. package/dist/index.cjs +1471 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.cts +468 -0
  5. package/dist/index.d.ts +468 -0
  6. package/dist/index.js +1443 -0
  7. package/dist/index.js.map +1 -0
  8. package/package.json +53 -63
  9. package/dist/cjs/adapters/clickhouse.d.ts +0 -48
  10. package/dist/cjs/adapters/clickhouse.d.ts.map +0 -1
  11. package/dist/cjs/adapters/clickhouse.js +0 -284
  12. package/dist/cjs/adapters/clickhouse.js.map +0 -1
  13. package/dist/cjs/adapters/introspection.spec.d.ts +0 -2
  14. package/dist/cjs/adapters/introspection.spec.d.ts.map +0 -1
  15. package/dist/cjs/adapters/introspection.spec.js +0 -192
  16. package/dist/cjs/adapters/introspection.spec.js.map +0 -1
  17. package/dist/cjs/adapters/postgres.d.ts +0 -46
  18. package/dist/cjs/adapters/postgres.d.ts.map +0 -1
  19. package/dist/cjs/adapters/postgres.js +0 -457
  20. package/dist/cjs/adapters/postgres.js.map +0 -1
  21. package/dist/cjs/adapters/postgres.spec.d.ts +0 -2
  22. package/dist/cjs/adapters/postgres.spec.d.ts.map +0 -1
  23. package/dist/cjs/adapters/postgres.spec.js +0 -37
  24. package/dist/cjs/adapters/postgres.spec.js.map +0 -1
  25. package/dist/cjs/adapters/types.d.ts +0 -38
  26. package/dist/cjs/adapters/types.d.ts.map +0 -1
  27. package/dist/cjs/adapters/types.js +0 -3
  28. package/dist/cjs/adapters/types.js.map +0 -1
  29. package/dist/cjs/anonymize.spec.d.ts +0 -2
  30. package/dist/cjs/anonymize.spec.d.ts.map +0 -1
  31. package/dist/cjs/anonymize.spec.js +0 -78
  32. package/dist/cjs/anonymize.spec.js.map +0 -1
  33. package/dist/cjs/clickhouseClient.spec.d.ts +0 -2
  34. package/dist/cjs/clickhouseClient.spec.d.ts.map +0 -1
  35. package/dist/cjs/clickhouseClient.spec.js +0 -286
  36. package/dist/cjs/clickhouseClient.spec.js.map +0 -1
  37. package/dist/cjs/connectors/base.d.ts +0 -14
  38. package/dist/cjs/connectors/base.d.ts.map +0 -1
  39. package/dist/cjs/connectors/base.js +0 -3
  40. package/dist/cjs/connectors/base.js.map +0 -1
  41. package/dist/cjs/connectors/clickhouse.d.ts +0 -35
  42. package/dist/cjs/connectors/clickhouse.d.ts.map +0 -1
  43. package/dist/cjs/connectors/clickhouse.js +0 -292
  44. package/dist/cjs/connectors/clickhouse.js.map +0 -1
  45. package/dist/cjs/index.d.ts +0 -498
  46. package/dist/cjs/index.d.ts.map +0 -1
  47. package/dist/cjs/index.js +0 -849
  48. package/dist/cjs/index.js.map +0 -1
  49. package/dist/cjs/index.test.d.ts +0 -2
  50. package/dist/cjs/index.test.d.ts.map +0 -1
  51. package/dist/cjs/index.test.js +0 -185
  52. package/dist/cjs/index.test.js.map +0 -1
  53. package/dist/cjs/introspectV3.d.ts +0 -45
  54. package/dist/cjs/introspectV3.d.ts.map +0 -1
  55. package/dist/cjs/introspectV3.js +0 -99
  56. package/dist/cjs/introspectV3.js.map +0 -1
  57. package/dist/cjs/multidb.spec.d.ts +0 -2
  58. package/dist/cjs/multidb.spec.d.ts.map +0 -1
  59. package/dist/cjs/multidb.spec.js +0 -76
  60. package/dist/cjs/multidb.spec.js.map +0 -1
  61. package/dist/cjs/package.json +0 -1
  62. package/dist/cjs/schema/types.d.ts +0 -73
  63. package/dist/cjs/schema/types.d.ts.map +0 -1
  64. package/dist/cjs/schema/types.js +0 -3
  65. package/dist/cjs/schema/types.js.map +0 -1
  66. package/dist/cjs/tenant-isolation.spec.d.ts +0 -2
  67. package/dist/cjs/tenant-isolation.spec.d.ts.map +0 -1
  68. package/dist/cjs/tenant-isolation.spec.js +0 -420
  69. package/dist/cjs/tenant-isolation.spec.js.map +0 -1
  70. package/dist/cjs/utils/clickhouse.d.ts +0 -9
  71. package/dist/cjs/utils/clickhouse.d.ts.map +0 -1
  72. package/dist/cjs/utils/clickhouse.js +0 -99
  73. package/dist/cjs/utils/clickhouse.js.map +0 -1
  74. package/dist/esm/adapters/clickhouse.d.ts +0 -48
  75. package/dist/esm/adapters/clickhouse.d.ts.map +0 -1
  76. package/dist/esm/adapters/clickhouse.js +0 -280
  77. package/dist/esm/adapters/clickhouse.js.map +0 -1
  78. package/dist/esm/adapters/introspection.spec.d.ts +0 -2
  79. package/dist/esm/adapters/introspection.spec.d.ts.map +0 -1
  80. package/dist/esm/adapters/introspection.spec.js +0 -190
  81. package/dist/esm/adapters/introspection.spec.js.map +0 -1
  82. package/dist/esm/adapters/postgres.d.ts +0 -46
  83. package/dist/esm/adapters/postgres.d.ts.map +0 -1
  84. package/dist/esm/adapters/postgres.js +0 -453
  85. package/dist/esm/adapters/postgres.js.map +0 -1
  86. package/dist/esm/adapters/postgres.spec.d.ts +0 -2
  87. package/dist/esm/adapters/postgres.spec.d.ts.map +0 -1
  88. package/dist/esm/adapters/postgres.spec.js +0 -35
  89. package/dist/esm/adapters/postgres.spec.js.map +0 -1
  90. package/dist/esm/adapters/types.d.ts +0 -38
  91. package/dist/esm/adapters/types.d.ts.map +0 -1
  92. package/dist/esm/adapters/types.js +0 -2
  93. package/dist/esm/adapters/types.js.map +0 -1
  94. package/dist/esm/anonymize.spec.d.ts +0 -2
  95. package/dist/esm/anonymize.spec.d.ts.map +0 -1
  96. package/dist/esm/anonymize.spec.js +0 -76
  97. package/dist/esm/anonymize.spec.js.map +0 -1
  98. package/dist/esm/clickhouseClient.spec.d.ts +0 -2
  99. package/dist/esm/clickhouseClient.spec.d.ts.map +0 -1
  100. package/dist/esm/clickhouseClient.spec.js +0 -281
  101. package/dist/esm/clickhouseClient.spec.js.map +0 -1
  102. package/dist/esm/connectors/base.d.ts +0 -14
  103. package/dist/esm/connectors/base.d.ts.map +0 -1
  104. package/dist/esm/connectors/base.js +0 -2
  105. package/dist/esm/connectors/base.js.map +0 -1
  106. package/dist/esm/connectors/clickhouse.d.ts +0 -35
  107. package/dist/esm/connectors/clickhouse.d.ts.map +0 -1
  108. package/dist/esm/connectors/clickhouse.js +0 -288
  109. package/dist/esm/connectors/clickhouse.js.map +0 -1
  110. package/dist/esm/index.d.ts +0 -498
  111. package/dist/esm/index.d.ts.map +0 -1
  112. package/dist/esm/index.js +0 -844
  113. package/dist/esm/index.js.map +0 -1
  114. package/dist/esm/index.test.d.ts +0 -2
  115. package/dist/esm/index.test.d.ts.map +0 -1
  116. package/dist/esm/index.test.js +0 -183
  117. package/dist/esm/index.test.js.map +0 -1
  118. package/dist/esm/introspectV3.d.ts +0 -45
  119. package/dist/esm/introspectV3.d.ts.map +0 -1
  120. package/dist/esm/introspectV3.js +0 -96
  121. package/dist/esm/introspectV3.js.map +0 -1
  122. package/dist/esm/multidb.spec.d.ts +0 -2
  123. package/dist/esm/multidb.spec.d.ts.map +0 -1
  124. package/dist/esm/multidb.spec.js +0 -74
  125. package/dist/esm/multidb.spec.js.map +0 -1
  126. package/dist/esm/schema/types.d.ts +0 -73
  127. package/dist/esm/schema/types.d.ts.map +0 -1
  128. package/dist/esm/schema/types.js +0 -2
  129. package/dist/esm/schema/types.js.map +0 -1
  130. package/dist/esm/tenant-isolation.spec.d.ts +0 -2
  131. package/dist/esm/tenant-isolation.spec.d.ts.map +0 -1
  132. package/dist/esm/tenant-isolation.spec.js +0 -418
  133. package/dist/esm/tenant-isolation.spec.js.map +0 -1
  134. package/dist/esm/utils/clickhouse.d.ts +0 -9
  135. package/dist/esm/utils/clickhouse.d.ts.map +0 -1
  136. package/dist/esm/utils/clickhouse.js +0 -92
  137. package/dist/esm/utils/clickhouse.js.map +0 -1
package/dist/cjs/index.js DELETED
@@ -1,849 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.QueryPanelSdkAPI = exports.PostgresAdapter = exports.ClickHouseAdapter = void 0;
4
- exports.anonymizeResults = anonymizeResults;
5
- const node_crypto_1 = require("node:crypto");
6
- const jose_1 = require("jose");
7
- const clickhouse_js_1 = require("./adapters/clickhouse.js");
8
- Object.defineProperty(exports, "ClickHouseAdapter", { enumerable: true, get: function () { return clickhouse_js_1.ClickHouseAdapter; } });
9
- const postgres_js_1 = require("./adapters/postgres.js");
10
- Object.defineProperty(exports, "PostgresAdapter", { enumerable: true, get: function () { return postgres_js_1.PostgresAdapter; } });
11
- const introspectV3_js_1 = require("./introspectV3.js");
12
- /**
13
- * Anonymizes query results by replacing actual values with their type names.
14
- * Useful for sharing result structure without exposing sensitive data.
15
- *
16
- * @param rows - Array of result rows to anonymize
17
- * @returns Array of rows with values replaced by type strings
18
- *
19
- * @example
20
- * anonymizeResults([{ year: 2025, transactionSum: 1000 }])
21
- * // Returns: [{ year: 'number', transactionSum: 'number' }]
22
- */
23
- function anonymizeResults(rows) {
24
- if (!rows || rows.length === 0) {
25
- return [];
26
- }
27
- return rows.map((row) => {
28
- const anonymized = {};
29
- for (const [key, value] of Object.entries(row)) {
30
- if (value === null) {
31
- anonymized[key] = "null";
32
- }
33
- else if (Array.isArray(value)) {
34
- anonymized[key] = "array";
35
- }
36
- else {
37
- anonymized[key] = typeof value;
38
- }
39
- }
40
- return anonymized;
41
- });
42
- }
43
- class QueryPanelSdkAPI {
44
- constructor(baseUrl, jwtTokenOrPrivateKey, organizationId, options) {
45
- // Database registry
46
- this.databases = new Map();
47
- this.databaseMetadata = new Map();
48
- // Schema sync tracking
49
- this.lastSyncHashes = new Map();
50
- this.syncedDatabases = new Set();
51
- this.baseUrl = baseUrl.replace(/\/$/, "");
52
- if (organizationId) {
53
- // New mode: generate JWTs
54
- this.privateKey = jwtTokenOrPrivateKey;
55
- this.organizationId = organizationId;
56
- }
57
- else {
58
- // Legacy mode: use provided JWT
59
- this.token = jwtTokenOrPrivateKey;
60
- }
61
- // Store additional headers if provided
62
- this.additionalHeaders = options?.additionalHeaders;
63
- }
64
- /**
65
- * Attach a database adapter with a specific name
66
- */
67
- attachDatabase(name, adapter) {
68
- this.databases.set(name, adapter);
69
- if (!this.defaultDatabase) {
70
- this.defaultDatabase = name;
71
- }
72
- }
73
- /**
74
- * Attach a ClickHouse database
75
- */
76
- attachClickhouse(name, clientFn, options) {
77
- const adapterOptions = {
78
- database: options?.database ?? name,
79
- ...(options?.defaultFormat
80
- ? { defaultFormat: options.defaultFormat }
81
- : {}),
82
- ...(options?.kind ? { kind: options.kind } : {}),
83
- ...(options?.allowedTables
84
- ? { allowedTables: options.allowedTables }
85
- : {}),
86
- };
87
- this.attachDatabase(name, new clickhouse_js_1.ClickHouseAdapter(clientFn, adapterOptions));
88
- // Store metadata
89
- const metadata = {
90
- name,
91
- dialect: "clickhouse",
92
- };
93
- if (options?.description)
94
- metadata.description = options.description;
95
- if (options?.tags)
96
- metadata.tags = options.tags;
97
- if (options?.tenantFieldName) {
98
- metadata.tenantFieldName = options.tenantFieldName;
99
- metadata.tenantFieldType = options.tenantFieldType ?? "String";
100
- // Default to enforcing tenant isolation if tenantFieldName is provided
101
- metadata.enforceTenantIsolation = options.enforceTenantIsolation ?? true;
102
- }
103
- this.databaseMetadata.set(name, metadata);
104
- }
105
- /**
106
- * Attach a Postgres database
107
- */
108
- attachPostgres(name, clientFn, options) {
109
- const adapterOptions = {
110
- database: options?.database ?? name,
111
- ...(options?.defaultSchema
112
- ? { defaultSchema: options.defaultSchema }
113
- : {}),
114
- ...(options?.kind ? { kind: options.kind } : {}),
115
- };
116
- this.attachDatabase(name, new postgres_js_1.PostgresAdapter(clientFn, adapterOptions));
117
- // Store metadata
118
- const metadata = {
119
- name,
120
- dialect: "postgres",
121
- };
122
- if (options?.description)
123
- metadata.description = options.description;
124
- if (options?.tags)
125
- metadata.tags = options.tags;
126
- if (options?.tenantFieldName) {
127
- metadata.tenantFieldName = options.tenantFieldName;
128
- // Default to enforcing tenant isolation if tenantFieldName is provided
129
- metadata.enforceTenantIsolation = options.enforceTenantIsolation ?? true;
130
- }
131
- this.databaseMetadata.set(name, metadata);
132
- }
133
- /**
134
- * Legacy method for backward compatibility
135
- * Attaches a ClickHouse client as the default database
136
- */
137
- attachClickhouseClient(fn) {
138
- this.attachClickhouse("default", fn);
139
- }
140
- /**
141
- * Get a database adapter by name, or return the default
142
- */
143
- getDatabase(name) {
144
- const dbName = name || this.defaultDatabase;
145
- if (!dbName) {
146
- throw new Error("No database attached. Use attachClickhouse() or attachPostgres() first.");
147
- }
148
- const adapter = this.databases.get(dbName);
149
- if (!adapter) {
150
- throw new Error(`Database '${dbName}' not found. Available: ${Array.from(this.databases.keys()).join(", ")}`);
151
- }
152
- return adapter;
153
- }
154
- /**
155
- * Synchronize database schema to the API for vector search.
156
- * Call this after schema migrations or when you want to update the schema context.
157
- * Uses hash-based change detection to avoid unnecessary syncs.
158
- */
159
- async syncSchema(databaseName, options, signal) {
160
- const adapter = this.getDatabase(databaseName);
161
- // Introspect schema
162
- const introspection = await adapter.introspect(options.tables ? { tables: options.tables } : undefined);
163
- // Generate hash of introspection for change detection
164
- const hash = this.hashIntrospection(introspection);
165
- const lastHash = this.lastSyncHashes.get(databaseName);
166
- // Skip if schema unchanged (unless force is true)
167
- if (!options.force && lastHash === hash) {
168
- console.log(`Schema unchanged for ${databaseName}, skipping sync`);
169
- return;
170
- }
171
- // Send introspection to API
172
- const response = await this.post("/v2/vectorize-schema", {
173
- database: databaseName,
174
- dialect: adapter.getDialect(),
175
- introspection,
176
- schemaHash: hash,
177
- force: options.force ?? false,
178
- }, options.tenantId, options.userId, options.scopes, signal);
179
- const effectiveHash = response?.schemaHash ?? hash;
180
- if (response?.status === "skipped") {
181
- console.log(`Schema hash unchanged on server for ${databaseName}, skipping upload`);
182
- }
183
- this.lastSyncHashes.set(databaseName, effectiveHash);
184
- this.syncedDatabases.add(databaseName);
185
- }
186
- /**
187
- * Introspect database schema and return V3 export format
188
- * This format is compatible with Bedrock knowledge base ingestion
189
- *
190
- * @param databaseName - Name of the attached database to introspect
191
- * @param options - Optional tenant ID and table filter
192
- * @returns Schema export in V3 format (schema_export.json)
193
- *
194
- * @example
195
- * ```typescript
196
- * const schema = await sdk.introspectV3("my_database", {
197
- * tenantId: "tenant-123",
198
- * tables: ["users", "orders"]
199
- * });
200
- *
201
- * // Upload to Bedrock
202
- * await fetch("/v3/ingest", {
203
- * method: "POST",
204
- * body: JSON.stringify(schema)
205
- * });
206
- * ```
207
- */
208
- async introspectV3(databaseName, options) {
209
- const adapter = this.getDatabase(databaseName);
210
- // Perform introspection
211
- const introspection = await adapter.introspect(options?.tables ? { tables: options.tables } : undefined);
212
- // Transform to V3 format
213
- return (0, introspectV3_js_1.transformToV3Export)(introspection, options?.tenantId);
214
- }
215
- /**
216
- * Introspect and ingest schema to Bedrock knowledge base (v3)
217
- * Combines introspectV3 + API call to /v3/ingest
218
- *
219
- * @param databaseName - Name of the attached database to introspect
220
- * @param options - Required tenant ID and optional table filter
221
- * @returns Ingestion summary from Bedrock
222
- *
223
- * @example
224
- * ```typescript
225
- * const result = await sdk.ingestSchemaV3("my_database", {
226
- * tenantId: "tenant-123",
227
- * tables: ["users", "orders"]
228
- * });
229
- *
230
- * console.log(`Ingested ${result.total_documents} documents`);
231
- * ```
232
- */
233
- async ingestSchemaV3(databaseName, options, signal) {
234
- // Introspect schema in V3 format
235
- const schema = await this.introspectV3(databaseName, {
236
- tenantId: options.tenantId,
237
- ...(options.tables && { tables: options.tables }),
238
- });
239
- // Post to v3/ingest endpoint
240
- const result = await this.post("/v3/ingest", schema, options.tenantId, options.userId, options.scopes, signal);
241
- // Mark database as synced
242
- this.syncedDatabases.add(databaseName);
243
- return result;
244
- }
245
- /**
246
- * Introspect and ingest schema to LangChain vector store (v4)
247
- * Combines introspectV3 + API call to /v4/ingest
248
- *
249
- * @param databaseName - Name of the attached database to introspect
250
- * @param options - Required tenant ID and optional table filter
251
- * @returns Ingestion summary from the LangChain module
252
- *
253
- * @example
254
- * ```typescript
255
- * const result = await sdk.ingestSchemaV4("my_database", {
256
- * tenantId: "tenant-123",
257
- * tables: ["users", "orders"]
258
- * });
259
- *
260
- * console.log(`Ingested ${result.chunksCreated} chunks`);
261
- * ```
262
- */
263
- async ingestSchemaV4(databaseName, options, signal) {
264
- // Introspect schema in V3 format (v4 uses the same schema format)
265
- const schemaExport = await this.introspectV3(databaseName, {
266
- tenantId: options.tenantId,
267
- ...(options.tables && { tables: options.tables }),
268
- });
269
- // Post to v4/ingest endpoint
270
- const result = await this.post("/v4/ingest", { schema: schemaExport }, options.tenantId, options.userId, options.scopes, signal);
271
- // Mark database as synced
272
- this.syncedDatabases.add(databaseName);
273
- return result;
274
- }
275
- /**
276
- * Ensure all attached databases have been synced at least once.
277
- * Called automatically on first ask() if needed (unless disabled).
278
- */
279
- async ensureDatabasesSynced(tenantId, userId, scopes, useV3, useV4, disableAutoSync) {
280
- if (disableAutoSync) {
281
- return; // Auto-sync disabled, user must call syncSchema/ingestSchemaV3/ingestSchemaV4 manually
282
- }
283
- const dbNames = Array.from(this.databases.keys());
284
- if (dbNames.length === 0)
285
- return;
286
- let unsyncedDbs = dbNames.filter((name) => !this.syncedDatabases.has(name));
287
- if (unsyncedDbs.length === 0)
288
- return;
289
- if (this.organizationId) {
290
- await Promise.all(unsyncedDbs.map((dbName) => this.reconcileSchemaStatus(dbName, tenantId, userId, scopes)));
291
- unsyncedDbs = dbNames.filter((name) => !this.syncedDatabases.has(name));
292
- if (unsyncedDbs.length === 0)
293
- return;
294
- }
295
- const version = useV4 ? "v4" : useV3 ? "v3" : "v2";
296
- console.log(`Auto-syncing databases (${version}): ${unsyncedDbs.join(", ")}`);
297
- // Sync all unsynced databases in parallel
298
- await Promise.all(unsyncedDbs.map((dbName) => useV4
299
- ? this.ingestSchemaV4(dbName, {
300
- tenantId,
301
- ...(userId ? { userId } : {}),
302
- ...(scopes ? { scopes } : {}),
303
- }).catch((err) => console.warn(`Failed to sync ${dbName}:`, err))
304
- : useV3
305
- ? this.ingestSchemaV3(dbName, {
306
- tenantId,
307
- ...(userId ? { userId } : {}),
308
- ...(scopes ? { scopes } : {}),
309
- }).catch((err) => console.warn(`Failed to sync ${dbName}:`, err))
310
- : this.syncSchema(dbName, {
311
- tenantId,
312
- ...(userId ? { userId } : {}),
313
- ...(scopes ? { scopes } : {}),
314
- }).catch((err) => console.warn(`Failed to sync ${dbName}:`, err))));
315
- }
316
- /**
317
- * Generate a hash of the introspection for change detection
318
- */
319
- hashIntrospection(introspection) {
320
- const tables = Array.isArray(introspection?.tables)
321
- ? introspection.tables
322
- : [];
323
- const normalized = tables.map((t) => {
324
- return {
325
- name: t?.name ?? "",
326
- schema: t?.schema ?? "",
327
- columns: Array.isArray(t?.columns)
328
- ? t.columns
329
- .map((c) => `${c?.name ?? ""}:${c?.type ?? ""}`)
330
- .sort()
331
- : [],
332
- };
333
- });
334
- normalized.sort((a, b) => {
335
- const schemaCompare = a.schema.localeCompare(b.schema);
336
- if (schemaCompare !== 0)
337
- return schemaCompare;
338
- return a.name.localeCompare(b.name);
339
- });
340
- const content = JSON.stringify(normalized);
341
- return (0, node_crypto_1.createHash)("sha256").update(content).digest("hex");
342
- }
343
- async reconcileSchemaStatus(databaseName, tenantId, userId, scopes) {
344
- const cachedHash = this.lastSyncHashes.get(databaseName);
345
- const params = new URLSearchParams({ database: databaseName });
346
- if (cachedHash)
347
- params.set("hash", cachedHash);
348
- try {
349
- const response = await this.get(`/v2/schema-sync-status?${params.toString()}`, tenantId, userId, scopes);
350
- if (response.status === "up_to_date" && response.schemaHash) {
351
- this.lastSyncHashes.set(databaseName, response.schemaHash);
352
- this.syncedDatabases.add(databaseName);
353
- return;
354
- }
355
- if (response.status === "stale" &&
356
- response.schemaHash &&
357
- response.introspectedAt) {
358
- this.lastSyncHashes.set(databaseName, response.schemaHash);
359
- }
360
- if (response.status === "not_found") {
361
- this.lastSyncHashes.delete(databaseName);
362
- }
363
- }
364
- catch (error) {
365
- if (error?.status === 404 ||
366
- error?.status === 400 ||
367
- error?.status === 501) {
368
- // Older server or missing org context – fall back to client-side sync
369
- return;
370
- }
371
- console.warn(`Failed to check schema sync status for '${databaseName}': ${error}`);
372
- }
373
- }
374
- /**
375
- * Ensures tenant isolation by adding tenant filter to SQL if not present.
376
- * Modifies SQL and params in-place to include tenant field.
377
- */
378
- ensureTenantIsolation(sql, params, metadata, tenantId) {
379
- if (!metadata.tenantFieldName || !metadata.enforceTenantIsolation) {
380
- return sql; // Enforcement disabled or no tenant field configured
381
- }
382
- const tenantFieldName = metadata.tenantFieldName;
383
- const tenantFieldType = metadata.tenantFieldType || "String";
384
- // Check if tenant field is already being filtered in SQL
385
- let parameterFound = false;
386
- let placeholderName;
387
- if (metadata.dialect === "clickhouse") {
388
- // ClickHouse: Check if the field name appears in a WHERE/AND clause with comparison
389
- // This catches patterns like: "customer_id = {tenantId:Int32}" or "customer_id = {customer_id:String}"
390
- const fieldFilterPattern = new RegExp(`\\b${tenantFieldName}\\s*=\\s*\\{([^}:]+)(?::[^}]+)?\\}`, "i");
391
- const match = sql.match(fieldFilterPattern);
392
- if (match) {
393
- parameterFound = true;
394
- placeholderName = match[1]; // Extract placeholder name (e.g., "tenantId" from "{tenantId:Int32}")
395
- }
396
- }
397
- else if (metadata.dialect === "postgres") {
398
- // Check if field name appears in SQL (basic check for tenant field)
399
- parameterFound = sql
400
- .toLowerCase()
401
- .includes(tenantFieldName.toLowerCase());
402
- }
403
- else {
404
- // Generic check: ensure the field name appears in SQL
405
- parameterFound = sql
406
- .toLowerCase()
407
- .includes(tenantFieldName.toLowerCase());
408
- }
409
- // Always enforce tenant parameter with correct tenantId
410
- // This ensures the parameter is set even if the API returns 0 or any other placeholder
411
- if (placeholderName) {
412
- // Use the actual placeholder name found in SQL (e.g., "tenantId")
413
- params[placeholderName] = tenantId;
414
- }
415
- else {
416
- // Fallback to field name
417
- params[tenantFieldName] = tenantId;
418
- }
419
- // If parameter not found in SQL, add tenant filter
420
- if (!parameterFound) {
421
- console.warn(`[Tenant Isolation] Adding missing tenant filter to SQL for field '${tenantFieldName}' on database '${metadata.name}'`);
422
- // Determine how to add the filter based on dialect
423
- let tenantFilter = "";
424
- if (metadata.dialect === "clickhouse") {
425
- // ClickHouse parameterized syntax
426
- tenantFilter = `${tenantFieldName} = {${tenantFieldName}:${tenantFieldType}}`;
427
- }
428
- else if (metadata.dialect === "postgres") {
429
- // Postgres would need $N syntax, but since we don't know the param index,
430
- // we'll use a simple comparison (the adapter should handle parameterization)
431
- tenantFilter = `${tenantFieldName} = '${tenantId}'`;
432
- }
433
- else {
434
- tenantFilter = `${tenantFieldName} = '${tenantId}'`;
435
- }
436
- // Determine if SQL already has a WHERE clause
437
- const hasWhere = /\bWHERE\b/i.test(sql);
438
- if (hasWhere) {
439
- // Find the WHERE clause and add AND condition
440
- // Look for WHERE and insert after it, or at the end before GROUP BY/ORDER BY/LIMIT
441
- const whereMatch = sql.match(/\bWHERE\b/i);
442
- if (whereMatch) {
443
- const whereIndex = whereMatch.index + whereMatch[0].length;
444
- // Find the end of WHERE clause (before GROUP BY, ORDER BY, LIMIT, or end)
445
- const afterWhere = sql.substring(whereIndex);
446
- const endMatch = afterWhere.match(/\b(GROUP\s+BY|ORDER\s+BY|LIMIT|HAVING)\b/i);
447
- if (endMatch && endMatch.index === 0) {
448
- // WHERE is immediately followed by GROUP BY/ORDER BY/LIMIT - insert before it
449
- sql =
450
- sql.substring(0, whereIndex) + ` ${tenantFilter} ` + afterWhere;
451
- }
452
- else {
453
- // Add as AND condition
454
- sql =
455
- sql.substring(0, whereIndex) +
456
- ` (${tenantFilter}) AND (` +
457
- afterWhere.trimStart() +
458
- ")";
459
- }
460
- }
461
- }
462
- else {
463
- // Add new WHERE clause before GROUP BY/ORDER BY/LIMIT or at the end
464
- const clauseMatch = sql.match(/\b(GROUP\s+BY|ORDER\s+BY|LIMIT|HAVING)\b/i);
465
- if (clauseMatch) {
466
- const insertIndex = clauseMatch.index;
467
- sql =
468
- sql.substring(0, insertIndex) +
469
- `WHERE ${tenantFilter} ` +
470
- sql.substring(insertIndex);
471
- }
472
- else {
473
- // Add at the end
474
- sql = sql.trim();
475
- if (!sql.endsWith(";")) {
476
- sql += ` WHERE ${tenantFilter}`;
477
- }
478
- else {
479
- sql = sql.substring(0, sql.length - 1) + ` WHERE ${tenantFilter};`;
480
- }
481
- }
482
- }
483
- console.log(`[Tenant Isolation] Modified SQL to include tenant filter: ${tenantFilter}`);
484
- }
485
- return sql;
486
- }
487
- async ask(question, options, signal) {
488
- const { tenantId, database, userId, scopes, sqlMaxAttempts = 3, chartMaxRetries = 3, useV3 = false, useV4 = false, disableAutoSync = false, } = options;
489
- // Auto-sync databases on first use (unless disabled)
490
- await this.ensureDatabasesSynced(tenantId, userId, scopes, useV3, useV4, disableAutoSync);
491
- let attempt = 0;
492
- const sessionId = (0, node_crypto_1.randomUUID)();
493
- let lastError;
494
- let failedSql;
495
- while (attempt < sqlMaxAttempts) {
496
- // Build available databases list with metadata (includes tenantFieldName per database)
497
- const availableDatabases = Array.from(this.databaseMetadata.values());
498
- // Determine endpoint based on version flag
499
- const endpoint = useV4
500
- ? "/v4/generate-sql"
501
- : useV3
502
- ? "/v3/generate-sql"
503
- : "/v2/generate-sql";
504
- const sqlResponse = await this.post(endpoint, {
505
- question,
506
- max_retries: Math.max(1, sqlMaxAttempts - attempt),
507
- attempt_count: attempt,
508
- ...(lastError ? { last_error: lastError } : {}),
509
- ...(failedSql ? { failed_sql: failedSql } : {}),
510
- ...(database ? { database } : {}), // Optional hint
511
- available_databases: availableDatabases,
512
- ...(options.additionalPrompts && options.additionalPrompts.length > 0
513
- ? { additional_prompts: options.additionalPrompts }
514
- : {}),
515
- }, tenantId, userId, scopes, signal, sessionId);
516
- try {
517
- // Get the database adapter (use API's selected database or hint)
518
- const dbName = database || sqlResponse.database || this.defaultDatabase;
519
- if (!dbName) {
520
- throw new Error("No database selected");
521
- }
522
- const adapter = this.getDatabase(dbName);
523
- // Validate dialect matches (if API provided it)
524
- if (sqlResponse.dialect &&
525
- adapter.getDialect() !== sqlResponse.dialect) {
526
- throw new Error(`Dialect mismatch: API selected ${sqlResponse.dialect} but client has ${adapter.getDialect()} for database '${dbName}'`);
527
- }
528
- // Ensure tenant isolation: add tenant filter to SQL and params if not present
529
- const dbMetadata = this.databaseMetadata.get(dbName);
530
- if (dbMetadata) {
531
- sqlResponse.sql = this.ensureTenantIsolation(sqlResponse.sql, sqlResponse.params, dbMetadata, tenantId);
532
- }
533
- // Validate SQL with EXPLAIN before executing (with merged params)
534
- await adapter.validate(sqlResponse.sql, sqlResponse.params);
535
- // Execute the query with parameters
536
- const execution = await adapter.execute(sqlResponse.sql, sqlResponse.params);
537
- const rows = Array.isArray(execution.rows) ? execution.rows : [];
538
- let chart;
539
- if (rows.length === 0) {
540
- chart = {
541
- vegaLiteSpec: null,
542
- notes: "Query returned no rows; chart generation skipped.",
543
- };
544
- }
545
- else {
546
- const chartResponse = await this.post("/v2/generate-chart", {
547
- question,
548
- sql: sqlResponse.sql,
549
- rationale: sqlResponse.rationale,
550
- fields: execution.fields,
551
- rows: anonymizeResults(rows),
552
- max_retries: chartMaxRetries,
553
- }, tenantId, userId, scopes, signal, sessionId);
554
- chart = {
555
- vegaLiteSpec: {
556
- ...chartResponse.vegaLiteSpec,
557
- data: {
558
- values: rows,
559
- },
560
- },
561
- notes: chartResponse.notes,
562
- };
563
- }
564
- return {
565
- sql: sqlResponse.sql,
566
- params: sqlResponse.params,
567
- rationale: sqlResponse.rationale,
568
- context: sqlResponse.context,
569
- chart,
570
- fields: execution.fields,
571
- rows,
572
- };
573
- }
574
- catch (error) {
575
- lastError = error instanceof Error ? error.message : String(error);
576
- failedSql = sqlResponse.sql;
577
- attempt += 1;
578
- if (attempt >= sqlMaxAttempts) {
579
- throw error;
580
- }
581
- }
582
- }
583
- throw new Error("Failed to generate chart after maximum attempts");
584
- }
585
- async train(body, options, signal) {
586
- return await this.post("/v2/train", body, options?.tenantId, options?.userId, options?.scopes, signal);
587
- }
588
- async stats(options, signal) {
589
- return await this.get("/stats", options.tenantId, options.userId, options.scopes, signal);
590
- }
591
- // Charts CRUD
592
- async createChart(body, options, signal) {
593
- return await this.post("/v2/charts", body, options.tenantId, options.userId, options.scopes, signal);
594
- }
595
- async listCharts(options, signal) {
596
- const queryParams = new URLSearchParams();
597
- if (options.pagination?.page) {
598
- queryParams.set("page", options.pagination.page.toString());
599
- }
600
- if (options.pagination?.limit) {
601
- queryParams.set("limit", options.pagination.limit.toString());
602
- }
603
- if (options.sortBy) {
604
- queryParams.set("sort_by", options.sortBy);
605
- }
606
- if (options.sortDir) {
607
- queryParams.set("sort_dir", options.sortDir);
608
- }
609
- if (options.name) {
610
- queryParams.set("name", options.name);
611
- }
612
- if (options.filterUserId) {
613
- queryParams.set("user_id", options.filterUserId);
614
- }
615
- if (options.createdFrom) {
616
- queryParams.set("created_from", options.createdFrom);
617
- }
618
- if (options.createdTo) {
619
- queryParams.set("created_to", options.createdTo);
620
- }
621
- if (options.updatedFrom) {
622
- queryParams.set("updated_from", options.updatedFrom);
623
- }
624
- if (options.updatedTo) {
625
- queryParams.set("updated_to", options.updatedTo);
626
- }
627
- const query = queryParams.toString() ? `?${queryParams.toString()}` : "";
628
- const charts = await this.get(`/v2/charts${query}`, options.tenantId, options.userId, options.scopes, signal);
629
- const chartsWithData = await Promise.all(charts.data.map(async (chart) => {
630
- return {
631
- ...chart,
632
- vega_lite_spec: {
633
- ...chart.vega_lite_spec,
634
- data: {
635
- values: (await this.runSafeQueryOnClient(chart.sql, chart.database, chart.sql_params || undefined)) ?? [],
636
- },
637
- },
638
- };
639
- }));
640
- return {
641
- ...charts,
642
- data: chartsWithData,
643
- };
644
- }
645
- async getChart(id, options, signal) {
646
- const chart = await this.get(`/v2/charts/${encodeURIComponent(id)}`, options.tenantId, options.userId, options.scopes, signal);
647
- return {
648
- ...chart,
649
- vega_lite_spec: {
650
- ...chart.vega_lite_spec,
651
- data: {
652
- values: await this.runSafeQueryOnClient(chart.sql, chart.database, chart.sql_params || undefined),
653
- },
654
- },
655
- };
656
- }
657
- async updateChart(id, body, options, signal) {
658
- return await this.put(`/v2/charts/${encodeURIComponent(id)}`, body, options.tenantId, options.userId, options.scopes, signal);
659
- }
660
- async deleteChart(id, options, signal) {
661
- await this.delete(`/v2/charts/${encodeURIComponent(id)}`, options.tenantId, options.userId, options.scopes, signal);
662
- }
663
- // Active Charts CRUD
664
- async createActiveChart(body, options, signal) {
665
- return await this.post("/v2/active-charts", body, options.tenantId, options.userId, options.scopes, signal);
666
- }
667
- async listActiveCharts(options, signal) {
668
- const queryParams = new URLSearchParams();
669
- if (options.pagination?.page) {
670
- queryParams.set("page", options.pagination.page.toString());
671
- }
672
- if (options.pagination?.limit) {
673
- queryParams.set("limit", options.pagination.limit.toString());
674
- }
675
- if (options.sortBy)
676
- queryParams.set("sort_by", options.sortBy);
677
- if (options.sortDir)
678
- queryParams.set("sort_dir", options.sortDir);
679
- if (options.name)
680
- queryParams.set("name", options.name);
681
- if (options.filterUserId)
682
- queryParams.set("user_id", options.filterUserId);
683
- if (options.createdFrom)
684
- queryParams.set("created_from", options.createdFrom);
685
- if (options.createdTo)
686
- queryParams.set("created_to", options.createdTo);
687
- if (options.updatedFrom)
688
- queryParams.set("updated_from", options.updatedFrom);
689
- if (options.updatedTo)
690
- queryParams.set("updated_to", options.updatedTo);
691
- const query = queryParams.toString() ? `?${queryParams.toString()}` : "";
692
- const charts = await this.get(`/v2/active-charts${query}`, options.tenantId, options.userId, options.scopes, signal);
693
- if (options.withdata) {
694
- charts.data = await Promise.all(charts.data.map(async (chart) => {
695
- return {
696
- ...chart,
697
- chart: await this.getChart(chart.chart_id, options),
698
- };
699
- }));
700
- }
701
- return charts;
702
- }
703
- async getActiveChart(id, options, signal) {
704
- const activeChart = await this.get(`/v2/active-charts/${encodeURIComponent(id)}`, options.tenantId, options.userId, options.scopes, signal);
705
- return {
706
- ...activeChart,
707
- ...(!!activeChart.chart && {
708
- chart: {
709
- ...activeChart.chart,
710
- vega_lite_spec: {
711
- ...activeChart.chart.vega_lite_spec,
712
- data: {
713
- values: await this.runSafeQueryOnClient(activeChart.chart.sql, activeChart.chart.database, activeChart.chart.sql_params || undefined),
714
- },
715
- },
716
- },
717
- }),
718
- };
719
- }
720
- async updateActiveChart(id, body, options, signal) {
721
- return await this.put(`/v2/active-charts/${encodeURIComponent(id)}`, body, options.tenantId, options.userId, options.scopes, signal);
722
- }
723
- async deleteActiveChart(id, options, signal) {
724
- await this.delete(`/v2/active-charts/${encodeURIComponent(id)}`, options.tenantId, options.userId, options.scopes, signal);
725
- }
726
- async get(path, tenantId, userId, scopes, signal, sessionId) {
727
- const res = await fetch(this.baseUrl + path, {
728
- method: "GET",
729
- headers: await this.headers(tenantId, userId, scopes, false, sessionId),
730
- signal,
731
- });
732
- return await this.parseResponse(res);
733
- }
734
- async post(path, body, tenantId, userId, scopes, signal, sessionId) {
735
- const res = await fetch(this.baseUrl + path, {
736
- method: "POST",
737
- headers: await this.headers(tenantId, userId, scopes, true, sessionId),
738
- body: JSON.stringify(body ?? {}),
739
- signal,
740
- });
741
- return await this.parseResponse(res);
742
- }
743
- async put(path, body, tenantId, userId, scopes, signal, sessionId) {
744
- const res = await fetch(this.baseUrl + path, {
745
- method: "PUT",
746
- headers: await this.headers(tenantId, userId, scopes, true, sessionId),
747
- body: JSON.stringify(body ?? {}),
748
- signal,
749
- });
750
- return await this.parseResponse(res);
751
- }
752
- async delete(path, tenantId, userId, scopes, signal, sessionId) {
753
- const res = await fetch(this.baseUrl + path, {
754
- method: "DELETE",
755
- headers: await this.headers(tenantId, userId, scopes, false, sessionId),
756
- signal,
757
- });
758
- return await this.parseResponse(res);
759
- }
760
- async generateJWT(tenantId, userId, scopes) {
761
- if (!this.privateKey || !this.organizationId) {
762
- throw new Error("Private key and organization ID are required for JWT generation");
763
- }
764
- // Cache the imported private key to avoid re-importing on every request
765
- if (!this.cachedPrivateKey) {
766
- this.cachedPrivateKey = await (0, jose_1.importPKCS8)(this.privateKey, "RS256");
767
- }
768
- const payload = {
769
- organizationId: this.organizationId,
770
- };
771
- if (tenantId) {
772
- payload.tenantId = tenantId;
773
- }
774
- if (userId) {
775
- payload.userId = userId;
776
- }
777
- if (scopes && scopes.length > 0) {
778
- payload.scopes = scopes;
779
- }
780
- return await new jose_1.SignJWT(payload)
781
- .setProtectedHeader({ alg: "RS256" })
782
- .setIssuedAt()
783
- .setExpirationTime("1h")
784
- .sign(this.cachedPrivateKey);
785
- }
786
- async headers(tenantId, userId, scopes, includeJsonContentType = true, sessionId) {
787
- let authToken;
788
- if (this.token) {
789
- // Legacy mode: use provided JWT
790
- authToken = this.token;
791
- }
792
- else {
793
- // New mode: generate JWT
794
- authToken = await this.generateJWT(tenantId, userId, scopes);
795
- }
796
- const headers = {
797
- Authorization: `Bearer ${authToken}`,
798
- Accept: "application/json",
799
- };
800
- if (includeJsonContentType) {
801
- headers["Content-Type"] = "application/json";
802
- }
803
- if (sessionId) {
804
- headers["x-session-id"] = sessionId;
805
- }
806
- // Merge in additional headers if provided
807
- if (this.additionalHeaders) {
808
- Object.assign(headers, this.additionalHeaders);
809
- }
810
- return headers;
811
- }
812
- async parseResponse(res) {
813
- const text = await res.text();
814
- let json;
815
- try {
816
- json = text ? JSON.parse(text) : undefined;
817
- }
818
- catch { }
819
- if (!res.ok) {
820
- const message = json?.error || res.statusText || "Request failed";
821
- const err = new Error(message);
822
- err.status = res.status;
823
- if (json?.details)
824
- err.details = json.details;
825
- // Special handling for 429 (MaxRetriesError from v4 API)
826
- if (res.status === 429 && json?.attemptCount && json?.maxRetries) {
827
- err.details = {
828
- ...json,
829
- message: `Maximum retry attempts (${json.maxRetries}) exceeded after ${json.attemptCount} attempts`,
830
- };
831
- }
832
- throw err;
833
- }
834
- return json;
835
- }
836
- async runSafeQueryOnClient(sql, database, params) {
837
- try {
838
- const adapter = this.getDatabase(database);
839
- const result = await adapter.execute(sql, params || {});
840
- return result.rows;
841
- }
842
- catch (error) {
843
- console.error(`Error running query on database '${database}': ${error}`);
844
- return [];
845
- }
846
- }
847
- }
848
- exports.QueryPanelSdkAPI = QueryPanelSdkAPI;
849
- //# sourceMappingURL=index.js.map