Skip to content

Commit

Permalink
feat: add connector factory method
Browse files Browse the repository at this point in the history
This changeset adds a new `connector` config option that may be used to
define a custom socket factory method. Providing a much more flexible
control of the socket connection creation.

Defining a custom `connector` config value allows Tedious to support a
larger variety of environments/setups such as proxy servers using secure
socket connections that are used by cloud providers such as GCP.

Linked below, the `pg` driver for PostgreSQL and the `mysql2` driver for
MySQL are prior art example of this pattern. Also linked below is the
Cloud SQL Node.js Connector, which demonstrates how third-party libraries
can leverage the custom socket factory method.

Refs: https://github.com/sidorares/node-mysql2/blob/ba15fe25703665e516ab0a23af8d828d1473b8c3/lib/connection.js#L63-L65
Refs: https://github.com/brianc/node-postgres/blob/b357e1884ad25b23a4ab034b443ddfc8c8261951/packages/pg/lib/connection.js#L20
Refs: https://github.com/GoogleCloudPlatform/cloud-sql-nodejs-connector
Signed-off-by: Ruy Adorno <ruyadorno@google.com>
  • Loading branch information
ruyadorno committed Apr 20, 2023
1 parent d075b9c commit d947ee1
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<import('net').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<import('net').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.');

Check warning on line 1344 in src/connection.ts

View check run for this annotation

Codecov / codecov/patch

src/connection.ts#L1344

Added line #L1344 was not covered by tests
}

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);

Check warning on line 1913 in src/connection.ts

View check run for this annotation

Codecov / codecov/patch

src/connection.ts#L1913

Added line #L1913 was not covered by tests
});
}, (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<import('net').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 d947ee1

Please sign in to comment.