@lightdash/warehouses 0.2880.0 → 0.2881.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.
@@ -1 +1 @@
1
- {"version":3,"file":"AthenaWarehouseClient.d.ts","sourceRoot":"","sources":["../../src/warehouseClients/AthenaWarehouseClient.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,YAAY,EAOf,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAGH,uBAAuB,EAGvB,MAAM,EAEN,mBAAmB,EACnB,gBAAgB,EAGhB,gBAAgB,EAChB,cAAc,EACjB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAM5C,OAAO,mBAAmB,MAAM,uBAAuB,CAAC;AACxD,OAAO,uBAAuB,MAAM,2BAA2B,CAAC;AAEhE,oBAAY,WAAW;IACnB,OAAO,YAAY;IACnB,OAAO,YAAY;IACnB,QAAQ,aAAa;IACrB,OAAO,YAAY;IACnB,MAAM,WAAW;IACjB,IAAI,SAAS;IACb,MAAM,WAAW;IACjB,OAAO,YAAY;IACnB,OAAO,YAAY;IACnB,IAAI,SAAS;IACb,SAAS,cAAc;IACvB,IAAI,SAAS;IACb,IAAI,SAAS;IACb,IAAI,SAAS;IACb,OAAO,wBAAwB;IAC/B,SAAS,cAAc;IACvB,YAAY,6BAA6B;IACzC,KAAK,UAAU;IACf,GAAG,QAAQ;IACX,GAAG,QAAQ;IACX,SAAS,cAAc;IACvB,IAAI,SAAS;CAChB;AAgCD,qBAAa,gBAAiB,SAAQ,uBAAuB;IACzD,QAAQ,CAAC,IAAI,yBAAyB;IAEtC,cAAc,IAAI,mBAAmB;IAIrC,wBAAwB,IAAI,MAAM;IAIlC,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM;IAajD,eAAe,IAAI,MAAM;IAIzB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;IAmBnC,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB,GAAG,MAAM;IAM7D,uBAAuB,CACnB,iBAAiB,EAAE,MAAM,EACzB,eAAe,EAAE,MAAM,GACxB,MAAM;IAKT,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM;CAIzC;AAKD,qBAAa,qBAAsB,SAAQ,mBAAmB,CAAC,uBAAuB,CAAC;IACnF,MAAM,EAAE,YAAY,CAAC;gBAET,WAAW,EAAE,uBAAuB;YAwDlC,sBAAsB;IAiDpC,OAAO,CAAC,UAAU;IA8BZ,WAAW,CACb,GAAG,EAAE,MAAM,EACX,cAAc,EAAE,CAAC,IAAI,EAAE,gBAAgB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,EAChE,OAAO,EAAE;QACL,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC;KACrB,GACF,OAAO,CAAC,IAAI,CAAC;IAyGV,UAAU,CACZ,QAAQ,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,GAChE,OAAO,CAAC,gBAAgB,CAAC;IAwDtB,YAAY,IAAI,OAAO,CACzB;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CACxD;IAyCK,SAAS,CACX,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,gBAAgB,CAAC;IA2C5B,UAAU,CAAC,KAAK,EAAE,KAAK,GAAG,KAAK;CAGlC"}
1
+ {"version":3,"file":"AthenaWarehouseClient.d.ts","sourceRoot":"","sources":["../../src/warehouseClients/AthenaWarehouseClient.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,YAAY,EAOf,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAGH,uBAAuB,EAGvB,MAAM,EAEN,mBAAmB,EACnB,gBAAgB,EAGhB,gBAAgB,EAChB,cAAc,EACjB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAM5C,OAAO,mBAAmB,MAAM,uBAAuB,CAAC;AACxD,OAAO,uBAAuB,MAAM,2BAA2B,CAAC;AAEhE,oBAAY,WAAW;IACnB,OAAO,YAAY;IACnB,OAAO,YAAY;IACnB,QAAQ,aAAa;IACrB,OAAO,YAAY;IACnB,MAAM,WAAW;IACjB,IAAI,SAAS;IACb,MAAM,WAAW;IACjB,OAAO,YAAY;IACnB,OAAO,YAAY;IACnB,IAAI,SAAS;IACb,SAAS,cAAc;IACvB,IAAI,SAAS;IACb,IAAI,SAAS;IACb,IAAI,SAAS;IACb,OAAO,wBAAwB;IAC/B,SAAS,cAAc;IACvB,YAAY,6BAA6B;IACzC,KAAK,UAAU;IACf,GAAG,QAAQ;IACX,GAAG,QAAQ;IACX,SAAS,cAAc;IACvB,IAAI,SAAS;CAChB;AA4HD,qBAAa,gBAAiB,SAAQ,uBAAuB;IACzD,QAAQ,CAAC,IAAI,yBAAyB;IAEtC,cAAc,IAAI,mBAAmB;IAIrC,wBAAwB,IAAI,MAAM;IAIlC,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM;IAajD,eAAe,IAAI,MAAM;IAIzB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;IAmBnC,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB,GAAG,MAAM;IAM7D,uBAAuB,CACnB,iBAAiB,EAAE,MAAM,EACzB,eAAe,EAAE,MAAM,GACxB,MAAM;IAKT,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM;CAIzC;AAKD,qBAAa,qBAAsB,SAAQ,mBAAmB,CAAC,uBAAuB,CAAC;IACnF,MAAM,EAAE,YAAY,CAAC;gBAET,WAAW,EAAE,uBAAuB;YAwDlC,sBAAsB;IAiDpC,OAAO,CAAC,UAAU;IA8BZ,WAAW,CACb,GAAG,EAAE,MAAM,EACX,cAAc,EAAE,CAAC,IAAI,EAAE,gBAAgB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,EAChE,OAAO,EAAE;QACL,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC;KACrB,GACF,OAAO,CAAC,IAAI,CAAC;IAyGV,UAAU,CACZ,QAAQ,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,GAChE,OAAO,CAAC,gBAAgB,CAAC;IAuDtB,YAAY,IAAI,OAAO,CACzB;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CACxD;IAwCK,SAAS,CACX,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,gBAAgB,CAAC;IA0C5B,UAAU,CAAC,KAAK,EAAE,KAAK,GAAG,KAAK;CAGlC"}
@@ -62,6 +62,78 @@ const convertDataTypeToDimensionType = (type) => {
62
62
  };
63
63
  // Force lowercase for column names
64
64
  const normalizeColumnName = (columnName) => columnName.toLowerCase();
65
+ const AWS_AUTH_ERROR_NAMES = new Set([
66
+ 'UnrecognizedClientException',
67
+ 'InvalidClientTokenId',
68
+ 'InvalidSignatureException',
69
+ 'SignatureDoesNotMatch',
70
+ 'ExpiredToken',
71
+ 'ExpiredTokenException',
72
+ 'CredentialsProviderError',
73
+ 'CredentialsError',
74
+ 'MissingAuthenticationToken',
75
+ 'MissingAuthenticationTokenException',
76
+ ]);
77
+ const getAuthErrorHint = (awsErrorName) => {
78
+ switch (awsErrorName) {
79
+ case 'UnrecognizedClientException':
80
+ case 'InvalidClientTokenId':
81
+ return 'AWS rejected the access key ID. Check the access key in your project settings is correct and the IAM user is enabled.';
82
+ case 'InvalidSignatureException':
83
+ case 'SignatureDoesNotMatch':
84
+ return 'AWS rejected the request signature. Check the secret access key matches the access key ID, and that your system clock is in sync.';
85
+ case 'ExpiredToken':
86
+ case 'ExpiredTokenException':
87
+ return 'AWS credentials have expired. If you are using temporary or assume-role credentials, generate new ones.';
88
+ case 'CredentialsProviderError':
89
+ case 'CredentialsError':
90
+ case 'MissingAuthenticationToken':
91
+ case 'MissingAuthenticationTokenException':
92
+ return 'No AWS credentials could be loaded. If using IAM Role authentication, make sure the host has an attached role with Athena access.';
93
+ default:
94
+ return '';
95
+ }
96
+ };
97
+ // Translates a raw error from the AWS SDK (or anywhere else thrown out of an
98
+ // Athena client.send call) into a Lightdash error. Auth/credential failures
99
+ // are surfaced as WarehouseConnectionError so the UI categorizes them as a
100
+ // project-config problem rather than a query problem.
101
+ const translateAthenaError = (error, options = {}) => {
102
+ const err = error;
103
+ const awsErrorName = typeof err?.name === 'string' &&
104
+ err.name !== 'Error' &&
105
+ !err.name.startsWith('Warehouse')
106
+ ? err.name
107
+ : undefined;
108
+ const httpStatusCode = typeof err?.$metadata?.httpStatusCode === 'number'
109
+ ? err.$metadata.httpStatusCode
110
+ : undefined;
111
+ const baseMessage = (0, common_1.getErrorMessage)(error);
112
+ const hint = awsErrorName ? getAuthErrorHint(awsErrorName) : '';
113
+ // e.g. "[UnrecognizedClientException 403]" or "[403]" if name is missing.
114
+ const tag = [awsErrorName, httpStatusCode]
115
+ .filter((p) => p !== undefined && p !== '')
116
+ .join(' ');
117
+ const messageParts = [
118
+ options.contextPrefix,
119
+ tag.length > 0 ? `[${tag}]` : null,
120
+ baseMessage,
121
+ hint,
122
+ ].filter((p) => typeof p === 'string' && p.length > 0);
123
+ const fullMessage = messageParts.join(' ');
124
+ // Auth signal precedence: known AWS error name first; otherwise fall back
125
+ // to HTTP 401 which is unambiguously an authentication failure. We do
126
+ // *not* fall back on 403 — it also covers IAM permission gaps (e.g.
127
+ // missing athena:ListTableMetadata), which are query-time problems and
128
+ // belong in WarehouseQueryError unless the caller asks otherwise.
129
+ const isAuthError = (awsErrorName !== undefined &&
130
+ AWS_AUTH_ERROR_NAMES.has(awsErrorName)) ||
131
+ httpStatusCode === 401;
132
+ if (isAuthError || options.defaultErrorClass === 'connection') {
133
+ return new common_1.WarehouseConnectionError(fullMessage);
134
+ }
135
+ return new common_1.WarehouseQueryError(fullMessage);
136
+ };
65
137
  class AthenaSqlBuilder extends WarehouseBaseSqlBuilder_1.default {
66
138
  constructor() {
67
139
  super(...arguments);
@@ -313,7 +385,10 @@ class AthenaWarehouseClient extends WarehouseBaseClient_1.default {
313
385
  if (error.name === 'MetadataException') {
314
386
  return undefined;
315
387
  }
316
- throw new common_1.WarehouseConnectionError(`Failed to fetch table metadata for '${database}.${schema}.${table}'. ${(0, common_1.getErrorMessage)(e)}`);
388
+ throw translateAthenaError(e, {
389
+ contextPrefix: `Failed to fetch table metadata for '${database}.${schema}.${table}'.`,
390
+ defaultErrorClass: 'connection',
391
+ });
317
392
  }
318
393
  });
319
394
  return results.reduce((acc, result) => {
@@ -354,7 +429,10 @@ class AthenaWarehouseClient extends WarehouseBaseClient_1.default {
354
429
  } while (nextToken);
355
430
  }
356
431
  catch (e) {
357
- throw new common_1.WarehouseConnectionError(`Failed to list tables in '${this.credentials.database}.${this.credentials.schema}'. ${(0, common_1.getErrorMessage)(e)}`);
432
+ throw translateAthenaError(e, {
433
+ contextPrefix: `Failed to list tables in '${this.credentials.database}.${this.credentials.schema}'.`,
434
+ defaultErrorClass: 'connection',
435
+ });
358
436
  }
359
437
  return tables;
360
438
  }
@@ -381,11 +459,14 @@ class AthenaWarehouseClient extends WarehouseBaseClient_1.default {
381
459
  return result;
382
460
  }
383
461
  catch (e) {
384
- throw new common_1.WarehouseConnectionError(`Failed to get fields for table '${db}.${sch}.${tableName}'. ${(0, common_1.getErrorMessage)(e)}`);
462
+ throw translateAthenaError(e, {
463
+ contextPrefix: `Failed to get fields for table '${db}.${sch}.${tableName}'.`,
464
+ defaultErrorClass: 'connection',
465
+ });
385
466
  }
386
467
  }
387
468
  parseError(error) {
388
- return new common_1.WarehouseQueryError((0, common_1.getErrorMessage)(error));
469
+ return translateAthenaError(error);
389
470
  }
390
471
  }
