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