@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;
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
30
|
+
"@lightdash/common": "0.2881.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/node-fetch": "2.6.13",
|