@lightdash/warehouses 0.2100.0 → 0.2101.0

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.
@@ -43,11 +43,19 @@ export declare class SnowflakeSqlBuilder extends WarehouseBaseSqlBuilder {
43
43
  export declare class SnowflakeWarehouseClient extends WarehouseBaseClient<CreateSnowflakeCredentials> {
44
44
  connectionOptions: ConnectionOptions;
45
45
  quotedIdentifiersIgnoreCase?: boolean;
46
+ private externalBrowserConnectionPromise?;
46
47
  constructor(credentials: CreateSnowflakeCredentials);
47
48
  private getConnection;
49
+ /**
50
+ * Creates and caches a connection for external browser authentication.
51
+ * This prevents opening multiple browser tabs when parallel queries are executed.
52
+ */
53
+ private createExternalBrowserConnection;
54
+ private createConnection;
48
55
  private prepareWarehouse;
49
56
  private getFieldsFromStatement;
50
57
  private getAsyncStatementResults;
58
+ private destroyConnection;
51
59
  getAsyncQueryResults<TFormattedRow extends Record<string, unknown>>({ sql, page, pageSize, queryId }: WarehouseGetAsyncQueryResultsArgs, rowFormatter?: (row: Record<string, unknown>) => TFormattedRow): Promise<WarehouseGetAsyncQueryResults<TFormattedRow>>;
52
60
  executeAsyncQuery({ sql, values, tags, timezone }: WarehouseExecuteAsyncQueryArgs, resultsStreamCallback: (rows: WarehouseResults['rows'], fields: WarehouseResults['fields']) => void): Promise<WarehouseExecuteAsyncQuery>;
53
61
  private executeAsyncStatement;
@@ -82,6 +82,7 @@ const normaliseSnowflakeType = (type) => {
82
82
  }
83
83
  return match[0];
84
84
  };
85
+ const EXTERNAL_BROWSER_AUTHENTICATOR = 'EXTERNALBROWSER';
85
86
  const mapFieldType = (type) => {
86
87
  switch (normaliseSnowflakeType(type)) {
87
88
  case SnowflakeTypes.NUMBER:
@@ -167,7 +168,7 @@ class SnowflakeWarehouseClient extends WarehouseBaseClient_1.default {
167
168
  }
168
169
  let authenticationOptions = {};
169
170
  // if authenticationType is undefined, we assume it is a password authentication, for backwards compatibility
170
- if (credentials.authenticationType === 'sso') {
171
+ if (credentials.authenticationType === common_1.SnowflakeAuthenticationType.SSO) {
171
172
  if (!credentials.token) {
172
173
  // Perhaps we forgot to refresh the token before building the client, check buildAdapter for more details
173
174
  throw new common_1.UnexpectedServerError('Snowflake token is required for SSO authentication');
@@ -178,9 +179,16 @@ class SnowflakeWarehouseClient extends WarehouseBaseClient_1.default {
178
179
  authenticator: 'OAUTH',
179
180
  };
180
181
  }
182
+ else if (credentials.authenticationType ===
183
+ common_1.SnowflakeAuthenticationType.EXTERNAL_BROWSER) {
184
+ authenticationOptions = {
185
+ authenticator: EXTERNAL_BROWSER_AUTHENTICATOR,
186
+ };
187
+ }
181
188
  else if (credentials.privateKey &&
182
189
  (!credentials.password ||
183
- credentials.authenticationType === 'private_key')) {
190
+ credentials.authenticationType ===
191
+ common_1.SnowflakeAuthenticationType.PRIVATE_KEY)) {
184
192
  if (!credentials.privateKeyPass) {
185
193
  authenticationOptions = {
186
194
  username: credentials.user,
@@ -242,9 +250,53 @@ class SnowflakeWarehouseClient extends WarehouseBaseClient_1.default {
242
250
  warehouse: this.connectionOptions.warehouse,
243
251
  accessUrl: this.connectionOptions.accessUrl,
244
252
  };
245
- console.info(`Initialized snowflake warehouse client with authentication type "${credentials.authenticationType}" and connection options: ${JSON.stringify(logConnectionOptions)}`);
253
+ if (credentials.requireUserCredentials)
254
+ console.info(`Initialized snowflake warehouse client with "requireUserCredentials" authentication type "${credentials.authenticationType}" and connection options: ${JSON.stringify(logConnectionOptions)}`);
246
255
  }
247
256
  async getConnection(connectionOptionsOverrides) {
257
+ // External browser authentication uses a cached connection to avoid opening multiple browser tabs
258
+ if (this.connectionOptions.authenticator ===
259
+ EXTERNAL_BROWSER_AUTHENTICATOR) {
260
+ return this.createExternalBrowserConnection(connectionOptionsOverrides);
261
+ }
262
+ // For other authentication types, create a new connection with optional overrides
263
+ return this.createConnection(connectionOptionsOverrides);
264
+ }
265
+ /**
266
+ * Creates and caches a connection for external browser authentication.
267
+ * This prevents opening multiple browser tabs when parallel queries are executed.
268
+ */
269
+ async createExternalBrowserConnection(connectionOptionsOverrides) {
270
+ // Return cached promise if one exists (handles both in-flight and completed connections)
271
+ if (this.externalBrowserConnectionPromise) {
272
+ return this.externalBrowserConnectionPromise;
273
+ }
274
+ // Create and cache the connection promise
275
+ this.externalBrowserConnectionPromise = (async () => {
276
+ let connection;
277
+ try {
278
+ connection = (0, snowflake_sdk_1.createConnection)({
279
+ ...this.connectionOptions,
280
+ ...connectionOptionsOverrides,
281
+ });
282
+ console.info(`Connecting to snowflake warehouse with "external_browser" authentication type`);
283
+ await Util.promisify(connection.connectAsync.bind(connection))();
284
+ }
285
+ catch (e) {
286
+ throw new common_1.WarehouseConnectionError(`Snowflake error: ${(0, common_1.getErrorMessage)(e)}`);
287
+ }
288
+ return connection;
289
+ })();
290
+ try {
291
+ return await this.externalBrowserConnectionPromise;
292
+ }
293
+ catch (e) {
294
+ // Clear cache on error to allow retry
295
+ this.externalBrowserConnectionPromise = undefined;
296
+ throw e;
297
+ }
298
+ }
299
+ async createConnection(connectionOptionsOverrides) {
248
300
  let connection;
249
301
  try {
250
302
  connection = (0, snowflake_sdk_1.createConnection)({
@@ -328,6 +380,22 @@ class SnowflakeWarehouseClient extends WarehouseBaseClient_1.default {
328
380
  numRows: statement.getNumRows(),
329
381
  };
330
382
  }
383
+ async destroyConnection(connection, authenticator) {
384
+ if (authenticator === EXTERNAL_BROWSER_AUTHENTICATOR) {
385
+ // EXTERNALBROWSER connections are never destroyed - they live for the lifetime of the client✅
386
+ // Other auth types (password, SSO, private key) still destroy connections after use
387
+ return;
388
+ }
389
+ console.info(`Destroying snowflake connection for authenticator ${authenticator}`);
390
+ await new Promise((resolve, reject) => {
391
+ connection.destroy((err, conn) => {
392
+ if (err) {
393
+ reject(new common_1.WarehouseConnectionError(err.message));
394
+ }
395
+ resolve(conn);
396
+ });
397
+ });
398
+ }
331
399
  async getAsyncQueryResults({ sql, page, pageSize, queryId }, rowFormatter) {
332
400
  if (queryId === null) {
333
401
  throw new common_1.WarehouseQueryError('Query ID is required');
@@ -350,14 +418,7 @@ class SnowflakeWarehouseClient extends WarehouseBaseClient_1.default {
350
418
  throw this.parseError(error, sql);
351
419
  }
352
420
  finally {
353
- await new Promise((resolve, reject) => {
354
- connection.destroy((err, conn) => {
355
- if (err) {
356
- reject(new common_1.WarehouseConnectionError(err.message));
357
- }
358
- resolve(conn);
359
- });
360
- });
421
+ await this.destroyConnection(connection, this.connectionOptions.authenticator);
361
422
  }
362
423
  }
363
424
  async executeAsyncQuery({ sql, values, tags, timezone }, resultsStreamCallback) {
@@ -451,14 +512,7 @@ class SnowflakeWarehouseClient extends WarehouseBaseClient_1.default {
451
512
  throw this.parseError(error, sql);
452
513
  }
453
514
  finally {
454
- await new Promise((resolve, reject) => {
455
- connection.destroy((err, conn) => {
456
- if (err) {
457
- reject(new common_1.WarehouseConnectionError(err.message));
458
- }
459
- resolve(conn);
460
- });
461
- });
515
+ await this.destroyConnection(connection, this.connectionOptions.authenticator);
462
516
  }
463
517
  }
464
518
  async executeStreamStatement(connection, sqlText, streamCallback, options) {
@@ -522,10 +576,7 @@ class SnowflakeWarehouseClient extends WarehouseBaseClient_1.default {
522
576
  }
523
577
  async runTableCatalogQuery(database, schema, table) {
524
578
  const sqlText = `SHOW COLUMNS IN TABLE ${table}`;
525
- const connection = await this.getConnection({
526
- schema,
527
- database,
528
- });
579
+ const connection = await this.getConnection({ schema, database });
529
580
  try {
530
581
  return await this.executeStatements(connection, sqlText);
531
582
  }
@@ -536,14 +587,7 @@ class SnowflakeWarehouseClient extends WarehouseBaseClient_1.default {
536
587
  return undefined;
537
588
  }
538
589
  finally {
539
- await new Promise((resolve, reject) => {
540
- connection.destroy((err, conn) => {
541
- if (err) {
542
- reject(new common_1.WarehouseConnectionError(err.message));
543
- }
544
- resolve(conn);
545
- });
546
- });
590
+ await this.destroyConnection(connection, this.connectionOptions.authenticator);
547
591
  }
548
592
  }
549
593
  async getCatalog(config) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightdash/warehouses",
3
- "version": "0.2100.0",
3
+ "version": "0.2101.0",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -14,10 +14,10 @@
14
14
  "lodash": "^4.17.21",
15
15
  "pg": "^8.13.1",
16
16
  "pg-cursor": "^2.10.0",
17
- "snowflake-sdk": "~2.1.3",
17
+ "snowflake-sdk": "~2.3.1",
18
18
  "ssh2": "^1.14.0",
19
19
  "trino-client": "0.2.6",
20
- "@lightdash/common": "0.2100.0"
20
+ "@lightdash/common": "0.2101.0"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@types/pg": "^8.11.10",