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