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-381: Flexible enums for regions #54

Merged
merged 10 commits into from
Apr 8, 2024
3 changes: 1 addition & 2 deletions examples/simple-examples/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
getRegion,
SinchClient,
SmsRegion,
ConversationService,
Expand Down Expand Up @@ -37,7 +36,7 @@ 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 smsRegion = getRegion(process.env.SMS_REGION) || SmsRegion.UNITED_STATES;
const smsRegion = process.env.SMS_REGION || SmsRegion.UNITED_STATES;
return new SinchClient({ servicePlanId, apiToken, smsRegion });
};

Expand Down
12 changes: 7 additions & 5 deletions packages/conversation/src/rest/v1/conversation-domain-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
buildOAuth2ApiClientOptions,
ConversationRegion,
SinchClientParameters,
SupportedConversationRegion,
UnifiedCredentials,
} from '@sinch/sdk-client';

Expand Down Expand Up @@ -75,9 +76,9 @@ export class ConversationDomainApi implements Api {
*/
public getSinchClient(): ApiClient {
if (!this.client) {
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 region = this.sinchClientParameters.conversationRegion ?? ConversationRegion.UNITED_STATES;
if(!Object.values(SupportedConversationRegion).includes(region as SupportedConversationRegion)) {
console.warn(`The region "${region}" is not known as a supported region for the Conversation API`);
}
const apiClientOptions = buildOAuth2ApiClientOptions(this.sinchClientParameters, 'Conversation');
this.client = new ApiFetchClient(apiClientOptions);
Expand All @@ -87,12 +88,13 @@ export class ConversationDomainApi implements Api {
}

private buildHostname(region: ConversationRegion) {
const formattedRegion = region !== '' ? `${region}.` : '';
switch (this.apiName) {
case 'TemplatesV1Api':
case 'TemplatesV2Api':
return this.sinchClientParameters.conversationTemplatesHostname ?? `https://${region}.template.api.sinch.com`;
return this.sinchClientParameters.conversationTemplatesHostname ?? `https://${formattedRegion}template.api.sinch.com`;
default:
return this.sinchClientParameters.conversationHostname ?? `https://${region}.conversation.api.sinch.com`;
return this.sinchClientParameters.conversationHostname ?? `https://${formattedRegion}conversation.api.sinch.com`;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,15 @@ describe('Conversation API', () => {
expect(conversationApi.client?.apiClientOptions.hostname).toBe('https://eu.conversation.api.sinch.com');
});

// 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 log a warning when using an unsupported region', async () => {
params.conversationRegion = 'bzh';
JPPortier marked this conversation as resolved.
Show resolved Hide resolved
conversationApi = new ConversationDomainApi(params, 'dummy');
console.warn = jest.fn();
conversationApi.getSinchClient();
expect(console.warn).toHaveBeenCalledWith(
'The region "bzh" is not known as a supported region for the Conversation API');
expect(conversationApi.client?.apiClientOptions.hostname).toBe('https://bzh.conversation.api.sinch.com');
});

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

it ('should update the region', () => {
conversationApi = new ConversationDomainApi(params, 'dummy');
conversationApi.setRegion(ConversationRegion.EUROPE);
conversationApi.getSinchClient();
expect(conversationApi.client).toBeDefined();
expect(conversationApi.client?.apiClientOptions.hostname).toBe('https://us.conversation.api.sinch.com');
conversationApi.setRegion(ConversationRegion.UNITED_STATES);
expect(conversationApi.client?.apiClientOptions.hostname).toBe('https://us.conversation.api.sinch.com');
conversationApi.setRegion(ConversationRegion.EUROPE);
expect(conversationApi.client?.apiClientOptions.hostname).toBe('https://eu.conversation.api.sinch.com');
conversationApi.setRegion(ConversationRegion.BRAZIL);
expect(conversationApi.client?.apiClientOptions.hostname).toBe('https://br.conversation.api.sinch.com');
conversationApi.setRegion('bzh');
expect(conversationApi.client?.apiClientOptions.hostname).toBe('https://bzh.conversation.api.sinch.com');
conversationApi.setRegion('');
expect(conversationApi.client?.apiClientOptions.hostname).toBe('https://conversation.api.sinch.com');
});

it ('should update the template v1 region', () => {
Expand Down
28 changes: 24 additions & 4 deletions packages/fax/src/rest/v3/fax-domain-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import {
Api,
ApiClient,
ApiFetchClient,
SinchClientParameters,
UnifiedCredentials,
buildOAuth2ApiClientOptions,
FaxRegion,
SinchClientParameters,
SupportedFaxRegion,
UnifiedCredentials,
} from '@sinch/sdk-client';

export class FaxDomainApi implements Api {
Expand All @@ -27,6 +28,17 @@ export class FaxDomainApi implements Api {
this.client.apiClientOptions.hostname = hostname;
}

/**
* Update the region in the basePath
* @param {FaxRegion} region - The new region to send the requests to
*/
public setRegion(region: FaxRegion) {
this.sinchClientParameters.faxRegion = region;
if (this.client) {
this.client.apiClientOptions.hostname = this.buildHostname(region);
}
}

/**
* Updates the credentials used to authenticate API requests
* @param {UnifiedCredentials} credentials
Expand Down Expand Up @@ -61,10 +73,18 @@ export class FaxDomainApi implements Api {
if (!this.client) {
const apiClientOptions = buildOAuth2ApiClientOptions(this.sinchClientParameters, 'Fax');
this.client = new ApiFetchClient(apiClientOptions);
const region: FaxRegion = this.sinchClientParameters.faxRegion || FaxRegion.DEFAULT;
this.client.apiClientOptions.hostname = this.sinchClientParameters.faxHostname ?? `https://${region}fax.api.sinch.com`;
const region = this.sinchClientParameters.faxRegion ?? FaxRegion.DEFAULT;
if(!Object.values(SupportedFaxRegion).includes(region as SupportedFaxRegion)) {
console.warn(`The region "${region}" is not known as a supported region for the Fax API`);
}
this.client.apiClientOptions.hostname = this.sinchClientParameters.faxHostname ?? this.buildHostname(region);
}
return this.client;
}

private buildHostname(region: FaxRegion) {
const formattedRegion = region !== '' ? `${region}.` : '';
return `https://${formattedRegion}fax.api.sinch.com`;
}

}
33 changes: 33 additions & 0 deletions packages/fax/tests/rest/v3/fax-domain-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ describe('Fax API', () => {
expect(faxApi.client?.apiClientOptions.hostname).toBe('https://apse1.fax.api.sinch.com');
});

it('should log a warning when using an unsupported region', async () => {
params.faxRegion = 'bzh';
faxApi = new FaxDomainApi(params, 'dummy');
console.warn = jest.fn();
faxApi.getSinchClient();
expect(console.warn).toHaveBeenCalledWith(
'The region "bzh" is not known as a supported region for the Fax API');
expect(faxApi.client?.apiClientOptions.hostname).toBe('https://bzh.fax.api.sinch.com');
});

it('should use the hostname parameter', () => {
params.faxHostname = CUSTOM_HOSTNAME;
faxApi = new FaxDomainApi(params, 'dummy');
Expand All @@ -42,4 +52,27 @@ describe('Fax API', () => {
expect(faxApi.client?.apiClientOptions.hostname).toBe(CUSTOM_HOSTNAME);
});

it ('should update the region', () => {
faxApi = new FaxDomainApi(params, 'dummy');
faxApi.getSinchClient();
expect(faxApi.client).toBeDefined();
expect(faxApi.client?.apiClientOptions.hostname).toBe('https://fax.api.sinch.com');
faxApi.setRegion(FaxRegion.DEFAULT);
expect(faxApi.client?.apiClientOptions.hostname).toBe('https://fax.api.sinch.com');
faxApi.setRegion(FaxRegion.UNITED_STATES);
expect(faxApi.client?.apiClientOptions.hostname).toBe('https://use1.fax.api.sinch.com');
faxApi.setRegion(FaxRegion.EUROPE);
expect(faxApi.client?.apiClientOptions.hostname).toBe('https://eu1.fax.api.sinch.com');
faxApi.setRegion(FaxRegion.SOUTH_AMERICA);
expect(faxApi.client?.apiClientOptions.hostname).toBe('https://sae1.fax.api.sinch.com');
faxApi.setRegion(FaxRegion.SOUTHEAST_ASIA_1);
expect(faxApi.client?.apiClientOptions.hostname).toBe('https://apse1.fax.api.sinch.com');
faxApi.setRegion(FaxRegion.SOUTHEAST_ASIA_2);
expect(faxApi.client?.apiClientOptions.hostname).toBe('https://apse2.fax.api.sinch.com');
faxApi.setRegion('bzh');
expect(faxApi.client?.apiClientOptions.hostname).toBe('https://bzh.fax.api.sinch.com');
faxApi.setRegion('');
expect(faxApi.client?.apiClientOptions.hostname).toBe('https://fax.api.sinch.com');
});

});
5 changes: 3 additions & 2 deletions packages/sdk-client/src/api/api-client-options-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ export const buildFlexibleOAuth2OrApiTokenApiClientOptions = (
): 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)) {
if ( params.forceOAuth2ForSmsApi
|| (!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 = {
Expand Down
85 changes: 46 additions & 39 deletions packages/sdk-client/src/domain/domain-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export interface UnifiedCredentials {
keySecret: string;
/** The region for the SMS API. Default region is US */
smsRegion?: SmsRegion;
/** boolean to force the usage of the OAuth2 authentication for the SMS API - to be used when a region other of US and EU supports OAuth2 but the SDK doesn't by default */
forceOAuth2ForSmsApi?: boolean;
/** The region for the Fax API. Default is auto-routing */
faxRegion?: FaxRegion;
/** The region for the Conversation API. Default region is US */
Expand All @@ -28,7 +30,7 @@ export interface ServicePlanIdCredentials {
servicePlanId: string;
/** Your API token. You can find this on your [Dashboard](https://dashboard.sinch.com/sms/api/rest). */
apiToken: string;
/** boolean to force the usage of the service plan Id + API token as credentials for the SMS API*/
/** boolean to force the usage of the service plan Id + API token as credentials for the SMS API */
forceServicePlanIdUsageForSmsApi?: boolean;
/** The region for the SMS API. Default region is US */
smsRegion?: SmsRegion;
Expand Down Expand Up @@ -73,61 +75,66 @@ export const isServicePlanIdCredentials = (credentials: any): credentials is Ser
&& candidate.apiToken !== undefined;
};

export enum SmsRegion {
// /////////////
// SMS regions
export enum SupportedSmsRegion {
UNITED_STATES = 'us',
EUROPE = 'eu',
BRAZIL = 'br',
CANADA = 'ca',
AUSTRALIA = 'au'
}

export function getRegion(value: string | undefined): SmsRegion | undefined {
if (!value) {
return undefined;
}

for (const region of Object.values(SmsRegion)) {
if (region === value.toLowerCase()) {
return region as SmsRegion;
}
}
console.error(`No region exist for the value '${value}'`);
return undefined;
}
export type SmsRegion = SupportedSmsRegion | string;

export const SmsRegion = {
...SupportedSmsRegion,
};

export enum VoiceRegion {
// /////////////
// Voice regions
export enum SupportedVoiceRegion {
DEFAULT = '',
UNITED_STATES = '-use1',
EUROPE = '-euc1',
SOUTH_AMERICA = '-sae1',
SOUTHEAST_ASIA_1 = '-apse1',
SOUTHEAST_ASIA_2 = '-apse2'
UNITED_STATES = 'use1',
EUROPE = 'euc1',
SOUTH_AMERICA = 'sae1',
SOUTHEAST_ASIA_1 = 'apse1',
SOUTHEAST_ASIA_2 = 'apse2'
}

export enum FaxRegion {
export type VoiceRegion = SupportedVoiceRegion | string;

export const VoiceRegion = {
...SupportedVoiceRegion,
};

// ///////////
// Fax regions
export enum SupportedFaxRegion {
DEFAULT = '',
UNITED_STATES = 'use1.',
EUROPE = 'eu1.',
SOUTH_AMERICA = 'sae1.',
SOUTHEAST_ASIA_1 = 'apse1.',
SOUTHEAST_ASIA_2 = 'apse2.'
UNITED_STATES = 'use1',
EUROPE = 'eu1',
SOUTH_AMERICA = 'sae1',
SOUTHEAST_ASIA_1 = 'apse1',
SOUTHEAST_ASIA_2 = 'apse2'
}

export enum ConversationRegion {
export type FaxRegion = SupportedFaxRegion | string;

export const FaxRegion = {
...SupportedFaxRegion,
};

// ////////////////////
// Conversation regions
export enum SupportedConversationRegion {
UNITED_STATES = 'us',
EUROPE = 'eu',
BRAZIL = 'br'
}

export const getVoiceRegion = (value: string | undefined): VoiceRegion | undefined => {
if (!value) {
return undefined;
}
for(const region of Object.values(VoiceRegion)) {
if (region === value.toLowerCase()) {
return region as VoiceRegion;
}
}
console.error(`No region exist for the value '${value}'`);
return undefined;
export type ConversationRegion = SupportedConversationRegion | string;

export const ConversationRegion = {
...SupportedConversationRegion,
};
27 changes: 25 additions & 2 deletions packages/sms/src/rest/v1/sms-domain-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
SmsRegion,
UnifiedCredentials,
ServicePlanIdCredentials,
SupportedSmsRegion,
} from '@sinch/sdk-client';

export class SmsDomainApi implements Api {
Expand All @@ -33,6 +34,18 @@ export class SmsDomainApi implements Api {
}
}

/**
* Update the region in the basePath
* @param {SmsRegion} region - The new region to send the requests to
*/
public setRegion(region: SmsRegion) {
this.sinchClientParameters.smsRegion = region;
if (this.client) {
const useZapStack = !this.client.apiClientOptions.useServicePlanId;
this.client.apiClientOptions.hostname = this.buildHostname(region, useZapStack);
}
}

/**
* Updates the credentials used to authenticate API requests
* @param {UnifiedCredentials | ServicePlanIdCredentials} credentials
Expand Down Expand Up @@ -65,12 +78,22 @@ export class SmsDomainApi implements Api {
*/
public getSinchClient(): ApiClient {
if (!this.client) {
const region = this.sinchClientParameters.smsRegion || SmsRegion.UNITED_STATES;
const region = this.sinchClientParameters.smsRegion ?? SmsRegion.UNITED_STATES;
if(!Object.values(SupportedSmsRegion).includes(region as SupportedSmsRegion)) {
console.warn(`The region "${region}" is not known as a supported region for the SMS API`);
}
const apiClientOptions = buildFlexibleOAuth2OrApiTokenApiClientOptions(this.sinchClientParameters, region, 'SMS');
this.client = new ApiFetchClient(apiClientOptions);
this.client.apiClientOptions.hostname = this.sinchClientParameters.smsHostname ?? `https://${apiClientOptions.useServicePlanId?'':'zt.'}${region}.sms.api.sinch.com`;
const useZapStack = !this.client.apiClientOptions.useServicePlanId;
this.client.apiClientOptions.hostname = this.sinchClientParameters.smsHostname
?? this.buildHostname(region, useZapStack);
}
return this.client;
}

private buildHostname(region: SmsRegion, useZapStack: boolean) {
const formattedRegion = region !== '' ? `${region}.` : '';
return `https://${useZapStack?'zt.':''}${formattedRegion}sms.api.sinch.com`;
}

}
Loading
Loading