diff --git a/package-lock.json b/package-lock.json index 2150d9d91..d301e4347 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4790,6 +4790,14 @@ "@types/node": "*" } }, + "@types/es-aggregate-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/es-aggregate-error/-/es-aggregate-error-1.0.2.tgz", + "integrity": "sha512-erqUpFXksaeR2kejKnhnjZjbFxUpGZx4Z7ydNL9ie8tEhXPiZTsLeUDJ6aR1F8j5wWUAtOAQWUqkc7givBJbBA==", + "requires": { + "@types/node": "*" + } + }, "@types/json-schema": { "version": "7.0.9", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", @@ -6239,7 +6247,7 @@ "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", "dev": true, "optional": true, "requires": { @@ -6615,6 +6623,53 @@ } } }, + "es-aggregate-error": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/es-aggregate-error/-/es-aggregate-error-1.0.7.tgz", + "integrity": "sha512-Zob/KUAOQUTvCzqgL2FEgtqNLPX5rYW3VhlldZ6La4W2mJUv3J8DmD0/sPxuD+kSAKF6nuceJ3uOSvCaaTLUyQ==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "function-bind": "^1.1.1", + "functions-have-names": "^1.2.2", + "get-intrinsic": "^1.1.1", + "globalthis": "^1.0.2" + }, + "dependencies": { + "es-abstract": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz", + "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==", + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.1.1", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-symbols": "^1.0.2", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.4", + "is-negative-zero": "^2.0.1", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.1", + "is-string": "^1.0.7", + "is-weakref": "^1.0.1", + "object-inspect": "^1.11.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.4", + "string.prototype.trimstart": "^1.0.4", + "unbox-primitive": "^1.0.1" + } + }, + "object-inspect": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", + "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==" + } + } + }, "es-to-primitive": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", @@ -7462,6 +7517,11 @@ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", "dev": true }, + "functions-have-names": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.2.tgz", + "integrity": "sha512-bLgc3asbWdwPbx2mNk2S49kmJCuQeu0nfmaOgbs8WIyzzkw3r4htszdIi9Q9EMezDPTYuJx2wvjZ/EwgAthpnA==" + }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -7511,6 +7571,15 @@ "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "dev": true }, + "get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, "get-value": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", @@ -7619,6 +7688,14 @@ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true }, + "globalthis": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.2.tgz", + "integrity": "sha512-ZQnSFO1la8P7auIOQECnm0sSuoMeaSq0EEdXMBFF2QJO4uNcwbyhSgG3MruWNbFTqCLmxVwGOl7LZ9kASvHdeQ==", + "requires": { + "define-properties": "^1.1.3" + } + }, "globby": { "version": "11.0.4", "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz", @@ -8221,6 +8298,11 @@ "has-tostringtag": "^1.0.0" } }, + "is-shared-array-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz", + "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==" + }, "is-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", @@ -8266,6 +8348,14 @@ "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "dev": true }, + "is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "requires": { + "call-bind": "^1.0.2" + } + }, "is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", @@ -12014,7 +12104,7 @@ "p-limit": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "integrity": "sha1-uGvV8MJWkJEcdZD8v8IBDVSzzLg=", "dev": true, "requires": { "p-try": "^1.0.0" diff --git a/package.json b/package.json index 28a5a8d63..0514bfefd 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,9 @@ "@azure/identity": "^2.0.1", "@azure/keyvault-keys": "^4.3.0", "@js-joda/core": "^4.0.0", + "@types/es-aggregate-error": "^1.0.2", "bl": "^5.0.0", + "es-aggregate-error": "^1.0.7", "iconv-lite": "^0.6.3", "jsbi": "^3.2.1", "native-duplexpair": "^1.0.0", diff --git a/src/connection.ts b/src/connection.ts index 078532f9f..70ff8c2a3 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -44,6 +44,7 @@ import { Parameter, TYPES } from './data-type'; import { BulkLoadPayload } from './bulk-load-payload'; import { Collation } from './collation'; +import AggregateError from 'es-aggregate-error'; import { version } from '../package.json'; import { URL } from 'url'; import { AttentionTokenHandler, InitialSqlTokenHandler, Login7TokenHandler, RequestTokenHandler, TokenHandler } from './token/handler'; @@ -938,7 +939,7 @@ class Connection extends EventEmitter { /** * @private */ - loginError: undefined | ConnectionError; + loginError: undefined | AggregateError | ConnectionError; /** * @private */ @@ -3163,6 +3164,13 @@ function emitAzureADPasswordClientIdDeprecationWarning() { ); } +function isTransientError(error: AggregateError | ConnectionError): boolean { + if (error instanceof AggregateError) { + error = error.errors[0]; + } + return (error instanceof ConnectionError) && !!error.isTransient; +} + export default Connection; module.exports = Connection; @@ -3352,7 +3360,7 @@ Connection.prototype.STATE = { this.transitionTo(this.STATE.LOGGED_IN_SENDING_INITIAL_SQL); } } else if (this.loginError) { - if (this.loginError.isTransient) { + if (isTransientError(this.loginError)) { this.debug.log('Initiating retry on transient error'); this.transitionTo(this.STATE.TRANSIENT_FAILURE_RETRY); } else { @@ -3418,7 +3426,7 @@ Connection.prototype.STATE = { this.socketError(err); }); } else if (this.loginError) { - if (this.loginError.isTransient) { + if (isTransientError(this.loginError)) { this.debug.log('Initiating retry on transient error'); this.transitionTo(this.STATE.TRANSIENT_FAILURE_RETRY); } else { @@ -3510,7 +3518,8 @@ Connection.prototype.STATE = { getToken((err, token) => { if (err) { - this.loginError = new ConnectionError('Security token could not be authenticated or authorized.', 'EFEDAUTH'); + this.loginError = new AggregateError( + [new ConnectionError('Security token could not be authenticated or authorized.', 'EFEDAUTH'), err]); this.emit('connect', this.loginError); this.transitionTo(this.STATE.FINAL); return; @@ -3519,7 +3528,7 @@ Connection.prototype.STATE = { this.sendFedAuthTokenMessage(token!); }); } else if (this.loginError) { - if (this.loginError.isTransient) { + if (isTransientError(this.loginError)) { this.debug.log('Initiating retry on transient error'); this.transitionTo(this.STATE.TRANSIENT_FAILURE_RETRY); } else { diff --git a/src/token/handler.ts b/src/token/handler.ts index 20a5df623..8d0088c9d 100644 --- a/src/token/handler.ts +++ b/src/token/handler.ts @@ -33,6 +33,8 @@ import { } from './token'; import BulkLoad from '../bulk-load'; +import AggregateError from 'es-aggregate-error'; + export class UnexpectedTokenError extends Error { constructor(handler: TokenHandler, token: Token) { super('Unexpected token `' + token.name + '` in `' + handler.constructor.name + '`'); @@ -368,12 +370,14 @@ export class Login7TokenHandler extends TokenHandler { export class RequestTokenHandler extends TokenHandler { connection: Connection; request: Request | BulkLoad; + errors: RequestError[]; constructor(connection: Connection, request: Request | BulkLoad) { super(); this.connection = connection; this.request = request; + this.errors = []; } onInfoMessage(token: InfoMessageToken) { @@ -392,8 +396,11 @@ export class RequestTokenHandler extends TokenHandler { error.serverName = token.serverName; error.procName = token.procName; error.lineNumber = token.lineNumber; - + this.errors.push(error); this.request.error = error; + if (this.request instanceof Request && this.errors.length > 1) { + this.request.error = new AggregateError(this.errors); + } } } diff --git a/test/integration/errors-test.js b/test/integration/errors-test.js index 3caf6f9b5..ef51cce00 100644 --- a/test/integration/errors-test.js +++ b/test/integration/errors-test.js @@ -4,8 +4,9 @@ const fs = require('fs'); const assert = require('chai').assert; const debug = false; -import Connection from '../../src/connection'; +import AggregateError from 'es-aggregate-error'; import { RequestError } from '../../src/errors'; +import Connection from '../../src/connection'; import Request from '../../src/request'; const config = JSON.parse( @@ -182,4 +183,65 @@ describe('Errors Test', function() { done(); }); }); + + it('should throw aggregate error with two error messages', function(done) { + const connection = new Connection(config); + + connection.connect((err) => { + if (err) { + return done(err); + } + + const request = new Request('create type test_type as table ( id int, primary key (code) );', (error) => { + assert.instanceOf(error, AggregateError); + + if (error instanceof AggregateError) { + assert.strictEqual(error.errors.length, 2); + assert.strictEqual(error.errors[0].message, 'Column name \'code\' does not exist in the target table or view.'); + assert.strictEqual(error.errors[1].message, 'Could not create constraint or index. See previous errors.'); + } + + connection.close(); + }); + + connection.execSql(request); + }); + + connection.on('end', function() { + done(); + }); + }); + + it('should throw aggregate error with AAD token retrieve', function(done) { + config.server = 'help.kusto.windows.net'; + config.authentication = { + type: 'azure-active-directory-password', + options: { + userName: 'username', + password: 'password', + // Lack of tenantId will generate a AAD token retrieve error + clientId: 'clientID', + } + }; + config.options.tdsVersion = '7_4'; + const connection = new Connection(config); + + /** @type {Error | undefined} */ + let connectionError; + connection.connect((err) => { + connectionError = err; + }); + + connection.on('end', function() { + assert.instanceOf(connectionError, AggregateError); + + if (connectionError instanceof AggregateError) { + assert.strictEqual(connectionError.errors.length, 2); + assert.strictEqual(connectionError.errors[0].message, 'Security token could not be authenticated or authorized.'); + assert.include(connectionError.errors[1].message, 'The grant type is not supported over the /common or /consumers endpoints.'); + } + + done(); + }); + }); });