Skip to content

Commit

Permalink
feat: add regional support (#563)
Browse files Browse the repository at this point in the history
  • Loading branch information
thinkingserious authored Apr 27, 2020
1 parent d3154d1 commit 4097475
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 13 deletions.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,28 @@ const client = require('twilio')(accountSid, authToken, {
});
```

### Specify Region and/or Edge

```javascript
var accountSid = process.env.TWILIO_ACCOUNT_SID; // Your Account SID from www.twilio.com/console
var authToken = process.env.TWILIO_AUTH_TOKEN; // Your Auth Token from www.twilio.com/console

const client = require('twilio')(accountSid, authToken, {
region: 'au1',
edge: 'sydney',
});
```

Alternatively, specify the edge and/or region after constructing the Twilio client:

```javascript
const client = require('twilio')(accountSid, authToken);
client.region = 'au1';
client.edge = 'sydney';
```

This will result in the `hostname` transforming from `api.twilio.com` to `api.sydney.au1.twilio.com`.

## Docker Image

The `Dockerfile` present in this repository and its respective `twilio/twilio-node` Docker image are currently used by Twilio for testing purposes only.
Expand Down
4 changes: 3 additions & 1 deletion lib/rest/Twilio.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,13 +146,15 @@ declare namespace Twilio {
* Options to pass to the Twilio Client constructor
*
* @property accountSid - The default accountSid. This is set to username if not provided
* @property edge - Twilio edge to use. Defaults to none
* @property env - The environment object. Defaults to process.env
* @property httpClient - The client used for http requests. Defaults to RequestClient
* @property lazyLoading - Enable lazy loading, loading time will decrease if enabled
* @property region - Twilio region to use. Defaults to none
* @property region - Twilio region to use. Defaults to us1 if edge provided
*/
export interface TwilioClientOptions {
accountSid?: string;
edge?: string;
env?: object;
httpClient?: RequestClient;
lazyLoading?: boolean;
Expand Down
55 changes: 43 additions & 12 deletions lib/rest/Twilio.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
/* jshint ignore:end */

var moduleInfo = require('../../package.json'); /* jshint ignore:line */
var _ = require('lodash'); /* jshint ignore:line */
var url = require('url'); /* jshint ignore:line */
var util = require('util'); /* jshint ignore:line */
var RestException = require('../base/RestException'); /* jshint ignore:line */

Expand Down Expand Up @@ -103,7 +103,9 @@ var RestException = require('../base/RestException'); /* jshint ignore:line */
* @param {string} [opts.accountSid] -
* The default accountSid. This is set to username if not provided
* @param {object} [opts.env] - The environment object. Defaults to process.env
* @param {string} [opts.region] - Twilio region to use. Defaults to none
* @param {string} [opts.edge] - Twilio edge to use. Defaults to none
* @param {string} [opts.region] -
* Twilio region to use. Defaults to us1 if edge provided
* @param {boolean} [opts.lazyLoading] -
* Enable lazy loading, loading time will decrease if enabled
*
Expand All @@ -122,6 +124,7 @@ function Twilio(username, password, opts) {
this._httpClient = this.httpClient;
}
this.region = opts.region;
this.edge = opts.edge;

if (!this.username) {
throw new Error('username is required');
Expand All @@ -131,7 +134,7 @@ function Twilio(username, password, opts) {
throw new Error('password is required');
}

if (!_.startsWith(this.accountSid, 'AC')) {
if (!this.accountSid.startsWith('AC')) {
throw new Error('accountSid must start with AC');
}

Expand Down Expand Up @@ -249,18 +252,12 @@ Twilio.prototype.request = function request(opts) {
headers.Accept = 'application/json';
}

var uri = opts.uri;
if (this.region) {
var parts = _.split(uri, '.');

if (parts.length > 1 && !_.isEqual(parts[1], this.region)) {
uri = _.join(_.concat([parts[0], this.region], _.slice(parts, 1)), '.');
}
}
var uri = new url.URL(opts.uri);
uri.hostname = this.getHostname(uri.hostname, this.edge, this.region);

return this.httpClient.request({
method: opts.method,
uri: uri,
uri: uri.href,
username: username,
password: password,
headers: headers,
Expand All @@ -271,6 +268,40 @@ Twilio.prototype.request = function request(opts) {
});
};

/* jshint ignore:start */
/**
* Adds a region and/or edge to a given hostname
*
* @function getHostname
* @memberof Twilio#
*
* @param {string} hostname - A URI hostname (e.g. api.twilio.com)
* @param {string} targetEdge - The targeted edge location (e.g. sydney)
* @param {string} targetRegion - The targeted region location (e.g. au1)
*/
/* jshint ignore:end */
Twilio.prototype.getHostname = function getHostname(hostname, targetEdge,
targetRegion) {
const domain = hostname.split('.').slice(-2).join('.');
const defaultRegion = 'us1';

const prefix = hostname.split('.' + domain)[0];
let [product, edge, region] = prefix.split('.');

targetRegion = targetRegion || (targetEdge && defaultRegion);
if (!targetRegion) {
return hostname;
}

if (!region || targetEdge) {
edge = targetEdge;
}

region = targetRegion;

return [product, edge, region, domain].filter(part => part).join('.');
};

/* jshint ignore:start */
/**
* Validates that a request to the new SSL certificate is successful.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"jsdoc": "^3.6.3",
"jshint": "^2.11.0",
"mock-fs": "^4.11.0",
"nock": "^10.0.6",
"node-mocks-http": "^1.8.1",
"proxyquire": "^2.1.3",
"typescript": "^2.8.3"
Expand Down
87 changes: 87 additions & 0 deletions spec/unit/rest/Twilio.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
'use strict';
const nock = require('nock');
var url = require('url'); /* jshint ignore:line */

describe('client', () => {
var client;
const twilio = require('../../../lib');

beforeEach(() => {
client = new twilio('ACXXXXXXXX', 'test-password');
});
describe('setting the region', () => {
it('should use the default region if only edge is defined', () => {
const scope = nock('https://api.edge.us1.twilio.com')
.get('/')
.reply(200, 'test response');
client.edge = 'edge';
return client.request({method: 'GET', uri: 'https://api.twilio.com'})
.then(() => scope.done());
});
it('should use the default region if only edge is defined and there is a defined region', () => {
const scope = nock('https://api.edge.us1.twilio.com')
.get('/')
.reply(200, 'test response');
client.edge = 'edge';
return client.request({method: 'GET', uri: 'https://api.region.twilio.com'})
.then(() => scope.done());
});
it('should set the region properly if only the region is specified', () => {
const scope = nock('https://api.region.twilio.com')
.get('/')
.reply(200, 'test response');
client.region = 'region';
return client.request({method: 'GET', uri: 'https://api.twilio.com'})
.then(() => scope.done());
});
it('should set the region and edge properly', () => {
const scope = nock('https://api.edge.region.twilio.com')
.get('/')
.reply(200, 'test response');
client.edge = 'edge';
client.region = 'region';
return client.request({method: 'GET', uri: 'https://api.twilio.com'})
.then(() => scope.done());
});
it('should set the region and edge properly when an edge is already included', () => {
const scope = nock('https://api.edge2.us1.twilio.com')
.get('/')
.reply(200, 'test response');
client.edge = 'edge2';
return client.request({method: 'GET', uri: 'https://api.edge1.region.twilio.com'})
.then(() => scope.done());
});
it('should set the region and edge properly when a region is already included', () => {
const scope = nock('https://api.edge.region2.twilio.com')
.get('/')
.reply(200, 'test response');
client.region = 'region2';
return client.request({method: 'GET', uri: 'https://api.edge.region.twilio.com'})
.then(() => scope.done());
});
it('should set the region properly when a region is already included', () => {
const scope = nock('https://api.region2.twilio.com')
.get('/')
.reply(200, 'test response');
client.region = 'region2';
return client.request({method: 'GET', uri: 'https://api.region.twilio.com'})
.then(() => scope.done());
});
it('should set the region properly on a custom domain', () => {
const scope = nock('https://api.region2.domain.com')
.get('/')
.reply(200, 'test response');
client.region = 'region2';
return client.request({method: 'GET', uri: 'https://api.domain.com'})
.then(() => scope.done());
});
it('should set the region properly when a port is included', () => {
const scope = nock('https://api.region.twilio.com:123')
.get('/')
.reply(200, 'test response');
client.region = 'region';
return client.request({method: 'GET', uri: 'https://api.twilio.com:123'})
.then(() => scope.done());
});
});
});

0 comments on commit 4097475

Please sign in to comment.