391
472
  exports.AthenaWarehouseClient = AthenaWarehouseClient;
@@ -103,4 +103,100 @@ describe('AthenaWarehouseClient', () => {
103
103
  });
104
104
  });
105
105
  });
106
+ describe('error translation', () => {
107
+ // Synthesizes an error with the shape produced by the AWS SDK:
108
+ // an Error subclass whose `name` is the AWS error code, with optional
109
+ // $metadata.httpStatusCode (set by the SDK on every ServiceException).
110
+ const makeAwsError = (name, message, httpStatusCode) => {
111
+ const err = new Error(message);
112
+ err.name = name;
113
+ if (httpStatusCode !== undefined) {
114
+ err.$metadata = { httpStatusCode };
115
+ }
116
+ return err;
117
+ };
118
+ const setMockSendToReject = (error) => {
119
+ mockAthenaClient.mockImplementation(() => ({
120
+ send: jest.fn().mockRejectedValue(error),
121
+ }));
122
+ };
123
+ test('translates UnrecognizedClientException into WarehouseConnectionError with hint', async () => {
124
+ setMockSendToReject(makeAwsError('UnrecognizedClientException', 'The security token included in the request is invalid.'));
125
+ const client = new AthenaWarehouseClient_1.AthenaWarehouseClient(baseCredentials);
126
+ await expect(client.test()).rejects.toBeInstanceOf(common_1.WarehouseConnectionError);
127
+ await expect(client.test()).rejects.toMatchObject({
128
+ message: expect.stringContaining('[UnrecognizedClientException]'),
129
+ });
130
+ await expect(client.test()).rejects.toMatchObject({
131
+ message: expect.stringContaining('AWS rejected the access key ID'),
132
+ });
133
+ });
134
+ test('translates InvalidSignatureException into WarehouseConnectionError', async () => {
135
+ setMockSendToReject(makeAwsError('InvalidSignatureException', 'Signature does not match.'));
136
+ const client = new AthenaWarehouseClient_1.AthenaWarehouseClient(baseCredentials);
137
+ await expect(client.test()).rejects.toBeInstanceOf(common_1.WarehouseConnectionError);
138
+ await expect(client.test()).rejects.toMatchObject({
139
+ message: expect.stringContaining('secret access key'),
140
+ });
141
+ });
142
+ test('translates ExpiredTokenException into WarehouseConnectionError', async () => {
143
+ setMockSendToReject(makeAwsError('ExpiredTokenException', 'The security token has expired.'));
144
+ const client = new AthenaWarehouseClient_1.AthenaWarehouseClient(baseCredentials);
145
+ await expect(client.test()).rejects.toBeInstanceOf(common_1.WarehouseConnectionError);
146
+ await expect(client.test()).rejects.toMatchObject({
147
+ message: expect.stringContaining('expired'),
148
+ });
149
+ });
150
+ test('translates CredentialsProviderError into WarehouseConnectionError', async () => {
151
+ setMockSendToReject(makeAwsError('CredentialsProviderError', 'Could not load credentials from any providers'));
152
+ const client = new AthenaWarehouseClient_1.AthenaWarehouseClient(baseCredentials);
153
+ await expect(client.test()).rejects.toBeInstanceOf(common_1.WarehouseConnectionError);
154
+ await expect(client.test()).rejects.toMatchObject({
155
+ message: expect.stringContaining('IAM Role'),
156
+ });
157
+ });
158
+ test('keeps non-auth errors as WarehouseQueryError from streamQuery', async () => {
159
+ setMockSendToReject(makeAwsError('InvalidRequestException', 'Workgroup primary not found'));
160
+ const client = new AthenaWarehouseClient_1.AthenaWarehouseClient(baseCredentials);
161
+ await expect(client.test()).rejects.toBeInstanceOf(common_1.WarehouseQueryError);
162
+ await expect(client.test()).rejects.toMatchObject({
163
+ message: expect.stringContaining('[InvalidRequestException]'),
164
+ });
165
+ });
166
+ test('falls back to httpStatusCode=401 when error name is unfamiliar', async () => {
167
+ // Some AWS error variants don't show up in our known-name set but
168
+ // are still authentication failures (e.g. credential-provider chain
169
+ // wrapping). HTTP 401 alone should be enough to classify as a
170
+ // connection error.
171
+ setMockSendToReject(makeAwsError('SomeFutureAwsAuthError', 'unauthenticated', 401));
172
+ const client = new AthenaWarehouseClient_1.AthenaWarehouseClient(baseCredentials);
173
+ await expect(client.test()).rejects.toBeInstanceOf(common_1.WarehouseConnectionError);
174
+ await expect(client.test()).rejects.toMatchObject({
175
+ message: expect.stringContaining('[SomeFutureAwsAuthError 401]'),
176
+ });
177
+ });
178
+ test('does NOT promote httpStatusCode=403 to a connection error (could be IAM gap)', async () => {
179
+ // AccessDeniedException is 403 but represents an IAM permission
180
+ // gap during a query — it should remain WarehouseQueryError when
181
+ // streamQuery is the caller. The catalog/tables/fields paths
182
+ // explicitly opt into 'connection' default elsewhere.
183
+ setMockSendToReject(makeAwsError('AccessDeniedException', 'You are not authorized to perform: athena:GetQueryResults', 403));
184
+ const client = new AthenaWarehouseClient_1.AthenaWarehouseClient(baseCredentials);
185
+ await expect(client.test()).rejects.toBeInstanceOf(common_1.WarehouseQueryError);
186
+ await expect(client.test()).rejects.toMatchObject({
187
+ message: expect.stringContaining('[AccessDeniedException 403]'),
188
+ });
189
+ });
190
+ test('catalog/tables/fields default to WarehouseConnectionError', async () => {
191
+ setMockSendToReject(makeAwsError('AccessDeniedException', 'You are not authorized to perform: athena:ListTableMetadata'));
192
+ const client = new AthenaWarehouseClient_1.AthenaWarehouseClient(baseCredentials);
193
+ await expect(client.getAllTables()).rejects.toBeInstanceOf(common_1.WarehouseConnectionError);
194
+ await expect(client.getAllTables()).rejects.toMatchObject({
195
+ message: expect.stringContaining('[AccessDeniedException]'),
196
+ });
197
+ await expect(client.getAllTables()).rejects.toMatchObject({
198
+ message: expect.stringContaining('Failed to list tables'),
199
+ });
200
+ });
201
+ });
106
202
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightdash/warehouses",
3
- "version": "0.2880.0",
3
+ "version": "0.2881.0",
4
4
  "description": "Warehouse connectors for Lightdash",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -27,7 +27,7 @@
27
27
  "snowflake-sdk": "2.3.4",
28
28
  "ssh2": "1.14.0",
29
29
  "trino-client": "0.2.9",
30
- "@lightdash/common": "0.2880.0"
30
+ "@lightdash/common": "0.2881.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node-fetch": "2.6.13",