Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add regional support #563

Merged
merged 16 commits into from
Apr 27, 2020
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 = 'twilio.com';
const defaultRegion = 'us1';

const prefix = hostname.split('.' + domain)[0];
childish-sambino marked this conversation as resolved.
Show resolved Hide resolved
let [product, edge, region] = prefix.split('.');

targetRegion = targetRegion || (targetEdge && defaultRegion);
if (!targetRegion) {
thinkingserious marked this conversation as resolved.
Show resolved Hide resolved
return hostname;
}

region = targetRegion;

if (targetEdge) {
edge = targetEdge;
}

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
63 changes: 63 additions & 0 deletions spec/unit/rest/Twilio.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
'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'})
childish-sambino marked this conversation as resolved.
Show resolved Hide resolved
.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());
});
});
});