From e4eadf858e1c4ab1f271af58a0b7954f8e131784 Mon Sep 17 00:00:00 2001 From: Ruy Adorno Date: Sat, 6 May 2023 10:02:15 -0300 Subject: [PATCH] feat: add connector factory method (#1540) This adds a new `connector` config option that may be used to define a custom socket factory method, providing much more flexible control of the socket connection creation. --- examples/custom-connector.js | 30 +++++++++++++++++ src/connection.ts | 25 +++++++++++--- test/unit/custom-connector.js | 62 +++++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 examples/custom-connector.js create mode 100644 test/unit/custom-connector.js diff --git a/examples/custom-connector.js b/examples/custom-connector.js new file mode 100644 index 000000000..45057addd --- /dev/null +++ b/examples/custom-connector.js @@ -0,0 +1,30 @@ +var net = require('net'); +var Connection = require('../lib/tedious').Connection; + +var config = { + server: '192.168.1.212', + authentication: { + type: 'default', + options: { + userName: 'test', + password: 'test' + } + }, + options: { + connector: async () => net.connect({ + host: '192.168.1.212', + port: 1433, + }) + } +}; + +const connection = new Connection(config); + +connection.connect((err) => { + if (err) { + console.log('Connection Failed'); + throw err; + } + + console.log('Custom connection Succeeded'); +}); diff --git a/src/connection.ts b/src/connection.ts index 20d35fd6c..4241efc85 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -343,6 +343,7 @@ export interface InternalConnectionOptions { columnEncryptionSetting: boolean; columnNameReplacer: undefined | ((colName: string, index: number, metadata: Metadata) => string); connectionRetryInterval: number; + connector: undefined | (() => Promise); connectTimeout: number; connectionIsolationLevel: typeof ISOLATION_LEVEL[keyof typeof ISOLATION_LEVEL]; cryptoCredentialsDetails: SecureContextOptions; @@ -535,6 +536,13 @@ export interface ConnectionOptions { */ connectionRetryInterval?: number; + /** + * Custom connector factory method. + * + * (default: `undefined`) + */ + connector?: () => Promise; + /** * The number of milliseconds before the attempt to connect is considered failed * @@ -1222,6 +1230,7 @@ class Connection extends EventEmitter { columnNameReplacer: undefined, connectionRetryInterval: DEFAULT_CONNECT_RETRY_INTERVAL, connectTimeout: DEFAULT_CONNECT_TIMEOUT, + connector: undefined, connectionIsolationLevel: ISOLATION_LEVEL.READ_COMMITTED, cryptoCredentialsDetails: {}, database: undefined, @@ -1330,6 +1339,14 @@ class Connection extends EventEmitter { this.config.options.connectTimeout = config.options.connectTimeout; } + if (config.options.connector !== undefined) { + if (typeof config.options.connector !== 'function') { + throw new TypeError('The "config.options.connector" property must be a function.'); + } + + this.config.options.connector = config.options.connector; + } + if (config.options.cryptoCredentialsDetails !== undefined) { if (typeof config.options.cryptoCredentialsDetails !== 'object' || config.options.cryptoCredentialsDetails === null) { throw new TypeError('The "config.options.cryptoCredentialsDetails" property must be of type Object.'); @@ -1884,7 +1901,7 @@ class Connection extends EventEmitter { const signal = this.createConnectTimer(); if (this.config.options.port) { - return this.connectOnPort(this.config.options.port, this.config.options.multiSubnetFailover, signal); + return this.connectOnPort(this.config.options.port, this.config.options.multiSubnetFailover, signal, this.config.options.connector); } else { return instanceLookup({ server: this.config.server, @@ -1893,7 +1910,7 @@ class Connection extends EventEmitter { signal: signal }).then((port) => { process.nextTick(() => { - this.connectOnPort(port, this.config.options.multiSubnetFailover, signal); + this.connectOnPort(port, this.config.options.multiSubnetFailover, signal, this.config.options.connector); }); }, (err) => { this.clearConnectTimer(); @@ -1956,14 +1973,14 @@ class Connection extends EventEmitter { return new TokenStreamParser(message, this.debug, handler, this.config.options); } - connectOnPort(port: number, multiSubnetFailover: boolean, signal: AbortSignal) { + connectOnPort(port: number, multiSubnetFailover: boolean, signal: AbortSignal, customConnector?: () => Promise) { const connectOpts = { host: this.routingData ? this.routingData.server : this.config.server, port: this.routingData ? this.routingData.port : port, localAddress: this.config.options.localAddress }; - const connect = multiSubnetFailover ? connectInParallel : connectInSequence; + const connect = customConnector || (multiSubnetFailover ? connectInParallel : connectInSequence); connect(connectOpts, dns.lookup, signal).then((socket) => { process.nextTick(() => { diff --git a/test/unit/custom-connector.js b/test/unit/custom-connector.js new file mode 100644 index 000000000..36e49d19d --- /dev/null +++ b/test/unit/custom-connector.js @@ -0,0 +1,62 @@ +const net = require('net'); +const assert = require('chai').assert; + +const { Connection } = require('../../src/tedious'); + +describe('custom connector', function() { + let server; + + beforeEach(function(done) { + server = net.createServer(); + server.listen(0, '127.0.0.1', done); + }); + + afterEach(() => { + server.close(); + }); + + it('connection using a custom connector', function(done) { + let attemptedConnection = false; + let customConnectorCalled = false; + + server.on('connection', async (connection) => { + attemptedConnection = true; + // no need to test auth/login, just end the connection sooner + connection.end(); + }); + + const host = server.address().address; + const port = server.address().port; + const connection = new Connection({ + server: host, + options: { + connector: async () => { + customConnectorCalled = true; + return net.connect({ + host, + port, + }); + }, + port + }, + }); + + connection.on('end', (err) => { + // validates the connection was stablished using the custom connector + assert.isOk(attemptedConnection); + assert.isOk(customConnectorCalled); + + connection.close(); + done(); + }); + + connection.on('error', (err) => { + // Connection lost errors are expected due to ending connection sooner + if (!/Connection lost/.test(err)) { + throw err; + } + }); + + connection.connect(); + }); +});