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

DEVEXP-384: Support regions for Fax API #53

Merged
merged 6 commits into from
Apr 8, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
6 changes: 3 additions & 3 deletions examples/simple-examples/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {
getRegion,
SinchClient,
Region,
SmsRegion,
ConversationService,
FaxService,
NumbersService,
Expand Down Expand Up @@ -37,8 +37,8 @@ export const initSmsServiceWithProjectId = (): SmsService => {
const initSmsClient = (): Pick<SinchClient, 'sms'> => {
const servicePlanId = process.env.SINCH_SERVICE_PLAN_ID || '';
const apiToken = process.env.SINCH_API_TOKEN || '';
const region = getRegion(process.env.SMS_REGION) || Region.UNITED_STATES;
return new SinchClient({ servicePlanId, apiToken, region });
const smsRegion = getRegion(process.env.SMS_REGION) || SmsRegion.UNITED_STATES;
return new SinchClient({ servicePlanId, apiToken, smsRegion });
};

export const initSmsServiceWithServicePlanId = (): SmsService => {
Expand Down
28 changes: 7 additions & 21 deletions packages/conversation/src/rest/v1/conversation-domain-api.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import {
Api,
ApiClient,
ApiClientOptions,
ApiFetchClient,
buildOAuth2ApiClientOptions,
ConversationRegion,
Oauth2TokenRequest,
Region,
SinchClientParameters,
UnifiedCredentials,
} from '@sinch/sdk-client';
Expand Down Expand Up @@ -36,10 +34,10 @@ export class ConversationDomainApi implements Api {

/**
* Update the region in the basePath
* @param {Region} region - The new region to send the requests to
* @param {ConversationRegion} region - The new region to send the requests to
*/
public setRegion(region: Region) {
this.sinchClientParameters.region = region;
public setRegion(region: ConversationRegion) {
this.sinchClientParameters.conversationRegion = region;
if (this.client) {
this.client.apiClientOptions.hostname = this.buildHostname(region);
}
Expand Down Expand Up @@ -77,30 +75,18 @@ export class ConversationDomainApi implements Api {
*/
public getSinchClient(): ApiClient {
if (!this.client) {
const region = this.sinchClientParameters.region || Region.UNITED_STATES;
const region = this.sinchClientParameters.conversationRegion || ConversationRegion.UNITED_STATES;
if(!Object.values(ConversationRegion).includes((region as unknown) as ConversationRegion)) {
console.warn(`The region '${region}' is not supported for the Conversation API`);
}
const apiClientOptions = this.buildApiClientOptions(this.sinchClientParameters);
const apiClientOptions = buildOAuth2ApiClientOptions(this.sinchClientParameters, 'Conversation');
this.client = new ApiFetchClient(apiClientOptions);
this.client.apiClientOptions.hostname = this.buildHostname(region);
}
return this.client;
}

private buildApiClientOptions(params: SinchClientParameters): ApiClientOptions {
if (!params.projectId || !params.keyId || !params.keySecret) {
throw new Error('Invalid configuration for the Conversation API: '
+ '"projectId", "keyId" and "keySecret" values must be provided');
}
return {
projectId: params.projectId,
requestPlugins: [new Oauth2TokenRequest( params.keyId, params.keySecret)],
useServicePlanId: false,
};
}

private buildHostname(region: Region) {
private buildHostname(region: ConversationRegion) {
switch (this.apiName) {
case 'TemplatesV1Api':
case 'TemplatesV2Api':
Expand Down
2 changes: 1 addition & 1 deletion packages/conversation/src/rest/v1/conversation-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export class ConversationService {

/**
* Update the default hostname for the Templates API
* @param {string} hostname - The new hostname to use for all the APIs.
* @param {string} hostname - The new hostname to use for the Templates APIs.
*/
public setTemplatesHostname(hostname: string) {
this.templatesV1.setHostname(hostname);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ConversationDomainApi } from '../../../src/rest/v1/conversation-domain-api';
import { TemplatesV2Api } from '../../../src';
import { ApiHostname, Region, UnifiedCredentials } from '@sinch/sdk-client';
import { ApiHostname, ConversationRegion, UnifiedCredentials } from '@sinch/sdk-client';

describe('Conversation API', () => {
let conversationApi: ConversationDomainApi;
Expand All @@ -25,23 +25,24 @@ describe('Conversation API', () => {
});

it('should change the URL when specifying a different region', () => {
params.region = Region.EUROPE;
params.conversationRegion = ConversationRegion.EUROPE;
conversationApi = new ConversationDomainApi(params, 'dummy');
conversationApi.getSinchClient();
expect(conversationApi.client?.apiClientOptions.hostname).toBe('https://eu.conversation.api.sinch.com');
});

it('should log a warning when using an unsupported region', async () => {
params.region = Region.CANADA;
conversationApi = new ConversationDomainApi(params, 'dummy');
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
conversationApi.getSinchClient();
// Add a small delay to allow jest to capture the warning
setTimeout(() => {
expect(consoleWarnSpy).toHaveBeenCalledWith('The region \'ca\' is not supported for the Conversation API');
consoleWarnSpy.mockRestore();
}, 20);
});
// TODO: Temporarily disabled. Will be back with DEVEXP-381
// it('should log a warning when using an unsupported region', async () => {
// params.conversationRegion = 'unknown';
// conversationApi = new ConversationDomainApi(params, 'dummy');
// const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
// conversationApi.getSinchClient();
// // Add a small delay to allow jest to capture the warning
// setTimeout(() => {
// expect(consoleWarnSpy).toHaveBeenCalledWith('The region \'ca\' is not supported for the Conversation API');
// consoleWarnSpy.mockRestore();
// }, 20);
// });

it('should use the hostname parameter but not for templates', () => {
params.conversationHostname = CUSTOM_HOSTNAME;
Expand Down Expand Up @@ -83,7 +84,7 @@ describe('Conversation API', () => {

it ('should update the region', () => {
conversationApi = new ConversationDomainApi(params, 'dummy');
conversationApi.setRegion(Region.EUROPE);
conversationApi.setRegion(ConversationRegion.EUROPE);
conversationApi.getSinchClient();
expect(conversationApi.client).toBeDefined();
expect(conversationApi.client?.apiClientOptions.hostname).toBe('https://eu.conversation.api.sinch.com');
Expand All @@ -94,7 +95,7 @@ describe('Conversation API', () => {
conversationApi.getSinchClient();
expect(conversationApi.client).toBeDefined();
expect(conversationApi.client?.apiClientOptions.hostname).toBe('https://us.template.api.sinch.com');
conversationApi.setRegion(Region.EUROPE);
conversationApi.setRegion(ConversationRegion.EUROPE);
expect(conversationApi.client?.apiClientOptions.hostname).toBe('https://eu.template.api.sinch.com');
});

Expand All @@ -103,7 +104,7 @@ describe('Conversation API', () => {
conversationApi.getSinchClient();
expect(conversationApi.client).toBeDefined();
expect(conversationApi.client?.apiClientOptions.hostname).toBe('https://us.template.api.sinch.com');
conversationApi.setRegion(Region.EUROPE);
conversationApi.setRegion(ConversationRegion.EUROPE);
expect(conversationApi.client?.apiClientOptions.hostname).toBe('https://eu.template.api.sinch.com');
});

Expand Down
21 changes: 5 additions & 16 deletions packages/fax/src/rest/v3/fax-domain-api.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import {
Api,
ApiClient,
ApiClientOptions,
ApiFetchClient,
SinchClientParameters,
Oauth2TokenRequest,
UnifiedCredentials,
buildOAuth2ApiClientOptions,
FaxRegion,
} from '@sinch/sdk-client';

export class FaxDomainApi implements Api {
Expand Down Expand Up @@ -59,23 +59,12 @@ export class FaxDomainApi implements Api {
*/
public getSinchClient(): ApiClient {
if (!this.client) {
const apiClientOptions = this.buildApiClientOptions(this.sinchClientParameters);
const apiClientOptions = buildOAuth2ApiClientOptions(this.sinchClientParameters, 'Fax');
this.client = new ApiFetchClient(apiClientOptions);
this.client.apiClientOptions.hostname = this.sinchClientParameters.faxHostname ?? 'https://fax.api.sinch.com';
const region: FaxRegion = this.sinchClientParameters.faxRegion || FaxRegion.DEFAULT;
this.client.apiClientOptions.hostname = this.sinchClientParameters.faxHostname ?? `https://${region}fax.api.sinch.com`;
}
return this.client;
}

private buildApiClientOptions(params: SinchClientParameters): ApiClientOptions {
if (!params.projectId || !params.keyId || !params.keySecret) {
throw new Error('Invalid configuration for the Fax API: '
+ '"projectId", "keyId" and "keySecret" values must be provided');
}
return {
projectId: params.projectId,
requestPlugins: [new Oauth2TokenRequest( params.keyId, params.keySecret)],
useServicePlanId: false,
};
}

}
9 changes: 8 additions & 1 deletion packages/fax/tests/rest/v3/fax-domain-api.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { FaxDomainApi } from '../../../src/rest/v3/fax-domain-api';
import { ApiHostname, UnifiedCredentials } from '@sinch/sdk-client';
import { ApiHostname, FaxRegion, UnifiedCredentials } from '@sinch/sdk-client';

describe('Fax API', () => {
let faxApi: FaxDomainApi;
Expand All @@ -21,6 +21,13 @@ describe('Fax API', () => {
expect(faxApi.client?.apiClientOptions.hostname).toBe('https://fax.api.sinch.com');
});

it('should change the URL when specifying a different region', () => {
params.faxRegion = FaxRegion.SOUTHEAST_ASIA_1;
faxApi = new FaxDomainApi(params, 'dummy');
faxApi.getSinchClient();
expect(faxApi.client?.apiClientOptions.hostname).toBe('https://apse1.fax.api.sinch.com');
});

it('should use the hostname parameter', () => {
params.faxHostname = CUSTOM_HOSTNAME;
faxApi = new FaxDomainApi(params, 'dummy');
Expand Down
18 changes: 4 additions & 14 deletions packages/numbers/src/rest/v1/numbers-domain-api.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import {
Api,
ApiClient, ApiClientOptions,
ApiClient,
ApiFetchClient,
buildOAuth2ApiClientOptions,
SinchClientParameters,
Oauth2TokenRequest, UnifiedCredentials,
UnifiedCredentials,
} from '@sinch/sdk-client';

export class NumbersDomainApi implements Api {
Expand Down Expand Up @@ -57,22 +58,11 @@ export class NumbersDomainApi implements Api {
*/
public getSinchClient(): ApiClient {
if (!this.client) {
const apiClientOptions = this.buildApiClientOptions(this.sinchClientParameters);
const apiClientOptions = buildOAuth2ApiClientOptions(this.sinchClientParameters, 'Numbers');
this.client = new ApiFetchClient(apiClientOptions);
this.client.apiClientOptions.hostname = this.sinchClientParameters.numbersHostname ?? 'https://numbers.api.sinch.com';
}
return this.client;
}

private buildApiClientOptions(params: SinchClientParameters): ApiClientOptions {
if (!params.projectId || !params.keyId || !params.keySecret) {
throw new Error('Invalid configuration for the Numbers API: '
+ '"projectId", "keyId" and "keySecret" values must be provided');
}
return {
projectId: params.projectId,
requestPlugins: [new Oauth2TokenRequest( params.keyId, params.keySecret)],
useServicePlanId: false,
};
}
}
124 changes: 87 additions & 37 deletions packages/sdk-client/src/api/api-client-options-helper.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,87 @@
// import { ApiTokenRequest, Oauth2TokenRequest, SigningRequest, XTimestampRequest } from '../plugins';
// // import { ApiClientOptions } from './api-client-options';
//
// export const buildApiClientOptionsForProjectId = (
// projectId: string,
// keyId: string,
// keySecret: string,
// ) => {
// return {
// projectId,
// requestPlugins: [new Oauth2TokenRequest(keyId, keySecret)],
// useServicePlanId: false,
// };
// };
//
// export const buildApiClientOptionForServicePlanId = (
// servicePlanId: string,
// apiToken: string,
// ) => {
// return {
// projectId: servicePlanId,
// requestPlugins: [new ApiTokenRequest(apiToken)],
// useServicePlanId: true,
// };
// };
//
// export const buildApiClientOptionForApplication = (
// applicationKey: string,
// applicationSecret: string,
// ) => {
// return {
// requestPlugins: [
// new XTimestampRequest(),
// new SigningRequest(applicationKey, applicationSecret),
// ],
// };
// };
import { SmsRegion, SinchClientParameters } from '../domain';
import { ApiClientOptions } from './api-client-options';
import {
ApiTokenRequest,
Oauth2TokenRequest,
SigningRequest,
XTimestampRequest,
} from '../plugins';

export const buildOAuth2ApiClientOptions = (params: SinchClientParameters, apiName: string): ApiClientOptions => {
if (!params.projectId || !params.keyId || !params.keySecret) {
throw new Error(`Invalid configuration for the ${apiName} API: "projectId", "keyId" and "keySecret" values must be provided`);
}
const apiClientOptions: ApiClientOptions = {
projectId: params.projectId,
requestPlugins: [new Oauth2TokenRequest(params.keyId, params.keySecret, params.authHostname)],
useServicePlanId: false,
};
addPlugins(apiClientOptions, params);
return apiClientOptions;
};

export const buildApplicationSignedApiClientOptions = (
params: SinchClientParameters, apiName: string,
): ApiClientOptions => {
if (!params.applicationKey || !params.applicationSecret) {
throw new Error(`Invalid configuration for the ${apiName} API: "applicationKey" and "applicationSecret" values must be provided`);
}
const apiClientOptions: ApiClientOptions = {
requestPlugins: [
new XTimestampRequest(),
new SigningRequest(params.applicationKey, params.applicationSecret),
],
};
addPlugins(apiClientOptions, params);
return apiClientOptions;
};

export const buildFlexibleOAuth2OrApiTokenApiClientOptions = (
params: SinchClientParameters, region: SmsRegion, apiName: string,
): ApiClientOptions => {
let apiClientOptions: ApiClientOptions | undefined;
// Check the region: if US or EU, try to use the OAuth2 authentication with the access key / secret under the project Id
if (!params.forceServicePlanIdUsageForSmsApi
&& (region === SmsRegion.UNITED_STATES || region === SmsRegion.EUROPE)) {
// Let's check the required parameters for OAuth2 authentication
if (params.projectId && params.keyId && params.keySecret) {
apiClientOptions = {
projectId: params.projectId,
requestPlugins: [new Oauth2TokenRequest(params.keyId, params.keySecret, params.authHostname)],
useServicePlanId: false,
};
}
}
if (!apiClientOptions) {
// The API client options couldn't be initialized for with the projectId unified authentication.
// Let's try with the servicePlanId
if (params.servicePlanId && params.apiToken) {
apiClientOptions = {
projectId: params.servicePlanId,
requestPlugins: [new ApiTokenRequest(params.apiToken)],
useServicePlanId: true,
};
}
}
if (!apiClientOptions) {
throw new Error(`Invalid parameters for the ${apiName} API: check your configuration`);
}
addPlugins(apiClientOptions, params);
return apiClientOptions;
};

const addPlugins = (apiClientOptions: ApiClientOptions, params: SinchClientParameters) => {
if (params.requestPlugins && params.requestPlugins.length > 0) {
if (!apiClientOptions.requestPlugins) {
apiClientOptions.requestPlugins = [];
}
apiClientOptions.requestPlugins.push(...params.requestPlugins);
}
if (params.responsePlugins && params.responsePlugins.length > 0) {
if (!apiClientOptions.responsePlugins) {
apiClientOptions.responsePlugins = [];
}
apiClientOptions.responsePlugins.push(...params.responsePlugins);
}
return apiClientOptions;
};
Loading