Skip to content

Commit

Permalink
feat: add connector factory method (#1540)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ruyadorno authored May 6, 2023
1 parent d075b9c commit e4eadf8
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 4 deletions.
30 changes: 30 additions & 0 deletions examples/custom-connector.js
Original file line number Diff line number Diff line change
@@ -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');
});
25 changes: 21 additions & 4 deletions src/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ export interface InternalConnectionOptions {
columnEncryptionSetting: boolean;
columnNameReplacer: undefined | ((colName: string, index: number, metadata: Metadata) => string);
connectionRetryInterval: number;
connector: undefined | (() => Promise<Socket>);
connectTimeout: number;
connectionIsolationLevel: typeof ISOLATION_LEVEL[keyof typeof ISOLATION_LEVEL];
cryptoCredentialsDetails: SecureContextOptions;
Expand Down Expand Up @@ -535,6 +536,13 @@ export interface ConnectionOptions {
*/
connectionRetryInterval?: number;

/**
* Custom connector factory method.
*
* (default: `undefined`)
*/
connector?: () => Promise<Socket>;

/**
* The number of milliseconds before the attempt to connect is considered failed
*
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.');
Expand Down Expand Up @@ -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,
Expand All @@ -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();
Expand Down Expand Up @@ -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<Socket>) {
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(() => {
Expand Down
62 changes: 62 additions & 0 deletions test/unit/custom-connector.js
Original file line number Diff line number Diff line change
@@ -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();
});
});

0 comments on commit e4eadf8

Please sign in to comment.