@querypanel/node-sdk 1.0.26 → 1.0.28

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