diff --git a/examples/simple-examples/.env.template b/examples/simple-examples/.env.template index 284ca213..58dd00ff 100644 --- a/examples/simple-examples/.env.template +++ b/examples/simple-examples/.env.template @@ -39,3 +39,8 @@ MESSAGE_ID=message_id to fill with one of the messages sent or injected with the TEMPLATE_ID=template_id to fill with one of the templates created with the Templates API (v1 or v2) WEBHOOK_ID=webhook_id to fill with one of the webhooks created with the Conversation API or the Dashboard WEBHOOK_TARGET=target URL where the events should be sent to +## Fax API +FAX_SERVICE_ID=serviceId to fill with one the fax services created with the Fax API +FAX_ID=id from a sendFax response +FAX_CALLBACK_URL=callback url to override the one defined in the default service or specified service +FAX_EMAIL=email to associate with a phone number to use the fax-to-email functionality diff --git a/examples/simple-examples/.gitignore b/examples/simple-examples/.gitignore index 2ff5437a..2ccc0a30 100644 --- a/examples/simple-examples/.gitignore +++ b/examples/simple-examples/.gitignore @@ -6,3 +6,4 @@ .DS_Store .env +fax-pdf/ diff --git a/examples/simple-examples/README.md b/examples/simple-examples/README.md index 975aa2c9..68ca9ad5 100644 --- a/examples/simple-examples/README.md +++ b/examples/simple-examples/README.md @@ -70,6 +70,11 @@ MESSAGE_ID=message_id to fill with one of the messages sent or injected with the TEMPLATE_ID=template_id to fill with one of the templates created with the Templates API (v1 or v2) WEBHOOK_ID=webhook_id to fill with one of the webhooks created with the Conversation API or the Dashboard WEBHOOK_TARGET=target URL where the events should be sent to +## Fax API +FAX_SERVICE_ID=serviceId to fill with one the fax services created with the Fax API +FAX_ID=id from a sendFax response +FAX_CALLBACK_URL=callback url to override the one defined in the default service or specified service +FAX_EMAIL=email to associate with a phone number to use the fax-to-email functionality ``` **Note**: If you prefer using environment variables, the sample app is also supporting them: they take precedence over the value from the `.env` file. @@ -225,3 +230,25 @@ yarn run numbers:regions:list | | [./src/conversation/webhooks/list.ts](./src/conversation/webhooks/list.ts) | `CONVERSATION_APP_ID` | | | [./src/conversation/webhooks/update.ts](./src/conversation/webhooks/update.ts) | `CONVERSATION_APP_ID` + `WEBHOOK_ID` | +### Fax + +| Service | Sample application name and location | Required parameters | +|----------|----------------------------------------------------------------------------------------|-----------------------------------| +| Services | [./src/fax/services/create.ts](./src/fax/services/create.ts) | `PHONE_NUMBER` | +| | [./src/fax/services/get.ts](./src/fax/services/get.ts) | `FAX_SERVICE_ID` | +| | [./src/fax/services/list.ts](./src/fax/services/list.ts) | | +| | [./src/fax/services/listNumbers.ts](./src/fax/services/listNumbers.ts) | `FAX_SERVICE_ID` | +| | [./src/fax/services/listEmailsForNumber.ts](./src/fax/services/listEmailsForNumber.ts) | `PHONE_NUMBER` + `FAX_SERVICE_ID` | +| | [./src/fax/services/update.ts](./src/fax/services/update.ts) | `FAX_SERVICE_ID` | +| | [./src/fax/services/delete.ts](./src/fax/services/delete.ts) | `FAX_SERVICE_ID` | +| Faxes | [./src/fax/faxes/send.ts](./src/fax/faxes/send.ts) | `PHONE_NUMBER` | +| | [./src/fax/faxes/get.ts](./src/fax/faxes/get.ts) | `FAX_ID` | +| | [./src/fax/faxes/list.ts](./src/fax/faxes/list.ts) | | +| | [./src/fax/faxes/downloadContent.ts](./src/fax/faxes/downloadContent.ts) | `FAX_ID` | +| | [./src/fax/faxes/deleteContent.ts](./src/fax/faxes/deleteContent.ts) | `FAX_ID` | +| Emails | [./src/fax/emails/add.ts](./src/fax/emails/add.ts) | `FAX_EMAIL` + `PHONE_NUMBER` | +| | [./src/fax/emails/list.ts](./src/fax/emails/list.ts) | | +| | [./src/fax/emails/listNumbers.ts](./src/fax/emails/listNumbers.ts) | `FAX_EMAIL` | +| | [./src/fax/emails/update.ts](./src/fax/emails/update.ts) | `FAX_EMAIL` + `PHONE_NUMBER` | +| | [./src/fax/emails/delete.ts](./src/fax/emails/delete.ts) | `FAX_EMAIL` | + diff --git a/examples/simple-examples/package.json b/examples/simple-examples/package.json index a57a2a6a..87ad7405 100644 --- a/examples/simple-examples/package.json +++ b/examples/simple-examples/package.json @@ -56,6 +56,23 @@ "conversation:templatev2:listTranslations": "ts-node src/conversation/templates-v2/list-translations.ts pretty", "conversation:templatev2:update": "ts-node src/conversation/templates-v2/update.ts", "conversation:templatev2:delete": "ts-node src/conversation/templates-v2/delete.ts", + "fax:services:create": "ts-node src/fax/services/create.ts", + "fax:services:get": "ts-node src/fax/services/get.ts", + "fax:services:list": "ts-node src/fax/services/list.ts", + "fax:services:listNumbers": "ts-node src/fax/services/listNumbers.ts", + "fax:services:listEmailsForNumber": "ts-node src/fax/services/listEmailsForNumber.ts", + "fax:services:update": "ts-node src/fax/services/update.ts", + "fax:services:delete": "ts-node src/fax/services/delete.ts", + "fax:faxes:send": "ts-node src/fax/faxes/send.ts", + "fax:faxes:get": "ts-node src/fax/faxes/get.ts", + "fax:faxes:list": "ts-node src/fax/faxes/list.ts", + "fax:faxes:download": "ts-node src/fax/faxes/downloadContent.ts", + "fax:faxes:delete": "ts-node src/fax/faxes/deleteContent.ts", + "fax:emails:add": "ts-node src/fax/emails/add.ts", + "fax:emails:list": "ts-node src/fax/emails/list.ts", + "fax:emails:listNumbers": "ts-node src/fax/emails/listNumbers.ts", + "fax:emails:update": "ts-node src/fax/emails/update.ts", + "fax:emails:delete": "ts-node src/fax/emails/delete.ts", "numbers:regions:list": "ts-node src/numbers/regions/list.ts", "numbers:available:list": "ts-node src/numbers/available/list.ts", "numbers:available:checkAvailability": "ts-node src/numbers/available/checkAvailability.ts", diff --git a/examples/simple-examples/src/config.ts b/examples/simple-examples/src/config.ts index 6925e777..db4052fe 100644 --- a/examples/simple-examples/src/config.ts +++ b/examples/simple-examples/src/config.ts @@ -145,6 +145,22 @@ export const getEventIdFromConfig = () => { return readVariable('EVENT_ID'); }; +export const getFaxServiceIdFromConfig = () => { + return readVariable('FAX_SERVICE_ID'); +}; + +export const getFaxIdFromConfig = () => { + return readVariable('FAX_ID'); +}; + +export const getFaxCallbackUrlFromConfig = () => { + return readVariable('FAX_CALLBACK_URL'); +}; + +export const getFaxEmailFromConfig = () => { + return readVariable('FAX_EMAIL'); +}; + const readVariable = ( name: string): string => { const value = process.env[name]; if (!value) { diff --git a/examples/simple-examples/src/fax/emails/add.ts b/examples/simple-examples/src/fax/emails/add.ts new file mode 100644 index 00000000..0b738e6c --- /dev/null +++ b/examples/simple-examples/src/fax/emails/add.ts @@ -0,0 +1,37 @@ +import { AddEmailToNumbersRequestData } from '@sinch/sdk-core'; +import { + getFaxEmailFromConfig, + getPhoneNumberFromConfig, + getPrintFormat, + initClient, + printFullResponse, +} from '../../config'; + +(async () => { + console.log('*************************'); + console.log('* createEmailForProject *'); + console.log('*************************'); + + const phoneNumber = getPhoneNumberFromConfig(); + const email = getFaxEmailFromConfig(); + + const requestData: AddEmailToNumbersRequestData = { + emailRequestBody: { + email, + phoneNumbers: [ + phoneNumber, + ], + }, + }; + + const sinchClient = initClient(); + const response = await sinchClient.fax.emails.addToNumbers(requestData); + + const printFormat = getPrintFormat(process.argv); + + if (printFormat === 'pretty') { + console.log(`Email successfully added to numbers '${response.phoneNumbers?.join(', ')}'`); + } else { + printFullResponse(response); + } +})(); diff --git a/examples/simple-examples/src/fax/emails/delete.ts b/examples/simple-examples/src/fax/emails/delete.ts new file mode 100644 index 00000000..157b955b --- /dev/null +++ b/examples/simple-examples/src/fax/emails/delete.ts @@ -0,0 +1,19 @@ +import { DeleteEmailRequestData } from '@sinch/sdk-core'; +import { getFaxEmailFromConfig, initClient } from '../../config'; + +(async () => { + console.log('***************'); + console.log('* deleteEmail *'); + console.log('***************'); + + const email = getFaxEmailFromConfig(); + + const requestData: DeleteEmailRequestData = { + email, + }; + + const sinchClient = initClient(); + await sinchClient.fax.emails.delete(requestData); + + console.log(`The email '${requestData.email}' has been successfully removed`); +})(); diff --git a/examples/simple-examples/src/fax/emails/list.ts b/examples/simple-examples/src/fax/emails/list.ts new file mode 100644 index 00000000..6f599f73 --- /dev/null +++ b/examples/simple-examples/src/fax/emails/list.ts @@ -0,0 +1,67 @@ +import { Email, ListEmailsForProjectRequestData, PageResult } from '@sinch/sdk-core'; +import { getPrintFormat, initClient, printFullResponse } from '../../config'; + +const populateEmailsList = ( + emailsPage: PageResult, + fullEmailsList: Email[], + emailsList: string[], +) => { + fullEmailsList.push(...emailsPage.data); + emailsPage.data.map((email) => { + emailsList.push(`Email '${email.email}' - Phone numbers: '${email.phoneNumbers?.join(', ')}'`); + }); +}; + +(async () => { + console.log('***********************'); + console.log('* getEmailsForProject *'); + console.log('***********************'); + + const requestData: ListEmailsForProjectRequestData = { + pageSize: 2, + }; + + const sinchClient = initClient(); + + // ---------------------------------------------- + // Method 1: Fetch the data page by page manually + // ---------------------------------------------- + let response = await sinchClient.fax.emails.list(requestData); + + // Init data structure to hold the response content + const fullEmailsList: Email[] = []; + // Init data structure to hold the response content for pretty print + const emailsList: string[] = []; + + // Loop on all the pages to get all the emails + let reachedEndOfPages = false; + while (!reachedEndOfPages) { + populateEmailsList(response, fullEmailsList, emailsList); + if (response.hasNextPage) { + response = await response.nextPage(); + } else { + reachedEndOfPages = true; + } + } + + const printFormat = getPrintFormat(process.argv); + + if (printFormat === 'pretty') { + console.log(emailsList.length > 0 + ? `List of emails:\n${emailsList.join('\n')}` + : 'Sorry, no emails were found'); + } else { + printFullResponse(fullEmailsList); + } + + // --------------------------------------------------------------------- + // Method 2: Use the iterator and fetch data on more pages automatically + // --------------------------------------------------------------------- + for await (const email of sinchClient.fax.emails.list(requestData)) { + if (printFormat === 'pretty') { + console.log(`Email '${email.email}' - Phone numbers: '${email.phoneNumbers?.join(', ')}'`); + } else { + console.log(email); + } + } +})(); diff --git a/examples/simple-examples/src/fax/emails/listNumbers.ts b/examples/simple-examples/src/fax/emails/listNumbers.ts new file mode 100644 index 00000000..5bd77638 --- /dev/null +++ b/examples/simple-examples/src/fax/emails/listNumbers.ts @@ -0,0 +1,74 @@ +import { + ListNumbersByEmailRequestData, + PageResult, + ServicePhoneNumber, +} from '@sinch/sdk-core'; +import { getFaxEmailFromConfig, getPrintFormat, initClient, printFullResponse } from '../../config'; + +const populateServiceNumbersList = ( + serviceNumbersPage: PageResult, + fullServiceNumbersList: ServicePhoneNumber[], + serviceNumbersList: string[], +) => { + fullServiceNumbersList.push(...serviceNumbersPage.data); + serviceNumbersPage.data.map((number) => { + serviceNumbersList.push(`Phone numbers: '${number.phoneNumber}'`); + }); +}; + +(async () => { + console.log('*********************'); + console.log('* getNumbersByEmail *'); + console.log('*********************'); + + const email = getFaxEmailFromConfig(); + + const requestData: ListNumbersByEmailRequestData = { + email, + pageSize: 2, + }; + + const sinchClient = initClient(); + + // ---------------------------------------------- + // Method 1: Fetch the data page by page manually + // ---------------------------------------------- + let response = await sinchClient.fax.emails.listNumbers(requestData); + + // Init data structure to hold the response content + const fullServiceNumbersList: ServicePhoneNumber[] = []; + // Init data structure to hold the response content for pretty print + const serviceNumbersList: string[] = []; + + // Loop on all the pages to get all the numbers + let reachedEndOfPages = false; + while (!reachedEndOfPages) { + populateServiceNumbersList(response, fullServiceNumbersList, serviceNumbersList); + if (response.hasNextPage) { + response = await response.nextPage(); + } else { + reachedEndOfPages = true; + } + } + + const printFormat = getPrintFormat(process.argv); + + if (printFormat === 'pretty') { + console.log(serviceNumbersList.length > 0 + ? `List of configured numbers for the email '${requestData.email}':\n${serviceNumbersList.join('\n')}` + : 'Sorry, no numbers were found'); + } else { + printFullResponse(fullServiceNumbersList); + } + + // --------------------------------------------------------------------- + // Method 2: Use the iterator and fetch data on more pages automatically + // --------------------------------------------------------------------- + for await (const number of sinchClient.fax.emails.listNumbers(requestData)) { + if (printFormat === 'pretty') { + console.log(`Phone numbers: '${number.phoneNumber}'`); + } else { + console.log(number); + } + } +})(); diff --git a/examples/simple-examples/src/fax/emails/update.ts b/examples/simple-examples/src/fax/emails/update.ts new file mode 100644 index 00000000..2d3c7520 --- /dev/null +++ b/examples/simple-examples/src/fax/emails/update.ts @@ -0,0 +1,38 @@ +import { UpdateEmailRequestData } from '@sinch/sdk-core'; +import { + getFaxEmailFromConfig, + getPhoneNumberFromConfig, + getPrintFormat, + initClient, + printFullResponse, +} from '../../config'; + +(async () => { + console.log('*************************'); + console.log('* createEmailForProject *'); + console.log('*************************'); + + const phoneNumber = getPhoneNumberFromConfig(); + const email = getFaxEmailFromConfig(); + + const requestData: UpdateEmailRequestData = { + email, + updateEmailRequestBody: { + phoneNumbers: [ + phoneNumber, + '+14155552222', + ], + }, + }; + + const sinchClient = initClient(); + const response = await sinchClient.fax.emails.update(requestData); + + const printFormat = getPrintFormat(process.argv); + + if (printFormat === 'pretty') { + console.log(`Email successfully updated with numbers '${response.phoneNumbers?.join(', ')}'`); + } else { + printFullResponse(response); + } +})(); diff --git a/examples/simple-examples/src/fax/faxes/deleteContent.ts b/examples/simple-examples/src/fax/faxes/deleteContent.ts new file mode 100644 index 00000000..875c7668 --- /dev/null +++ b/examples/simple-examples/src/fax/faxes/deleteContent.ts @@ -0,0 +1,19 @@ +import { DeleteFaxContentRequestData } from '@sinch/sdk-core'; +import { getFaxIdFromConfig, initClient } from '../../config'; + +(async () => { + console.log('************************'); + console.log('* deleteFaxContentById *'); + console.log('************************'); + + const faxId = getFaxIdFromConfig(); + + const requestData: DeleteFaxContentRequestData = { + id: faxId, + }; + + const sinchClient = initClient(); + await sinchClient.fax.faxes.deleteContent(requestData); + + console.log(`The content of the fax with the id '${requestData.id}' has been successfully removed from storage`); +})(); diff --git a/examples/simple-examples/src/fax/faxes/downloadContent.ts b/examples/simple-examples/src/fax/faxes/downloadContent.ts new file mode 100644 index 00000000..b54610eb --- /dev/null +++ b/examples/simple-examples/src/fax/faxes/downloadContent.ts @@ -0,0 +1,25 @@ +import { DownloadFaxContentRequestData } from '@sinch/sdk-core'; +import { getFaxIdFromConfig, initClient } from '../../config'; +import * as fs from 'fs'; +import * as path from 'path'; + +(async () => { + console.log('******************'); + console.log('* getFaxFileById *'); + console.log('******************'); + + const faxId = getFaxIdFromConfig(); + + const requestData: DownloadFaxContentRequestData = { + id: faxId, + fileFormat: 'pdf', + }; + + const sinchClient = initClient(); + const response = await sinchClient.fax.faxes.downloadContent(requestData); + + const filePath = path.join('./fax-pdf', response.fileName); + fs.writeFileSync(filePath, response.buffer); + + console.log('File successfully downloaded'); +})(); diff --git a/examples/simple-examples/src/fax/faxes/get.ts b/examples/simple-examples/src/fax/faxes/get.ts new file mode 100644 index 00000000..303b867e --- /dev/null +++ b/examples/simple-examples/src/fax/faxes/get.ts @@ -0,0 +1,28 @@ +import { GetFaxRequestData } from '@sinch/sdk-core'; +import { getFaxIdFromConfig, getPrintFormat, initClient, printFullResponse } from '../../config'; + +(async () => { + console.log('*******************'); + console.log('* getFaxInfoPerId *'); + console.log('*******************'); + + const faxId = getFaxIdFromConfig(); + + const requestData: GetFaxRequestData = { + id: faxId, + }; + + const sinchClient = initClient(); + const response = await sinchClient.fax.faxes.get(requestData); + + const printFormat = getPrintFormat(process.argv); + + if (printFormat === 'pretty') { + console.log(`Fax found: it has been created at '${response.createTime}' and the status is '${response.status}'`); + if (response.status === 'FAILURE') { + console.log(`Error type: ${response.errorType} (${response.errorId}): ${response.errorCode}`); + } + } else { + printFullResponse(response); + } +})(); diff --git a/examples/simple-examples/src/fax/faxes/list.ts b/examples/simple-examples/src/fax/faxes/list.ts new file mode 100644 index 00000000..949cb946 --- /dev/null +++ b/examples/simple-examples/src/fax/faxes/list.ts @@ -0,0 +1,67 @@ +import { Fax, ListFaxesRequestData, PageResult } from '@sinch/sdk-core'; +import { getPrintFormat, initClient, printFullResponse } from '../../config'; + +const populateFaxesList = ( + faxPage: PageResult, + fullFaxesList: Fax[], + faxesList: string[], +) => { + fullFaxesList.push(...faxPage.data); + faxPage.data.map((fax) => { + faxesList.push(`Fax ID: '${fax.id}' - Created at: '${fax.createTime}' - Status: '${fax.status}'`); + }); +}; + +(async () => { + console.log('************'); + console.log('* getFaxes *'); + console.log('************'); + + const requestData: ListFaxesRequestData = { + pageSize: 2, + }; + + const sinchClient = initClient(); + + // ---------------------------------------------- + // Method 1: Fetch the data page by page manually + // ---------------------------------------------- + let response = await sinchClient.fax.faxes.list(requestData); + + // Init data structure to hold the response content + const fullFaxesList: Fax[] = []; + // Init data structure to hold the response content for pretty print + const faxesList: string[] = []; + + // Loop on all the pages to get all the faxes + let reachedEndOfPages = false; + while (!reachedEndOfPages) { + populateFaxesList(response, fullFaxesList, faxesList); + if (response.hasNextPage) { + response = await response.nextPage(); + } else { + reachedEndOfPages = true; + } + } + + const printFormat = getPrintFormat(process.argv); + + if (printFormat === 'pretty') { + console.log(faxesList.length > 0 + ? `List of faxes:\n${faxesList.join('\n')}` + : 'Sorry, no faxes were found'); + } else { + printFullResponse(fullFaxesList); + } + + // --------------------------------------------------------------------- + // Method 2: Use the iterator and fetch data on more pages automatically + // --------------------------------------------------------------------- + for await (const fax of sinchClient.fax.faxes.list(requestData)) { + if (printFormat === 'pretty') { + console.log(`Fax ID: '${fax.id}' - Created at: '${fax.createTime}' - Status: '${fax.status}'`); + } else { + console.log(fax); + } + } +})(); diff --git a/examples/simple-examples/src/fax/faxes/send.ts b/examples/simple-examples/src/fax/faxes/send.ts new file mode 100644 index 00000000..93da7602 --- /dev/null +++ b/examples/simple-examples/src/fax/faxes/send.ts @@ -0,0 +1,39 @@ +import { SendFaxRequestData } from '@sinch/sdk-core'; +import { + getFaxCallbackUrlFromConfig, + getPhoneNumberFromConfig, + getPrintFormat, + initClient, + printFullResponse, +} from '../../config'; + +(async () => { + console.log('***********'); + console.log('* sendFax *'); + console.log('***********'); + + const originPhoneNumber = getPhoneNumberFromConfig(); + const destinationPhoneNumber = getPhoneNumberFromConfig(); + const faxCallbackUrl = getFaxCallbackUrlFromConfig(); + + const requestData: SendFaxRequestData = { + sendFaxRequestBody: { + to: destinationPhoneNumber, + from: originPhoneNumber, + contentUrl: 'https://developers.sinch.com/fax/fax.pdf', + callbackUrl: faxCallbackUrl, + callbackContentType: 'application/json', + }, + }; + + const sinchClient = initClient(); + const response = await sinchClient.fax.faxes.send(requestData); + + const printFormat = getPrintFormat(process.argv); + + if (printFormat === 'pretty') { + console.log(`Fax successfully created at '${response.createTime?.toISOString()}'. Status = '${response.status}'`); + } else { + printFullResponse(response); + } +})(); diff --git a/examples/simple-examples/src/fax/services/create.ts b/examples/simple-examples/src/fax/services/create.ts new file mode 100644 index 00000000..2c88a9b4 --- /dev/null +++ b/examples/simple-examples/src/fax/services/create.ts @@ -0,0 +1,37 @@ +import { CreateServiceRequestData } from '@sinch/sdk-core'; +import { getPhoneNumberFromConfig, getPrintFormat, initClient, printFullResponse } from '../../config'; + +(async () => { + console.log('*****************'); + console.log('* createService *'); + console.log('*****************'); + + const phoneNumber = getPhoneNumberFromConfig(); + + const requestData: CreateServiceRequestData = { + createServiceRequestBody: { + name: 'New service with the Node.js SDK', + incomingWebhookUrl: 'https://yourserver/incomingFax', + webhookContentType: 'multipart/form-data', + defaultForProject: false, + defaultFrom: phoneNumber, + numberOfRetries: 3, + retryDelaySeconds: 60, + imageConversionMethod: 'HALFTONE', + saveOutboundFaxDocuments: true, + saveInboundFaxDocuments: true, + }, + }; + + const sinchClient = initClient(); + const response = await sinchClient.fax.services.create(requestData); + + const printFormat = getPrintFormat(process.argv); + + if (printFormat === 'pretty') { + console.log(`New service created with the id '${response.id}' - Name: ${response.name}`); + } else { + printFullResponse(response); + } + console.log(`You may want to update your .env file with the following value: FAX_SERVICE_ID=${response.id}`); +})(); diff --git a/examples/simple-examples/src/fax/services/delete.ts b/examples/simple-examples/src/fax/services/delete.ts new file mode 100644 index 00000000..40f0c83e --- /dev/null +++ b/examples/simple-examples/src/fax/services/delete.ts @@ -0,0 +1,19 @@ +import { DeleteServiceRequestData } from '@sinch/sdk-core'; +import { getFaxServiceIdFromConfig, initClient } from '../../config'; + +(async () => { + console.log('*****************'); + console.log('* removeService *'); + console.log('*****************'); + + const serviceId = getFaxServiceIdFromConfig(); + + const requestData: DeleteServiceRequestData = { + serviceId, + }; + + const sinchClient = initClient(); + await sinchClient.fax.services.delete(requestData); + + console.log(`The service with the id '${requestData.serviceId}' has been successfully removed`); +})(); diff --git a/examples/simple-examples/src/fax/services/get.ts b/examples/simple-examples/src/fax/services/get.ts new file mode 100644 index 00000000..38c78979 --- /dev/null +++ b/examples/simple-examples/src/fax/services/get.ts @@ -0,0 +1,25 @@ +import { GetServiceRequestData } from '@sinch/sdk-core'; +import { getFaxServiceIdFromConfig, getPrintFormat, initClient, printFullResponse } from '../../config'; + +(async () => { + console.log('**************'); + console.log('* getService *'); + console.log('**************'); + + const serviceId = getFaxServiceIdFromConfig(); + + const requestData: GetServiceRequestData = { + serviceId, + }; + + const sinchClient = initClient(); + const response = await sinchClient.fax.services.get(requestData); + + const printFormat = getPrintFormat(process.argv); + + if (printFormat === 'pretty') { + console.log(`Service found: name = '${response.name}'`); + } else { + printFullResponse(response); + } +})(); diff --git a/examples/simple-examples/src/fax/services/list.ts b/examples/simple-examples/src/fax/services/list.ts new file mode 100644 index 00000000..baff2dc4 --- /dev/null +++ b/examples/simple-examples/src/fax/services/list.ts @@ -0,0 +1,67 @@ +import { ListServicesRequestData, PageResult, ServiceResponse } from '@sinch/sdk-core'; +import { getPrintFormat, initClient, printFullResponse } from '../../config'; + +const populateServicesList = ( + servicesPage: PageResult, + fullServicesList: ServiceResponse[], + servicesList: string[], +) => { + fullServicesList.push(...servicesPage.data); + servicesPage.data.map((service) => { + servicesList.push(`Service ID: '${service.id} - Service name: '${service.name}'`); + }); +}; + +(async () => { + console.log('****************'); + console.log('* listServices *'); + console.log('****************'); + + const requestData: ListServicesRequestData = { + pageSize: 2, + }; + + const sinchClient = initClient(); + + // ---------------------------------------------- + // Method 1: Fetch the data page by page manually + // ---------------------------------------------- + let response = await sinchClient.fax.services.list(requestData); + + // Init data structure to hold the response content + const fullServicesList: ServiceResponse[] = []; + // Init data structure to hold the response content for pretty print + const servicesList: string[] = []; + + // Loop on all the pages to get all the services + let reachedEndOfPages = false; + while (!reachedEndOfPages) { + populateServicesList(response, fullServicesList, servicesList); + if (response.hasNextPage) { + response = await response.nextPage(); + } else { + reachedEndOfPages = true; + } + } + + const printFormat = getPrintFormat(process.argv); + + if (printFormat === 'pretty') { + console.log(servicesList.length > 0 + ? `List of services:\n${servicesList.join('\n')}` + : 'Sorry, no services were found'); + } else { + printFullResponse(fullServicesList); + } + + // --------------------------------------------------------------------- + // Method 2: Use the iterator and fetch data on more pages automatically + // --------------------------------------------------------------------- + for await (const service of sinchClient.fax.services.list(requestData)) { + if (printFormat === 'pretty') { + console.log(`Service ID: '${service.id} - Service name: '${service.name}'`); + } else { + console.log(service); + } + } +})(); diff --git a/examples/simple-examples/src/fax/services/listEmailsForNumber.ts b/examples/simple-examples/src/fax/services/listEmailsForNumber.ts new file mode 100644 index 00000000..cbc02c5e --- /dev/null +++ b/examples/simple-examples/src/fax/services/listEmailsForNumber.ts @@ -0,0 +1,65 @@ +import { ListEmailsForNumberRequestData } from '@sinch/sdk-core'; +import { + getFaxServiceIdFromConfig, + getPhoneNumberFromConfig, + getPrintFormat, + initClient, + printFullResponse, +} from '../../config'; + +(async () => { + console.log('**********************'); + console.log('* getEmailsForNumber *'); + console.log('**********************'); + + const phoneNumber = getPhoneNumberFromConfig(); + const serviceId = getFaxServiceIdFromConfig(); + + const requestData: ListEmailsForNumberRequestData = { + phoneNumber, + serviceId, + pageSize: 2, + }; + + const sinchClient = initClient(); + + // ---------------------------------------------- + // Method 1: Fetch the data page by page manually + // ---------------------------------------------- + let response = await sinchClient.fax.services.listEmailsForNumber(requestData); + + // Init data structure to hold the response content + const emailsList: string[] = []; + + // Loop on all the pages to get all the numbers + let reachedEndOfPages = false; + while (!reachedEndOfPages) { + emailsList.push(...response.data); + if (response.hasNextPage) { + response = await response.nextPage(); + } else { + reachedEndOfPages = true; + } + } + + const printFormat = getPrintFormat(process.argv); + + if (printFormat === 'pretty') { + console.log(emailsList.length > 0 + ? `List of emails:\n${emailsList.join('\n')}` + : 'Sorry, no emails were found'); + } else { + printFullResponse(response); + } + + // --------------------------------------------------------------------- + // Method 2: Use the iterator and fetch data on more pages automatically + // --------------------------------------------------------------------- + for await (const email of sinchClient.fax.services.listEmailsForNumber(requestData)) { + if (printFormat === 'pretty') { + console.log(`Email: '${email}'`); + } else { + console.log(email); + } + } +})(); diff --git a/examples/simple-examples/src/fax/services/listNumbers.ts b/examples/simple-examples/src/fax/services/listNumbers.ts new file mode 100644 index 00000000..08d1c4a3 --- /dev/null +++ b/examples/simple-examples/src/fax/services/listNumbers.ts @@ -0,0 +1,70 @@ +import { ListNumbersForServiceRequestData, PageResult, ServicePhoneNumber } from '@sinch/sdk-core'; +import { getFaxServiceIdFromConfig, getPrintFormat, initClient, printFullResponse } from '../../config'; + +const populateNumbersList = ( + numbersPage: PageResult, + fullNumbersList: ServicePhoneNumber[], + numbersList: string[], +) => { + fullNumbersList.push(...numbersPage.data); + numbersPage.data.map((servicePhoneNumber) => { + numbersList.push(`Phone number: '${servicePhoneNumber.phoneNumber} - Service ID: '${servicePhoneNumber.serviceId}'`); + }); +}; + +(async () => { + console.log('*************************'); + console.log('* listNumbersForService *'); + console.log('*************************'); + + const serviceId = getFaxServiceIdFromConfig(); + + const requestData: ListNumbersForServiceRequestData = { + serviceId, + pageSize: 2, + }; + + const sinchClient = initClient(); + + // ---------------------------------------------- + // Method 1: Fetch the data page by page manually + // ---------------------------------------------- + let response = await sinchClient.fax.services.listNumbers(requestData); + + // Init data structure to hold the response content + const fullNumbersList: ServicePhoneNumber[] = []; + // Init data structure to hold the response content for pretty print + const numbersList: string[] = []; + + // Loop on all the pages to get all the numbers + let reachedEndOfPages = false; + while (!reachedEndOfPages) { + populateNumbersList(response, fullNumbersList, numbersList); + if (response.hasNextPage) { + response = await response.nextPage(); + } else { + reachedEndOfPages = true; + } + } + + const printFormat = getPrintFormat(process.argv); + + if (printFormat === 'pretty') { + console.log(numbersList.length > 0 + ? `List of numbers:\n${numbersList.join('\n')}` + : 'Sorry, no numbers were found'); + } else { + printFullResponse(fullNumbersList); + } + + // --------------------------------------------------------------------- + // Method 2: Use the iterator and fetch data on more pages automatically + // --------------------------------------------------------------------- + for await (const servicePhoneNumber of sinchClient.fax.services.listNumbers(requestData)) { + if (printFormat === 'pretty') { + console.log(`Phone number: '${servicePhoneNumber.phoneNumber} - Service ID: '${servicePhoneNumber.serviceId}'`); + } else { + console.log(servicePhoneNumber); + } + } +})(); diff --git a/examples/simple-examples/src/fax/services/update.ts b/examples/simple-examples/src/fax/services/update.ts new file mode 100644 index 00000000..e1a61106 --- /dev/null +++ b/examples/simple-examples/src/fax/services/update.ts @@ -0,0 +1,45 @@ +import { UpdateServiceRequestData } from '@sinch/sdk-core'; +import { + getFaxCallbackUrlFromConfig, + getFaxServiceIdFromConfig, + getPrintFormat, + initClient, + printFullResponse, +} from '../../config'; + +(async () => { + console.log('*****************'); + console.log('* updateService *'); + console.log('*****************'); + + const serviceId = getFaxServiceIdFromConfig(); + const webhookUrl = getFaxCallbackUrlFromConfig(); + + const requestData: UpdateServiceRequestData = { + serviceId, + updateServiceRequestBody: { + name: 'Updated name with the Node.js SDK', + incomingWebhookUrl: webhookUrl, + webhookContentType: 'application/json', + defaultForProject: true, + imageConversionMethod: 'MONOCHROME', + saveOutboundFaxDocuments: true, + saveInboundFaxDocuments: true, + emailSettings: { + pdfPassword: 'pwd', + useBodyAsCoverPage: true, + }, + }, + }; + + const sinchClient = initClient(); + const response = await sinchClient.fax.services.update(requestData); + + const printFormat = getPrintFormat(process.argv); + + if (printFormat === 'pretty') { + console.log(`The service with the id '${response.id}' has been updated - New name: ${response.name}`); + } else { + printFullResponse(response); + } +})(); diff --git a/examples/webhooks/.gitignore b/examples/webhooks/.gitignore index 2ff5437a..fe1ed8d9 100644 --- a/examples/webhooks/.gitignore +++ b/examples/webhooks/.gitignore @@ -6,3 +6,5 @@ .DS_Store .env + +fax-upload/ diff --git a/examples/webhooks/package.json b/examples/webhooks/package.json index 19159d36..6c0b57f4 100644 --- a/examples/webhooks/package.json +++ b/examples/webhooks/package.json @@ -25,6 +25,7 @@ "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", "@types/express": "^4.17.17", + "@types/multer": "^1.4.11", "@types/node": "^20.8.7", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", diff --git a/examples/webhooks/src/app.module.ts b/examples/webhooks/src/app.module.ts index 5d6a66e4..69fe2d33 100644 --- a/examples/webhooks/src/app.module.ts +++ b/examples/webhooks/src/app.module.ts @@ -1,16 +1,21 @@ import { Module } from '@nestjs/common'; +import { MulterModule } from '@nestjs/platform-express'; import { AppController } from './controller/app.controller'; import { NumbersService } from './services/numbers.service'; import { SmsService } from './services/sms.service'; import { VerificationService } from './services/verification.service'; import { VoiceService } from './services/voice.service'; import { ConversationService } from './services/conversation.service'; +import { FaxService } from './services/fax.service'; @Module({ - imports: [], + imports: [ + MulterModule.register({}), + ], controllers: [AppController], providers: [ ConversationService, + FaxService, NumbersService, SmsService, VerificationService, diff --git a/examples/webhooks/src/controller/app.controller.ts b/examples/webhooks/src/controller/app.controller.ts index 585b3ae5..15731471 100644 --- a/examples/webhooks/src/controller/app.controller.ts +++ b/examples/webhooks/src/controller/app.controller.ts @@ -1,7 +1,9 @@ -import { Body, Controller, Post, Req, Res } from '@nestjs/common'; +import { Body, Controller, Post, Req, Res, UploadedFile, UseInterceptors } from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; import { Request, Response } from 'express'; import { ConversationCallbackWebhooks, + FaxCallbackWebhooks, NumbersCallbackWebhooks, SmsCallbackWebhooks, VerificationCallbackWebhooks, @@ -12,6 +14,7 @@ import { SmsService } from '../services/sms.service'; import { VerificationService } from '../services/verification.service'; import { VoiceService } from '../services/voice.service'; import { ConversationService } from '../services/conversation.service'; +import { FaxService } from '../services/fax.service'; require('dotenv').config(); // Const for Conversation API @@ -29,6 +32,7 @@ export class AppController { constructor( private readonly conversationService: ConversationService, + private readonly faxService: FaxService, private readonly numbersService: NumbersService, private readonly smsService: SmsService, private readonly verificationService: VerificationService, @@ -56,6 +60,24 @@ export class AppController { } } + @Post('/fax') + @UseInterceptors(FileInterceptor('file')) + public fax(@UploadedFile() file: Express.Multer.File, @Req() request: Request, @Res() res: Response) { + // Initialize the class that will be used to validate the request and parse it + const faxCallbackWebhook = new FaxCallbackWebhooks(); + try { + // There is no request validation for the SMS API, so we can parse it and revive its content directly + const event = faxCallbackWebhook.parseEvent(request.body); + // Once the request has been revived, delegate the event management to the SMS service + const contentType = request.headers['content-type']; + this.faxService.handleEvent(event, contentType, file); + res.status(200).send(); + } catch (error) { + console.error(error); + res.status(500).send(); + } + } + @Post('/numbers') public numbers(@Req() request: Request, @Res() res: Response) { // Initialize the class that will be used to validate the request and parse it diff --git a/examples/webhooks/src/main.ts b/examples/webhooks/src/main.ts index 9ae939d0..28cbfa78 100644 --- a/examples/webhooks/src/main.ts +++ b/examples/webhooks/src/main.ts @@ -11,8 +11,8 @@ async function bootstrap() { req.rawBody = buf.toString(encoding || 'utf8'); } }; - app.use(bodyParser.urlencoded({verify: rawBodyBuffer, extended: true })); - app.use(bodyParser.json({ verify: rawBodyBuffer })); + app.use(bodyParser.urlencoded({verify: rawBodyBuffer, extended: true, limit: '10mb' })); + app.use(bodyParser.json({ verify: rawBodyBuffer, limit: '10mb' })); await app.listen(3000); } diff --git a/examples/webhooks/src/services/fax.service.ts b/examples/webhooks/src/services/fax.service.ts new file mode 100644 index 00000000..cedff508 --- /dev/null +++ b/examples/webhooks/src/services/fax.service.ts @@ -0,0 +1,44 @@ +import { Injectable } from '@nestjs/common'; +import { + FaxBase64File, + FaxCompletedEventJson, + FaxWebhookEventParsed, + IncomingFaxEventJson, +} from '@sinch/sdk-core'; +import * as path from 'path'; +import * as fs from 'fs'; + +@Injectable() +export class FaxService { + + handleEvent(event: FaxWebhookEventParsed, contentType?: string, file?: Express.Multer.File): void { + if (contentType === 'application/json') { + console.log(`** application/json\n${event.event}: ${event.fax!.id} - ${event.eventTime}`); + if (event.event === 'INCOMING_FAX') { + const incomingFaxEvent = event as IncomingFaxEventJson; + this.saveBase64File(incomingFaxEvent, event.fax!.id!); + } + if (event.event === 'FAX_COMPLETED') { + const faxCompletedEvent = event as FaxCompletedEventJson; + for (const fileBase64 of faxCompletedEvent.files!) { + this.saveBase64File(fileBase64, event.fax!.id!); + } + } + } else if (contentType?.includes('multipart/form-data')) { + console.log(`** multipart/form-data\n${event.event}: ${event.fax!.id} - ${event.eventTime}`); + console.log('Saving file...'); + const filePath = path.join('./fax-upload', file!.originalname); + fs.writeFileSync(filePath, file!.buffer); + console.log('File saved! ' + filePath); + } + } + + private saveBase64File(fileBase64: FaxBase64File, faxId: string) { + console.log('Saving file...'); + const filePath = path.join('./fax-upload', faxId + '.' + fileBase64.fileType!.toLowerCase()); + const buffer = Buffer.from(fileBase64.file!, 'base64'); + fs.writeFileSync(filePath, buffer); + console.log('File saved! ' + filePath); + } + +} diff --git a/examples/webhooks/tsconfig.json b/examples/webhooks/tsconfig.json index 95f5641c..e43d996c 100644 --- a/examples/webhooks/tsconfig.json +++ b/examples/webhooks/tsconfig.json @@ -16,6 +16,7 @@ "noImplicitAny": false, "strictBindCallApply": false, "forceConsistentCasingInFileNames": false, - "noFallthroughCasesInSwitch": false + "noFallthroughCasesInSwitch": false, + "resolveJsonModule": true, } } diff --git a/packages/fax/README.md b/packages/fax/README.md index a903bae2..5e200b18 100644 --- a/packages/fax/README.md +++ b/packages/fax/README.md @@ -37,8 +37,8 @@ The `Fax` API uses the Sinch unified authentication with OAuth2. You will need t If you are using this SDK as part of the Sinch SDK (`@sinch/sdk-core`) you can access it as the `fax` property of the client that you would have instantiated. ```typescript -import { - // TODO +import { + SendFaxRequestData, SinchClient, UnifiedCredentials, } from '@sinch/sdk-core'; @@ -51,12 +51,16 @@ const credentials: UnifiedCredentials = { const sinch = new SinchClient(credentials); -const requestData: TODO = { - TODO, +const requestData: SendFaxRequestData = { + sendFaxRequestBody: { + to: '+12015555555', + contentUrl: 'https://developers.sinch.com/fax/fax.pdf', + callbackUrl: 'https://yourserver/incomingFax', + }, }; // Access the 'fax' domain registered on the Sinch Client -// TODO +const response = await sinchClient.fax.faxes.send(requestData); ``` ### Standalone @@ -67,8 +71,8 @@ The SDK can be used standalone if you need to use only the Fax APIs. import { UnifiedCredentials, } from '@sinch/sdk-client'; -import { - // TODO +import { + SendFaxRequestData, } from '@sinch/fax'; const credentials: UnifiedCredentials = { @@ -78,14 +82,18 @@ const credentials: UnifiedCredentials = { }; // Declare the 'fax' controller in a standalone way -const fax = new Fax(credentials); - -const requestData: TODO = { - TODO, +const faxService = new FaxService(credentials); + +const requestData: SendFaxRequestData = { + sendFaxRequestBody: { + to: '+12015555555', + contentUrl: 'https://developers.sinch.com/fax/fax.pdf', + callbackUrl: 'https://yourserver/incomingFax', + }, }; -// Use the standalone declaration of the 'fax' controller -// TODO +// Use the standalone declaration of the 'fax' service +const response = await faxService.faxes.send(requestData); ``` ## Promises @@ -94,10 +102,18 @@ All the methods that interact with the Sinch APIs use Promises. You can use `awa ```typescript // Method 1: Wait for the Promise to complete (you need to be in an 'async' method) -// TODO +let sendFaxResult: Fax; +try { + sendFaxResult = await sinch.fax.faxes.send(requestData); + console.log(`Fax successfully created at '${sendFaxResult.createTime}'. Status = '${sendFaxResult.status}`); +} catch (error: any) { + console.error(`ERROR ${error.statusCode}: Impossible to crete the fax sent to ${requestdata.to}.`); +} // Method 2: Resolve the promise -// TODO +sinch.fax.faxes.send(requestData) + .then(response => console.log(`Fax successfully created at '${sendFaxResult.createTime}'. Status = '${sendFaxResult.status}`)) + .catch(error => console.error(`ERROR ${error.statusCode}: Impossible to crete the fax sent to ${requestdata.to}.`)); ``` ## Contact diff --git a/packages/fax/src/index.ts b/packages/fax/src/index.ts new file mode 100644 index 00000000..20936a85 --- /dev/null +++ b/packages/fax/src/index.ts @@ -0,0 +1,3 @@ +export * from './models'; +export * from './rest'; +export * from '@sinch/sdk-client'; diff --git a/packages/fax/src/models/index.ts b/packages/fax/src/models/index.ts new file mode 100644 index 00000000..daae7fa6 --- /dev/null +++ b/packages/fax/src/models/index.ts @@ -0,0 +1 @@ +export * from './v3'; diff --git a/packages/fax/src/models/v3/bar-code/bar-code.ts b/packages/fax/src/models/v3/bar-code/bar-code.ts new file mode 100644 index 00000000..97bf148e --- /dev/null +++ b/packages/fax/src/models/v3/bar-code/bar-code.ts @@ -0,0 +1,13 @@ + +/** + * Sinch will scan all pages of all incoming faxes for Code-128 and DataMatrix bar codes and include this information in webhook requests and via the API. + */ +export interface BarCode { + + /** The type of barcode found. */ + type?: 'CODE_128' | 'DATA_MATRIX'; + /** The page number on which the barcode was found. */ + page?: number; + /** The information of the barcode. */ + value?: string; +} diff --git a/packages/fax/src/models/v3/bar-code/index.ts b/packages/fax/src/models/v3/bar-code/index.ts new file mode 100644 index 00000000..109fc47f --- /dev/null +++ b/packages/fax/src/models/v3/bar-code/index.ts @@ -0,0 +1 @@ +export type { BarCode } from './bar-code'; diff --git a/packages/fax/src/models/v3/email/email.ts b/packages/fax/src/models/v3/email/email.ts new file mode 100644 index 00000000..8d33c415 --- /dev/null +++ b/packages/fax/src/models/v3/email/email.ts @@ -0,0 +1,13 @@ +interface EmailBase { + + email?: string; + /** Numbers you want to associate with this email. */ + phoneNumbers?: string[]; +} + +export interface EmailRequest extends EmailBase {} + +export interface Email extends EmailBase { + /** The `Id` of the project associated with the call. */ + projectId?: string; +} diff --git a/packages/fax/src/models/v3/email/index.ts b/packages/fax/src/models/v3/email/index.ts new file mode 100644 index 00000000..6b6a3f17 --- /dev/null +++ b/packages/fax/src/models/v3/email/index.ts @@ -0,0 +1 @@ +export type { Email, EmailRequest } from './email'; diff --git a/packages/fax/src/models/v3/enums.ts b/packages/fax/src/models/v3/enums.ts new file mode 100644 index 00000000..7ce8b33a --- /dev/null +++ b/packages/fax/src/models/v3/enums.ts @@ -0,0 +1,36 @@ +// export type { TypeEnum as BarCodeTypeEnum } from './bar-code/bar-code'; +// export type { TypeEnum as CallErrorTypeEnum, ErrorCodeEnum as CallErrorErrorCodeEnum } from './call-error/call-error'; +// export type { TypeEnum as DocumentConversionErrorTypeEnum, ErrorCodeEnum as DocumentConversionErrorErrorCodeEnum } from './document-conversion-error/document-conversion-error'; +// export type { CallbackContentTypeEnum as FaxCallbackContentTypeEnum, ImageConversionMethodEnum as FaxImageConversionMethodEnum } from './fax/fax'; +// export type { FileTypeEnum as FaxBase64FileFileTypeEnum } from './fax-base64-file/fax-base64-file'; +// export type { TypeEnum as FaxErrorTypeEnum } from './fax-error/fax-error'; +// export type { CallbackContentTypeEnum as SendFaxRequest1CallbackContentTypeEnum, ImageConversionMethodEnum as SendFaxRequest1ImageConversionMethodEnum } from './send-fax-request1/send-fax-request1'; + +export type ImageConversionMethod = 'HALFTONE' | 'MONOCHROME'; + +export type WebhookContentType = 'multipart/form-data' | 'application/json'; + +/** + * The direction of the fax. + */ +export type FaxDirection = 'OUTBOUND' | 'INBOUND'; + +/** + * The status of the fax + */ +export type FaxStatus = 'QUEUED' | 'IN_PROGRESS' | 'COMPLETED' | 'FAILURE'; + +/** + * Error type + */ +export type ErrorType = + 'DOCUMENT_CONVERSION_ERROR' + | 'CALL_ERROR' + | 'FAX_ERROR' + | 'FATAL_ERROR' + | 'GENERAL_ERROR' + | 'LINE_ERROR'; + +export type FaxBase64FileType = 'DOC' | 'DOCX' | 'PDF' | 'TIF' | 'JPG' | 'ODT' | 'TXT' | 'HTML' | 'PNG'; + +export type FaxWebhookEvent = 'INCOMING_FAX' | 'FAX_COMPLETED'; diff --git a/packages/fax/src/models/v3/fax-base64-file/fax-base64-file.ts b/packages/fax/src/models/v3/fax-base64-file/fax-base64-file.ts new file mode 100644 index 00000000..1a7d79ab --- /dev/null +++ b/packages/fax/src/models/v3/fax-base64-file/fax-base64-file.ts @@ -0,0 +1,9 @@ +import { FaxBase64FileType } from '../enums'; + +export interface FaxBase64File { + + /** When application/json Base64 encoded file content, this is only present when using application/json for request/response. */ + file?: string; + /** When request/response is application json and file is part of payload. This is the file type of the file. */ + fileType?: FaxBase64FileType; +} diff --git a/packages/fax/src/models/v3/fax-base64-file/index.ts b/packages/fax/src/models/v3/fax-base64-file/index.ts new file mode 100644 index 00000000..897efcac --- /dev/null +++ b/packages/fax/src/models/v3/fax-base64-file/index.ts @@ -0,0 +1 @@ +export type { FaxBase64File } from './fax-base64-file'; diff --git a/packages/fax/src/models/v3/fax-content-url/fax-content-url.ts b/packages/fax/src/models/v3/fax-content-url/fax-content-url.ts new file mode 100644 index 00000000..6e6c5dba --- /dev/null +++ b/packages/fax/src/models/v3/fax-content-url/fax-content-url.ts @@ -0,0 +1,5 @@ + +/** + * Give us any URL on the Internet (including ones with basic authentication) At least one file or contentUrl parameter is required. Please note: If you are passing fax a secure URL (starting with https://), make sure that your SSL certificate (including your intermediate cert, if you have one) is installed properly, valid, and up-to-date. If the file parameter is specified as well, content from URLs will be rendered before content from files. You can add multiple URLs by adding them as an array them with a comma when posting as multipart/form-data For example: \"https://developers.sinch.com/fax/fax.pdf, https://developers.sinch.com/\" or if posting JSON `\"contentUrl\": [\"https://developers.sinch.com/fax/fax.pdf\", \"https://developers.sinch.com/\"]` + */ +export type FaxContentUrl = string | string[]; diff --git a/packages/fax/src/models/v3/fax-content-url/index.ts b/packages/fax/src/models/v3/fax-content-url/index.ts new file mode 100644 index 00000000..e81b1973 --- /dev/null +++ b/packages/fax/src/models/v3/fax-content-url/index.ts @@ -0,0 +1 @@ +export type { FaxContentUrl } from './fax-content-url'; diff --git a/packages/fax/src/models/v3/fax-money/fax-money.ts b/packages/fax/src/models/v3/fax-money/fax-money.ts new file mode 100644 index 00000000..412d5225 --- /dev/null +++ b/packages/fax/src/models/v3/fax-money/fax-money.ts @@ -0,0 +1,11 @@ + +/** + * This is where we need description to be overridden by `$ref:` description + */ +export interface FaxMoney { + + /** The 3-letter currency code defined in ISO 4217. */ + currencyCode?: string; + /** The amount with 4 decimals and decimal delimiter `.`. */ + amount?: string; +} diff --git a/packages/fax/src/models/v3/fax-money/index.ts b/packages/fax/src/models/v3/fax-money/index.ts new file mode 100644 index 00000000..48e147d9 --- /dev/null +++ b/packages/fax/src/models/v3/fax-money/index.ts @@ -0,0 +1 @@ +export type { FaxMoney } from './fax-money'; diff --git a/packages/fax/src/models/v3/fax-request/fax-request.ts b/packages/fax/src/models/v3/fax-request/fax-request.ts new file mode 100644 index 00000000..9330d560 --- /dev/null +++ b/packages/fax/src/models/v3/fax-request/fax-request.ts @@ -0,0 +1,44 @@ +import { ImageConversionMethod, WebhookContentType } from '../enums'; +import { FaxContentUrl } from '../fax-content-url'; +import { FaxBase64File } from '../fax-base64-file'; + +export type FaxRequest = FaxRequestJson | FaxRequestFormData; + +interface FaxRequestBase { + /** A phone number in [E.164](https://community.sinch.com/t5/Glossary/E-164/ta-p/7537) format, including the leading \\\'+\\\'. */ + to: string; + /** A phone number in [E.164](https://community.sinch.com/t5/Glossary/E-164/ta-p/7537) format, including the leading \\\'+\\\'. */ + from?: string; + /** */ + contentUrl?: FaxContentUrl; + /** Text that will be displayed at the top of each page of the fax. 50 characters maximum. Default header text is \\\"-\\\". Note that the header is not applied until the fax is transmitted, so it will not appear on fax PDFs or thumbnails. */ + headerText?: string; + /** If true, page numbers will be displayed in the header. Default is true. */ + headerPageNumbers?: boolean; + /** A [TZ database name](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) string specifying the timezone for the header timestamp. */ + headerTimeZone?: string; + /** The number of seconds to wait between retries if the fax is not yet completed. */ + retryDelaySeconds?: number; + /** You can use this to attach labels to your call that you can use in your applications. It is a key value store. */ + labels?: { [key: string]: string; }; + /** The URL to which a callback will be sent when the fax is completed. The callback will be sent as a POST request with a JSON body. The callback will be sent to the URL specified in the `callbackUrl` parameter, if provided, otherwise it will be sent to the URL specified in the `callbackUrl` field of the Fax Service object. */ + callbackUrl?: string; + /** The content type of the callback. */ + callbackContentType?: WebhookContentType; + /** Determines how documents are converted to black and white. Defaults to value selected on Fax Service object. */ + imageConversionMethod?: ImageConversionMethod; + /** ID of the fax service used. */ + serviceId?: string; + /** | The number of times the fax will be retired before cancel. Default value is set in your fax service. | The maximum number of retries is 5. */ + maxRetries?: number; +} + +export type FaxRequestJson = FaxRequestBase & { + /** An array of base64 encoded files */ + files: FaxBase64File[]; +} + +export interface FaxRequestFormData extends FaxRequestBase { + /** The file(s) you want to send as a fax as body attachment. */ + file?: any; +} diff --git a/packages/fax/src/models/v3/fax-request/index.ts b/packages/fax/src/models/v3/fax-request/index.ts new file mode 100644 index 00000000..ef1da073 --- /dev/null +++ b/packages/fax/src/models/v3/fax-request/index.ts @@ -0,0 +1 @@ +export type { FaxRequest, FaxRequestJson, FaxRequestFormData } from './fax-request'; diff --git a/packages/fax/src/models/v3/fax/fax.ts b/packages/fax/src/models/v3/fax/fax.ts new file mode 100644 index 00000000..89b78d6e --- /dev/null +++ b/packages/fax/src/models/v3/fax/fax.ts @@ -0,0 +1,62 @@ +import { BarCode } from '../bar-code'; +import { FaxContentUrl } from '../fax-content-url'; +import { FaxMoney } from '../fax-money'; +import { ErrorType, FaxDirection, FaxStatus, ImageConversionMethod, WebhookContentType } from '../enums'; + +export interface Fax { + + /** The id of a fax */ + id?: string; + /** @see FaxDirection */ + direction?: FaxDirection; + /** A phone number in [E.164](https://community.sinch.com/t5/Glossary/E-164/ta-p/7537) format, including the leading \'+\'. */ + from?: string; + /** A phone number in [E.164](https://community.sinch.com/t5/Glossary/E-164/ta-p/7537) format, including the leading \'+\'. */ + to?: string; + /** @see FaxContentUrl */ + contentUrl?: FaxContentUrl; + /** The number of pages in the fax. */ + numberOfPages?: number; + /** @see FaxStatus */ + status?: FaxStatus; + /** The total price for this fax. This field is populated after the final fax price is calculated. */ + price?: FaxMoney; + /** The bar codes found in the fax. This field is populated when sinch detects bar codes on incoming faxes. */ + barCodes?: BarCode[]; + /** A timestamp representing the time when the initial API call was made. */ + createTime?: Date; + /** If the job is complete, this is a timestamp representing the time the job was completed. */ + completedTime?: Date; + /** Text that will be displayed at the top of each page of the fax. 50 characters maximum. Default header text is \"-\". Note that the header is not applied until the fax is transmitted, so it will not appear on fax PDFs or thumbnails. */ + headerText?: string; + /** If true, page numbers will be displayed in the header. Default is true. */ + headerPageNumbers?: boolean; + /** A [TZ database name](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) string specifying the timezone for the header timestamp. */ + headerTimeZone?: string; + /** The number of seconds to wait between retries if the fax is not yet completed. */ + retryDelaySeconds?: number; + /** You can use this to attach labels to your call that you can use in your applications. It is a key value store. */ + labels?: { [key: string]: string; }; + /** The URL to which a callback will be sent when the fax is completed. The callback will be sent as a POST request with a JSON body. The callback will be sent to the URL specified in the `callbackUrl` parameter, if provided, otherwise it will be sent to the URL specified in the `callbackUrl` field of the Fax Service object. */ + callbackUrl?: string; + /** The content type of the callback. */ + callbackContentType?: WebhookContentType; + /** Determines how documents are converted to black and white. Defaults to value selected on Fax Service object. */ + imageConversionMethod?: ImageConversionMethod; + /** @see ErrorType */ + errorType?: ErrorType; + /** One of the error numbers listed in the [Fax Error Messages section](#FaxErrors). */ + errorId?: number; + /** One of the error codes listed in the [Fax Error Messages section](#FaxErrors). */ + errorCode?: string; + /** The `Id` of the project associated with the call. */ + projectId?: string; + /** ID of the fax service used. */ + serviceId?: string; + /** | The number of times the fax will be retired before cancel. Default value is set in your fax service. | The maximum number of retries is 5. */ + maxRetries?: number; + /** The number of times the fax has been retried. */ + retryCount?: number; + /** Only shown on the fax result. This indicates if the content of the fax is stored with Sinch. (true or false) */ + hasFile?: string; +} diff --git a/packages/fax/src/models/v3/fax/index.ts b/packages/fax/src/models/v3/fax/index.ts new file mode 100644 index 00000000..531deb48 --- /dev/null +++ b/packages/fax/src/models/v3/fax/index.ts @@ -0,0 +1 @@ +export type { Fax } from './fax'; diff --git a/packages/fax/src/models/v3/index.ts b/packages/fax/src/models/v3/index.ts new file mode 100644 index 00000000..59421719 --- /dev/null +++ b/packages/fax/src/models/v3/index.ts @@ -0,0 +1,30 @@ +// export * from './bad-request-detail'; +export * from './bar-code'; +// export * from './call-error'; +// export * from './document-conversion-error'; +export * from './email'; +// export * from './error'; +// export * from './error-detail'; +// export * from './error-info'; +// export * from './error-type'; +export * from './fax'; +export * from './fax-base64-file'; +export * from './fax-content-url'; +// export * from './fax-error'; +// export * from './fax-errors'; +// export * from './fax-status'; +// export * from './field-violation'; +// export * from './generic-event'; +export * from './fax-money'; +export * from './fax-request'; +export * from './mod-events'; +// export * from './pagination'; +// export * from './quota-failure'; +// export * from './quota-failure-detail'; +// export * from './request-info-detail'; +// export * from './send-fax-request1'; +export * from './service'; +export * from './service-email-settings'; +export * from './service-phone-number'; +export * from './update-email-request'; +export * from './enums'; diff --git a/packages/fax/src/models/v3/mod-events/base-fax-event/base-fax-event.ts b/packages/fax/src/models/v3/mod-events/base-fax-event/base-fax-event.ts new file mode 100644 index 00000000..52901028 --- /dev/null +++ b/packages/fax/src/models/v3/mod-events/base-fax-event/base-fax-event.ts @@ -0,0 +1,18 @@ +import { Fax } from '../../fax'; +import { FaxWebhookEvent } from '../../enums'; + +export interface BaseFaxEvent { + /** The different events that can trigger a webhook */ + event?: FaxWebhookEvent; + /** Time of the event. */ + eventTime?: Date; + /** @see Fax */ + fax?: Fax; +} + +export interface FaxEventJson extends BaseFaxEvent {} + +export interface FaxEventFormData extends BaseFaxEvent { + /** The fax content as a PDF attachment to the body. */ + file?: string; +} diff --git a/packages/fax/src/models/v3/mod-events/base-fax-event/index.ts b/packages/fax/src/models/v3/mod-events/base-fax-event/index.ts new file mode 100644 index 00000000..8c7c93c7 --- /dev/null +++ b/packages/fax/src/models/v3/mod-events/base-fax-event/index.ts @@ -0,0 +1 @@ +export type { FaxEventJson, FaxEventFormData } from './base-fax-event'; diff --git a/packages/fax/src/models/v3/mod-events/fax-completed-event/fax-completed-event.ts b/packages/fax/src/models/v3/mod-events/fax-completed-event/fax-completed-event.ts new file mode 100644 index 00000000..3952ebc4 --- /dev/null +++ b/packages/fax/src/models/v3/mod-events/fax-completed-event/fax-completed-event.ts @@ -0,0 +1,16 @@ +import { FaxBase64File } from '../../fax-base64-file'; +import { FaxEventFormData, FaxEventJson } from '../base-fax-event'; + +export type FaxCompletedEvent = FaxCompletedEventJson | FaxCompletedEventFormData; + +export interface FaxCompletedEventJson extends FaxEventJson { + /** Always FAX_COMPLETED for this event. */ + event?: 'FAX_COMPLETED'; + /** */ + files?: FaxBase64File[]; +} + +export interface FaxCompletedEventFormData extends FaxEventFormData { + /** Always FAX_COMPLETED for this event. */ + event?: 'FAX_COMPLETED'; +} diff --git a/packages/fax/src/models/v3/mod-events/fax-completed-event/index.ts b/packages/fax/src/models/v3/mod-events/fax-completed-event/index.ts new file mode 100644 index 00000000..f6e30334 --- /dev/null +++ b/packages/fax/src/models/v3/mod-events/fax-completed-event/index.ts @@ -0,0 +1 @@ +export type { FaxCompletedEvent, FaxCompletedEventJson, FaxCompletedEventFormData } from './fax-completed-event'; diff --git a/packages/fax/src/models/v3/mod-events/incoming-fax-event/incoming-fax-event.ts b/packages/fax/src/models/v3/mod-events/incoming-fax-event/incoming-fax-event.ts new file mode 100644 index 00000000..b3550db6 --- /dev/null +++ b/packages/fax/src/models/v3/mod-events/incoming-fax-event/incoming-fax-event.ts @@ -0,0 +1,18 @@ +import { FaxEventFormData, FaxEventJson } from '../base-fax-event'; +import { FaxBase64FileType } from '../../enums'; + +export type IncomingFaxEvent = IncomingFaxEventJson | IncomingFaxEventFormData; + +export interface IncomingFaxEventJson extends FaxEventJson { + /** Always INCOMING_FAX for this event. */ + event?: 'INCOMING_FAX'; + /** The base64 encoded file. */ + file?: string; + /** The file type of the attached file. */ + fileType?: FaxBase64FileType; +} + +export interface IncomingFaxEventFormData extends FaxEventFormData { + /** Always INCOMING_FAX for this event. */ + event?: 'INCOMING_FAX'; +} diff --git a/packages/fax/src/models/v3/mod-events/incoming-fax-event/index.ts b/packages/fax/src/models/v3/mod-events/incoming-fax-event/index.ts new file mode 100644 index 00000000..46005f61 --- /dev/null +++ b/packages/fax/src/models/v3/mod-events/incoming-fax-event/index.ts @@ -0,0 +1 @@ +export type { IncomingFaxEvent, IncomingFaxEventJson, IncomingFaxEventFormData } from './incoming-fax-event'; diff --git a/packages/fax/src/models/v3/mod-events/index.ts b/packages/fax/src/models/v3/mod-events/index.ts new file mode 100644 index 00000000..39332884 --- /dev/null +++ b/packages/fax/src/models/v3/mod-events/index.ts @@ -0,0 +1,2 @@ +export * from './fax-completed-event'; +export * from './incoming-fax-event'; diff --git a/packages/fax/src/models/v3/service-email-settings/index.ts b/packages/fax/src/models/v3/service-email-settings/index.ts new file mode 100644 index 00000000..2ec63f57 --- /dev/null +++ b/packages/fax/src/models/v3/service-email-settings/index.ts @@ -0,0 +1 @@ +export type { ServiceEmailSettings } from './service-email-settings'; diff --git a/packages/fax/src/models/v3/service-email-settings/service-email-settings.ts b/packages/fax/src/models/v3/service-email-settings/service-email-settings.ts new file mode 100644 index 00000000..fdee163f --- /dev/null +++ b/packages/fax/src/models/v3/service-email-settings/service-email-settings.ts @@ -0,0 +1,8 @@ + +export interface ServiceEmailSettings { + + /** The password to encrypt the PDF file */ + pdfPassword?: string; + /** Use the body of the email as the cover page */ + useBodyAsCoverPage?: boolean; +} diff --git a/packages/fax/src/models/v3/service-phone-number/index.ts b/packages/fax/src/models/v3/service-phone-number/index.ts new file mode 100644 index 00000000..18278be4 --- /dev/null +++ b/packages/fax/src/models/v3/service-phone-number/index.ts @@ -0,0 +1 @@ +export type { ServicePhoneNumber } from './service-phone-number'; diff --git a/packages/fax/src/models/v3/service-phone-number/service-phone-number.ts b/packages/fax/src/models/v3/service-phone-number/service-phone-number.ts new file mode 100644 index 00000000..131778b5 --- /dev/null +++ b/packages/fax/src/models/v3/service-phone-number/service-phone-number.ts @@ -0,0 +1,10 @@ + +export interface ServicePhoneNumber { + + /** A phone number in [E.164](https://community.sinch.com/t5/Glossary/E-164/ta-p/7537) format, including the leading \'+\'. */ + phoneNumber?: string; + /** The `Id` of the project associated with the call. */ + projectId?: string; + /** ID of the fax service used. */ + serviceId?: string; +} diff --git a/packages/fax/src/models/v3/service/index.ts b/packages/fax/src/models/v3/service/index.ts new file mode 100644 index 00000000..ddb75485 --- /dev/null +++ b/packages/fax/src/models/v3/service/index.ts @@ -0,0 +1 @@ +export type { ServiceRequest, ServiceResponse } from './service'; diff --git a/packages/fax/src/models/v3/service/service.ts b/packages/fax/src/models/v3/service/service.ts new file mode 100644 index 00000000..eb78237f --- /dev/null +++ b/packages/fax/src/models/v3/service/service.ts @@ -0,0 +1,38 @@ +import { ImageConversionMethod, WebhookContentType } from '../enums'; +import { ServiceEmailSettings } from '../service-email-settings'; + +/** + * You can use the default created service, or create multiple services within the same project to have different default behavior for all your different faxing use cases. + */ +export interface ServiceRequest { + + /** A friendly name for the service. Maximum is 60 characters. */ + name?: string; + /** The URL to which Sinch will post when someone sends a fax to your Sinch number. To accept incoming faxes this must be set and your Sinch phone number must be configured to receive faxes. */ + incomingWebhookUrl?: string; + /** The content type of the webhook. */ + webhookContentType?: WebhookContentType; + /** If set to true this is the service used to create faxes when no serviceId is specified in the API endpoints. */ + defaultForProject?: boolean; + /** One of your sinch numbers connected to this service or any of your verified numbers */ + defaultFrom?: string; + /** The number of times to retry sending a fax if it fails. Default is 3. Maximum is 5. */ + numberOfRetries?: number; + /** The number of seconds to wait between retries if the fax is not yet completed. */ + retryDelaySeconds?: number; + /** Determines how documents are converted to black and white. Value should be halftone or monochrome. Defaults to value selected on Fax Settings page */ + imageConversionMethod?: ImageConversionMethod; + /** Save fax documents with sinch when you send faxes */ + saveOutboundFaxDocuments?: boolean; + /** Save fax documents with sinch when you receive faxes */ + saveInboundFaxDocuments?: boolean; + /** @see ServiceEmailSettings */ + emailSettings?: ServiceEmailSettings; +} + +export interface ServiceResponse extends ServiceRequest { + /** ID of the fax service used. */ + id?: string; + /** The `Id` of the project associated with the call. */ + projectId?: string; +} diff --git a/packages/fax/src/models/v3/update-email-request/index.ts b/packages/fax/src/models/v3/update-email-request/index.ts new file mode 100644 index 00000000..dd4a0560 --- /dev/null +++ b/packages/fax/src/models/v3/update-email-request/index.ts @@ -0,0 +1 @@ +export type { UpdateEmailRequest } from './update-email-request'; diff --git a/packages/fax/src/models/v3/update-email-request/update-email-request.ts b/packages/fax/src/models/v3/update-email-request/update-email-request.ts new file mode 100644 index 00000000..6a67056f --- /dev/null +++ b/packages/fax/src/models/v3/update-email-request/update-email-request.ts @@ -0,0 +1,6 @@ + +export interface UpdateEmailRequest { + + /** List of numbers */ + phoneNumbers: string[]; +} diff --git a/packages/fax/src/rest/index.ts b/packages/fax/src/rest/index.ts new file mode 100644 index 00000000..daae7fa6 --- /dev/null +++ b/packages/fax/src/rest/index.ts @@ -0,0 +1 @@ +export * from './v3'; diff --git a/packages/fax/src/rest/v3/callbacks/callbacks-webhook.ts b/packages/fax/src/rest/v3/callbacks/callbacks-webhook.ts new file mode 100644 index 00000000..2b13e896 --- /dev/null +++ b/packages/fax/src/rest/v3/callbacks/callbacks-webhook.ts @@ -0,0 +1,66 @@ +import { CallbackProcessor } from '@sinch/sdk-client'; +import { IncomingHttpHeaders } from 'http'; +import { Fax, FaxCompletedEvent, IncomingFaxEvent } from '../../../models'; + +export type FaxWebhookEventParsed = IncomingFaxEvent | FaxCompletedEvent; + +export class FaxCallbackWebhooks implements CallbackProcessor { + + /** + * Reviver for a Fax Event + * This method ensures the object can be treated as a Fax Event and should be called before any action is taken to manipulate the object. + * @param {any} eventBody - the event body or form containing the Fax event notification. + * @return {FaxWebhookEventParsed} - The parsed Fax event object + */ + public parseEvent(eventBody: any): FaxWebhookEventParsed { + let incomingFaxEvent: IncomingFaxEvent | null = null; + let faxCompletedEvent: FaxCompletedEvent | null = null; + if (eventBody.event) { + switch (eventBody.event) { + case 'INCOMING_FAX': + incomingFaxEvent = eventBody as IncomingFaxEvent; + if (eventBody.eventTime) { + incomingFaxEvent.eventTime = new Date(eventBody.eventTime); + } + // In case of multipart/form-data, the server may not have parsed the 'fax' property as a JSON object, so we do it here + if (typeof eventBody.fax === 'string') { + incomingFaxEvent.fax = this.reviveFax(eventBody.fax); + } + return incomingFaxEvent; + case 'FAX_COMPLETED': + faxCompletedEvent = eventBody as FaxCompletedEvent; + if (faxCompletedEvent.eventTime) { + faxCompletedEvent.eventTime = new Date(faxCompletedEvent.eventTime); + } + // In case of multipart/form-data, the server may not have parsed the 'fax' property as a JSON object, so we do it here + if (typeof eventBody.fax === 'string') { + faxCompletedEvent.fax = this.reviveFax(eventBody.fax); + } + return faxCompletedEvent; + default: + throw new Error(`Unknown Fax event: ${eventBody.event}`); + } + } + console.log(eventBody); + throw new Error('Unknown Fax event'); + } + + private reviveFax(faxAsString: string): Fax { + const fax: Fax = JSON.parse(faxAsString); + if (fax.createTime) { + fax.createTime = new Date(fax.createTime); + } + if (fax.completedTime) { + fax.completedTime = new Date(fax.completedTime); + } + return fax; + } + + public validateAuthenticationHeader( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _headers: IncomingHttpHeaders, _body: any, _path: string, _method: string): boolean { + // No header validation is implemented for Fax API + return true; + } + +} diff --git a/packages/fax/src/rest/v3/callbacks/index.ts b/packages/fax/src/rest/v3/callbacks/index.ts new file mode 100644 index 00000000..f04a2490 --- /dev/null +++ b/packages/fax/src/rest/v3/callbacks/index.ts @@ -0,0 +1 @@ +export * from './callbacks-webhook'; diff --git a/packages/fax/src/rest/v3/emails/emails-api.jest.fixture.ts b/packages/fax/src/rest/v3/emails/emails-api.jest.fixture.ts new file mode 100644 index 00000000..b5c32349 --- /dev/null +++ b/packages/fax/src/rest/v3/emails/emails-api.jest.fixture.ts @@ -0,0 +1,35 @@ +import { Email, ServicePhoneNumber } from '../../../models'; +import { + EmailsApi, + DeleteEmailRequestData, + ListEmailsForProjectRequestData, + ListNumbersByEmailRequestData, + UpdateEmailRequestData, + AddEmailToNumbersRequestData, +} from './emails-api'; +import { ApiListPromise } from '@sinch/sdk-client'; + +export class EmailsApiFixture implements Partial> { + + /** + * Fixture associated to function addToNumbers + */ + public addToNumbers: jest.Mock, [AddEmailToNumbersRequestData]> = jest.fn(); + /** + * Fixture associated to function deleteEmail + */ + public delete: jest.Mock, [DeleteEmailRequestData]> = jest.fn(); + /** + * Fixture associated to function getEmailsForProject + */ + public list: jest.Mock, [ListEmailsForProjectRequestData]> = jest.fn(); + /** + * Fixture associated to function getNumbersByEmail + */ + public listNumbers: jest.Mock, [ListNumbersByEmailRequestData]> = jest.fn(); + /** + * Fixture associated to function updateEmail + */ + public update: jest.Mock, [UpdateEmailRequestData]> = jest.fn(); +} + diff --git a/packages/fax/src/rest/v3/emails/emails-api.ts b/packages/fax/src/rest/v3/emails/emails-api.ts new file mode 100644 index 00000000..1135753a --- /dev/null +++ b/packages/fax/src/rest/v3/emails/emails-api.ts @@ -0,0 +1,224 @@ +import { Email, EmailRequest, ServicePhoneNumber, UpdateEmailRequest } from '../../../models'; +import { + ApiListPromise, + buildPageResultPromise, + createIteratorMethodsForPagination, + PaginatedApiProperties, + PaginationEnum, + RequestBody, + SinchClientParameters, +} from '@sinch/sdk-client'; +import { FaxDomainApi } from '../fax-domain-api'; + +export interface AddEmailToNumbersRequestData { + /** */ + 'emailRequestBody': EmailRequest; +} +export interface DeleteEmailRequestData { + /** The email you want to delete. */ + 'email': string; +} +export interface ListEmailsForProjectRequestData { + /** Number of items to return on each page. */ + 'pageSize'?: number; + /** Optional. The page to fetch. If not specified, the first page will be returned. */ + 'page'?: string; +} +export interface ListNumbersByEmailRequestData { + /** The email you want to get numbers for. */ + 'email': string; + /** Number of items to return on each page. */ + 'pageSize'?: number; + /** Optional. The page to fetch. If not specified, the first page will be returned. */ + 'page'?: string; +} +export interface UpdateEmailRequestData { + /** The email you want to work with. */ + 'email': string; + /** */ + 'updateEmailRequestBody'?: UpdateEmailRequest; +} + +export class EmailsApi extends FaxDomainApi { + + /** + * Initialize your interface + * + * @param {SinchClientParameters} sinchClientParameters - The parameters used to initialize the API Client. + */ + constructor(sinchClientParameters: SinchClientParameters) { + super(sinchClientParameters, 'EmailsApi'); + } + + /** + * Add an email + * Add an email to be used for sending and receiving faxes. + * @param { AddEmailToNumbersRequestData } data - The data to provide to the API call. + */ + public async addToNumbers(data: AddEmailToNumbersRequestData): Promise { + this.client = this.getSinchClient(); + const getParams = this.client.extractQueryParams(data, [] as never[]); + const headers: { [key: string]: string | undefined } = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + + const body: RequestBody = data['emailRequestBody'] ? JSON.stringify(data['emailRequestBody']) : '{}'; + const basePathUrl = `${this.client.apiClientOptions.basePath}/v3/projects/${this.client.apiClientOptions.projectId}/emails`; + + const requestOptions = await this.client.prepareOptions(basePathUrl, 'POST', getParams, headers, body || undefined); + const url = this.client.prepareUrl(requestOptions.basePath, requestOptions.queryParams); + + return this.client.processCall({ + url, + requestOptions, + apiName: this.apiName, + operationId: 'CreateEmailForProject', + }); + } + + /** + * Remove email + * Delete an email and associated numbers to that email to disable that email from sending and receiving faxes. + * @param { DeleteEmailRequestData } data - The data to provide to the API call. + */ + public async delete(data: DeleteEmailRequestData): Promise { + this.client = this.getSinchClient(); + const getParams = this.client.extractQueryParams(data, [] as never[]); + const headers: { [key: string]: string | undefined } = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + + const body: RequestBody = data['email'] ? JSON.stringify(data['email']) : '{}'; + const basePathUrl = `${this.client.apiClientOptions.basePath}/v3/projects/${this.client.apiClientOptions.projectId}/emails/${data['email']}`; + + const requestOptions + = await this.client.prepareOptions(basePathUrl, 'DELETE', getParams, headers, body || undefined); + const url = this.client.prepareUrl(requestOptions.basePath, requestOptions.queryParams); + + return this.client.processCall({ + url, + requestOptions, + apiName: this.apiName, + operationId: 'DeleteEmail', + }); + } + + /** + * List emails + * List emails for the project. + * @param { ListEmailsForProjectRequestData } data - The data to provide to the API call. + * @return {ApiListPromise} - The list of emails for the project + */ + public list(data: ListEmailsForProjectRequestData): ApiListPromise { + this.client = this.getSinchClient(); + const getParams = this.client.extractQueryParams(data, ['pageSize', 'page']); + const headers: { [key: string]: string | undefined } = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + + const body: RequestBody = ''; + const basePathUrl = `${this.client.apiClientOptions.basePath}/v3/projects/${this.client.apiClientOptions.projectId}/emails`; + + const requestOptionsPromise = this.client.prepareOptions(basePathUrl, 'GET', getParams, headers, body || undefined); + + const operationProperties: PaginatedApiProperties = { + pagination: PaginationEnum.PAGE3, + apiName: this.apiName, + operationId: 'GetEmailsForProject', + dataKey: 'emails', + }; + + // Create the promise containing the response wrapped as a PageResult + const listPromise = buildPageResultPromise( + this.client, + requestOptionsPromise, + operationProperties); + + // Add properties to the Promise to offer the possibility to use it as an iterator + Object.assign( + listPromise, + createIteratorMethodsForPagination( + this.client, requestOptionsPromise, listPromise, operationProperties), + ); + + return listPromise as ApiListPromise; + } + + /** + * Get numbers for email + * Get configured numbers for an email + * @param { ListNumbersByEmailRequestData } data - The data to provide to the API call. + * @return {ApiListPromise} + */ + public listNumbers(data: ListNumbersByEmailRequestData): ApiListPromise { + this.client = this.getSinchClient(); + const getParams = this.client.extractQueryParams(data, [ + 'email', + 'pageSize', + 'page']); + const headers: { [key: string]: string | undefined } = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + + const body: RequestBody = ''; + const basePathUrl = `${this.client.apiClientOptions.basePath}/v3/projects/${this.client.apiClientOptions.projectId}/emails/${data['email']}/numbers`; + + const requestOptionsPromise = this.client.prepareOptions(basePathUrl, 'GET', getParams, headers, body || undefined); + + const operationProperties: PaginatedApiProperties = { + pagination: PaginationEnum.PAGE3, + apiName: this.apiName, + operationId: 'GetNumbersByEmail', + dataKey: 'phoneNumbers', + }; + + // Create the promise containing the response wrapped as a PageResult + const listPromise = buildPageResultPromise( + this.client, + requestOptionsPromise, + operationProperties); + + // Add properties to the Promise to offer the possibility to use it as an iterator + Object.assign( + listPromise, + createIteratorMethodsForPagination( + this.client, requestOptionsPromise, listPromise, operationProperties), + ); + + return listPromise as ApiListPromise; + } + + /** + * Update numbers for email + * Set the numbers for an email. + * @param { UpdateEmailRequestData } data - The data to provide to the API call. + */ + public async update(data: UpdateEmailRequestData): Promise { + this.client = this.getSinchClient(); + const getParams = this.client.extractQueryParams(data, [] as never[]); + const headers: { [key: string]: string | undefined } = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + + const body: RequestBody = data['updateEmailRequestBody'] + ? JSON.stringify(data['updateEmailRequestBody']) + : '{}'; + const basePathUrl = `${this.client.apiClientOptions.basePath}/v3/projects/${this.client.apiClientOptions.projectId}/emails/${data['email']}`; + + const requestOptions = await this.client.prepareOptions(basePathUrl, 'PUT', getParams, headers, body || undefined); + const url = this.client.prepareUrl(requestOptions.basePath, requestOptions.queryParams); + + return this.client.processCall({ + url, + requestOptions, + apiName: this.apiName, + operationId: 'UpdateEmail', + }); + } + +} diff --git a/packages/fax/src/rest/v3/emails/index.ts b/packages/fax/src/rest/v3/emails/index.ts new file mode 100644 index 00000000..19072ef6 --- /dev/null +++ b/packages/fax/src/rest/v3/emails/index.ts @@ -0,0 +1,2 @@ +export * from './emails-api'; +export * from './emails-api.jest.fixture'; diff --git a/packages/fax/src/rest/v3/enums.ts b/packages/fax/src/rest/v3/enums.ts new file mode 100644 index 00000000..28ebfa1d --- /dev/null +++ b/packages/fax/src/rest/v3/enums.ts @@ -0,0 +1,2 @@ +// export type { GetFaxFileByIdFileFormatEnum, SendFaxCallbackContentTypeEnum, SendFaxImageConversionMethodEnum } from './faxes'; +export {}; diff --git a/packages/fax/src/rest/v3/fax-domain-api.ts b/packages/fax/src/rest/v3/fax-domain-api.ts new file mode 100644 index 00000000..9071386b --- /dev/null +++ b/packages/fax/src/rest/v3/fax-domain-api.ts @@ -0,0 +1,81 @@ +import { + Api, + ApiClient, + ApiClientOptions, + ApiFetchClient, + SinchClientParameters, + Oauth2TokenRequest, + UnifiedCredentials, +} from '@sinch/sdk-client'; + +export class FaxDomainApi implements Api { + public readonly apiName: string; + public client?: ApiClient; + private sinchClientParameters: SinchClientParameters; + + constructor(sinchClientParameters: SinchClientParameters, apiName: string) { + this.sinchClientParameters = sinchClientParameters; + this.apiName = apiName; + } + + /** + * Update the default basePath for the API + * @param {string} basePath - The new base path to use for the APIs. + */ + public setBasePath(basePath: string) { + this.client = this.getSinchClient(); + this.client.apiClientOptions.basePath = basePath; + } + + /** + * Updates the credentials used to authenticate API requests + * @param {UnifiedCredentials} credentials + */ + public setCredentials(credentials: UnifiedCredentials) { + const parametersBackup = { ...this.sinchClientParameters }; + this.sinchClientParameters = { + ...parametersBackup, + ...credentials, + }; + this.resetApiClient(); + try { + this.getSinchClient(); + } catch (error) { + console.error('Impossible to assign the new credentials to the Fax API'); + this.sinchClientParameters = parametersBackup; + throw error; + } + } + + private resetApiClient() { + this.client = undefined; + } + + /** + * Checks the configuration parameters are ok and initialize the API client. Once initialized, the same instance will + * be returned for the subsequent API calls (singleton pattern) + * @return {ApiClient} the API Client or throws an error in case the configuration parameters are not ok + * @private + */ + public getSinchClient(): ApiClient { + if (!this.client) { + const apiClientOptions = this.buildApiClientOptions(this.sinchClientParameters); + this.client = new ApiFetchClient(apiClientOptions); + this.client.apiClientOptions.basePath = 'https://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, + }; + } + +} diff --git a/packages/fax/src/rest/v3/fax-service.ts b/packages/fax/src/rest/v3/fax-service.ts new file mode 100644 index 00000000..c8c0436c --- /dev/null +++ b/packages/fax/src/rest/v3/fax-service.ts @@ -0,0 +1,27 @@ +import { SinchClientParameters } from '@sinch/sdk-client'; +import { EmailsApi } from './emails'; +import { FaxesApi } from './faxes'; +import { ServicesApi } from './services'; + +export class FaxService { + public readonly emails: EmailsApi; + public readonly faxes: FaxesApi; + public readonly services: ServicesApi; + + constructor(params: SinchClientParameters) { + this.emails = new EmailsApi(params); + this.faxes = new FaxesApi(params); + this.services = new ServicesApi(params); + } + + /** + * Update the default basePath for each API + * + * @param {string} basePath - The new base path to use for all the APIs. + */ + public setBasePath(basePath: string) { + this.emails.setBasePath(basePath); + this.faxes.setBasePath(basePath); + this.services.setBasePath(basePath); + } +} diff --git a/packages/fax/src/rest/v3/faxes/faxes-api.jest.fixture.ts b/packages/fax/src/rest/v3/faxes/faxes-api.jest.fixture.ts new file mode 100644 index 00000000..1de9ba86 --- /dev/null +++ b/packages/fax/src/rest/v3/faxes/faxes-api.jest.fixture.ts @@ -0,0 +1,28 @@ +import { Fax } from '../../../models'; +import { FaxesApi, DeleteFaxContentRequestData, DownloadFaxContentRequestData, GetFaxRequestData, ListFaxesRequestData, SendFaxRequestData } from './faxes-api'; +import { ApiListPromise, FileBuffer } from '@sinch/sdk-client'; + +export class FaxesApiFixture implements Partial> { + + /** + * Fixture associated to function deleteFaxContentById + */ + public deleteContent: jest.Mock, [DeleteFaxContentRequestData]> = jest.fn(); + /** + * Fixture associated to function getFaxFileById + */ + public downloadContent: jest.Mock, [DownloadFaxContentRequestData]> = jest.fn(); + /** + * Fixture associated to function getFaxInfoPerId + */ + public get: jest.Mock, [GetFaxRequestData]> = jest.fn(); + /** + * Fixture associated to function getFaxes + */ + public list: jest.Mock, [ListFaxesRequestData]> = jest.fn(); + /** + * Fixture associated to function sendFax + */ + public send: jest.Mock, [SendFaxRequestData]> = jest.fn(); +} + diff --git a/packages/fax/src/rest/v3/faxes/faxes-api.ts b/packages/fax/src/rest/v3/faxes/faxes-api.ts new file mode 100644 index 00000000..73978363 --- /dev/null +++ b/packages/fax/src/rest/v3/faxes/faxes-api.ts @@ -0,0 +1,273 @@ +import { + ApiListPromise, + buildPageResultPromise, + createIteratorMethodsForPagination, + FileBuffer, + PaginatedApiProperties, + PaginationEnum, + RequestBody, + SinchClientParameters, +} from '@sinch/sdk-client'; +import { FaxDomainApi } from '../fax-domain-api'; +import { + Fax, + FaxDirection, + FaxStatus, + FaxRequest, + FaxRequestJson, + FaxRequestFormData, +} from '../../../models'; + +export interface DeleteFaxContentRequestData { + /** The ID of the fax. */ + 'id': string; +} +export interface DownloadFaxContentRequestData { + /** The ID of the fax. */ + 'id': string; + /** The file format to download. Currently only PDF is supported. */ + 'fileFormat'?: 'pdf'; +} +export interface GetFaxRequestData { + /** The ID of the fax. */ + 'id': string; +} +export interface ListFaxesRequestData { + /** Filter calls based on `createTime`. If you make the query more precise, fewer results will be returned. For example, `2021-02-01` will return all calls from the first of February 2021, and `2021-02-01T14:00:00Z` will return all calls after 14:00 on the first of February. This field also supports `<=` and `>=` to search for calls in a range `?createTime>=2021-10-01&startTime<=2021-10-30` to get a list of calls for all of October 2021. It is also possible to submit partial dates. For example, `createTime=2021-02` will return all calls for February 2021. If not value is submitted, the default value is the prior week. */ + 'createTime'?: string; + /** Limits results to faxes with the specified direction. */ + 'direction'?: FaxDirection; + /** Limits results to faxes with the specified status. */ + 'status'?: FaxStatus; + /** A phone number that you want to use to filter results. The parameter search with startsWith, so you can pass a partial number to get all faxes sent to numbers that start with the number you passed. */ + 'to'?: string; + /** A phone number that you want to use to filter results. The parameter search with startsWith, so you can pass a partial number to get all faxes sent to numbers that start with the number you passed. */ + 'from'?: string; + /** The maximum number of items to return per request. The default is 100 and the maximum is 1000. If you need to export larger amounts and pagination is not suitable for you can use the Export function in the dashboard. */ + 'pageSize'?: number; + /** The page you want to retrieve returned from a previous List request, if any */ + 'page'?: string; +} +export interface SendFaxRequestData { + 'sendFaxRequestBody': FaxRequest; +} + +export class FaxesApi extends FaxDomainApi { + + /** + * Initialize your interface + * + * @param {SinchClientParameters} sinchClientParameters - The parameters used to initialize the API Client. + */ + constructor(sinchClientParameters: SinchClientParameters) { + super(sinchClientParameters, 'FaxesApi'); + } + + /** + * Delete fax content + * Delete the fax content for a fax using the ID number of the fax. Please note that this only deletes the content of the fax from storage. + * @param { DeleteFaxContentRequestData } data - The data to provide to the API call. + */ + public async deleteContent(data: DeleteFaxContentRequestData): Promise { + this.client = this.getSinchClient(); + const getParams = this.client.extractQueryParams(data, [] as never[]); + const headers: { [key: string]: string | undefined } = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + + const body: RequestBody = ''; + const basePathUrl = `${this.client.apiClientOptions.basePath}/v3/projects/${this.client.apiClientOptions.projectId}/faxes/${data['id']}/file`; + + const requestOptions + = await this.client.prepareOptions(basePathUrl, 'DELETE', getParams, headers, body || undefined); + const url = this.client.prepareUrl(requestOptions.basePath, requestOptions.queryParams); + + return this.client.processCall({ + url, + requestOptions, + apiName: this.apiName, + operationId: 'DeleteFaxContentById', + }); + } + + /** + * Download fax content + * Download the fax content. + * @param { DownloadFaxContentRequestData } data - The data to provide to the API call. + */ + public async downloadContent(data: DownloadFaxContentRequestData): Promise { + this.client = this.getSinchClient(); + data['fileFormat'] = data['fileFormat'] !== undefined ? data['fileFormat'] : 'pdf'; + const getParams = this.client.extractQueryParams(data, [] as never[]); + const headers: { [key: string]: string | undefined } = { + 'Content-Type': 'application/json', + 'Accept': 'application/pdf', + }; + + const body: RequestBody = ''; + const basePathUrl = `${this.client.apiClientOptions.basePath}/v3/projects/${this.client.apiClientOptions.projectId}/faxes/${data['id']}/file.${data['fileFormat']}`; + + const requestOptions = await this.client.prepareOptions(basePathUrl, 'GET', getParams, headers, body || undefined); + const url = this.client.prepareUrl(requestOptions.basePath, requestOptions.queryParams); + + return this.client.processFileCall({ + url, + requestOptions, + apiName: this.apiName, + operationId: 'GetFaxFileById', + }); + } + + /** + * Get fax + * Get fax information using the ID number of the fax. + * @param { GetFaxRequestData } data - The data to provide to the API call. + */ + public async get(data: GetFaxRequestData): Promise { + this.client = this.getSinchClient(); + const getParams = this.client.extractQueryParams(data, [] as never[]); + const headers: { [key: string]: string | undefined } = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + + const body: RequestBody = ''; + const basePathUrl = `${this.client.apiClientOptions.basePath}/v3/projects/${this.client.apiClientOptions.projectId}/faxes/${data['id']}`; + + const requestOptions = await this.client.prepareOptions(basePathUrl, 'GET', getParams, headers, body || undefined); + const url = this.client.prepareUrl(requestOptions.basePath, requestOptions.queryParams); + + return this.client.processCall({ + url, + requestOptions, + apiName: this.apiName, + operationId: 'GetFaxInfoPerId', + }); + } + + /** + * List faxes + * List faxes sent (OUTBOUND) or received (INBOUND), set parameters to filter the list. Example: Return calls made between 1st of January 2021 and 10th of January 2021. ``` created>=2021-01-01&startTime<=2021-01-10 ``` + * @param { ListFaxesRequestData } data - The data to provide to the API call. + * @return {ApiListPromise} + */ + public list(data: ListFaxesRequestData): ApiListPromise { + this.client = this.getSinchClient(); + const getParams = this.client.extractQueryParams(data, [ + 'createTime', + 'direction', + 'status', + 'to', + 'from', + 'pageSize', + 'page']); + const headers: { [key: string]: string | undefined } = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + + const body: RequestBody = ''; + const basePathUrl = `${this.client.apiClientOptions.basePath}/v3/projects/${this.client.apiClientOptions.projectId}/faxes`; + + const requestOptionsPromise = this.client.prepareOptions(basePathUrl, 'GET', getParams, headers, body || undefined); + + const operationProperties: PaginatedApiProperties = { + pagination: PaginationEnum.PAGE3, + apiName: this.apiName, + operationId: 'GetFaxes', + dataKey: 'faxes', + }; + + // Create the promise containing the response wrapped as a PageResult + const listPromise = buildPageResultPromise( + this.client, + requestOptionsPromise, + operationProperties); + + // Add properties to the Promise to offer the possibility to use it as an iterator + Object.assign( + listPromise, + createIteratorMethodsForPagination( + this.client, requestOptionsPromise, listPromise, operationProperties), + ); + + return listPromise as ApiListPromise; + } + + /** + * Send a fax + * Create and send a fax. Fax content may be supplied via one or more files or URLs of supported filetypes. + * This endpoint supports the following content types for the fax payload: + * - Multipart/form-data + * - Application/json + * We will however always return a fax object in the response in application json. + * If you supply a callbackUrl the callback will be sent as multipart/form-data with the content of the fax as an attachment to the body, *unless* you specify callbackContentType as application/json. + * #### file(s) + * Files may be included in the POST request as multipart/form-data parts. + * #### contentUrl + * Any URL on the Internet (including ones with basic authentication), and we'll pull it down and make it a fax. This might be useful to you if you're using a web framework for templates and creating fax files. + * Please note: If you are passing fax a secure URL (starting with 'https://'), make sure that your SSL certificate (including your intermediate cert, if you have one) is installed properly, valid, and up-to-date. + * @param { SendFaxRequestData } data - The data to provide to the API call. + */ + public async send(data: SendFaxRequestData): Promise { + this.client = this.getSinchClient(); + const requestBody = data.sendFaxRequestBody; + requestBody['headerText'] = requestBody['headerText'] !== undefined + ? requestBody['headerText'] : ''; + requestBody['headerPageNumbers'] = requestBody['headerPageNumbers'] !== undefined + ? requestBody['headerPageNumbers'] : true; + requestBody['headerTimeZone'] = requestBody['headerTimeZone'] !== undefined + ? requestBody['headerTimeZone'] : 'America/New_York'; + requestBody['retryDelaySeconds'] = requestBody['retryDelaySeconds'] !== undefined + ? requestBody['retryDelaySeconds'] : 60; + requestBody['callbackContentType'] = requestBody['callbackContentType'] !== undefined + ? requestBody['callbackContentType'] : 'multipart/form-data'; + requestBody['imageConversionMethod'] = requestBody['imageConversionMethod'] !== undefined + ? requestBody['imageConversionMethod'] : 'HALFTONE'; + const getParams = this.client.extractQueryParams(data, [] as never[]); + const headers: { [key: string]: string | undefined } = { + 'Accept': 'application/json', + }; + + let body: RequestBody; + // Except if the request body contains a non-empty property 'files' (where the message will be sent as application/json) + // the request will be sent as multipart/formdata + const isUsingJson = (data['sendFaxRequestBody'] as FaxRequestJson).files !== undefined; + if (isUsingJson) { + headers['Content-Type'] = 'application/json'; + body = JSON.stringify(data['sendFaxRequestBody']); + } else { + const formParams:any = {}; + const requestData = data.sendFaxRequestBody as FaxRequestFormData; + if( requestData.to ) { formParams['to'] = requestData.to; } + if( requestData.file ) { formParams['file'] = requestData.file; } + if( requestData.from ) { formParams['from'] = requestData.from; } + if( requestData.contentUrl ) { formParams['contentUrl'] = requestData.contentUrl; } + if( requestData.headerText ) { formParams['headerText'] = requestData.headerText; } + if( requestData.headerPageNumbers ) { formParams['headerPageNumbers'] = requestData.headerPageNumbers; } + if( requestData.headerTimeZone ) { formParams['headerTimeZone'] = requestData.headerTimeZone; } + if( requestData.retryDelaySeconds ) { formParams['retryDelaySeconds'] = requestData.retryDelaySeconds; } + if( requestData.labels ) { formParams['labels'] = requestData.labels; } + if( requestData.callbackUrl ) { formParams['callbackUrl'] = requestData.callbackUrl; } + if( requestData.callbackContentType ) { formParams['callbackContentType'] = requestData.callbackContentType; } + if( requestData.imageConversionMethod ) {formParams['imageConversionMethod'] = requestData.imageConversionMethod;} + if( requestData.serviceId ) { formParams['serviceId'] = requestData.serviceId; } + if( requestData.maxRetries ) { formParams['maxRetries'] = requestData.maxRetries; } + body = this.client.processFormData(formParams, 'multipart/form-data'); + } + + const basePathUrl = `${this.client.apiClientOptions.basePath}/v3/projects/${this.client.apiClientOptions.projectId}/faxes`; + + const requestOptions = await this.client.prepareOptions(basePathUrl, 'POST', getParams, headers, body || undefined); + const url = this.client.prepareUrl(requestOptions.basePath, requestOptions.queryParams); + + return this.client.processCall({ + url, + requestOptions, + apiName: this.apiName, + operationId: 'SendFax', + }); + } + +} diff --git a/packages/fax/src/rest/v3/faxes/index.ts b/packages/fax/src/rest/v3/faxes/index.ts new file mode 100644 index 00000000..1b6230b9 --- /dev/null +++ b/packages/fax/src/rest/v3/faxes/index.ts @@ -0,0 +1,2 @@ +export * from './faxes-api'; +export * from './faxes-api.jest.fixture'; diff --git a/packages/fax/src/rest/v3/index.ts b/packages/fax/src/rest/v3/index.ts new file mode 100644 index 00000000..110257fa --- /dev/null +++ b/packages/fax/src/rest/v3/index.ts @@ -0,0 +1,6 @@ +export * from './callbacks'; +export * from './emails'; +export * from './faxes'; +export * from './services'; +export * from './enums'; +export * from './fax-service'; diff --git a/packages/fax/src/rest/v3/services/index.ts b/packages/fax/src/rest/v3/services/index.ts new file mode 100644 index 00000000..5ebde6b2 --- /dev/null +++ b/packages/fax/src/rest/v3/services/index.ts @@ -0,0 +1,2 @@ +export * from './services-api'; +export * from './services-api.jest.fixture'; diff --git a/packages/fax/src/rest/v3/services/services-api.jest.fixture.ts b/packages/fax/src/rest/v3/services/services-api.jest.fixture.ts new file mode 100644 index 00000000..4123c67f --- /dev/null +++ b/packages/fax/src/rest/v3/services/services-api.jest.fixture.ts @@ -0,0 +1,46 @@ +import { ServicePhoneNumber, ServiceResponse } from '../../../models'; +import { + ServicesApi, + CreateServiceRequestData, + GetServiceRequestData, + ListNumbersForServiceRequestData, + ListServicesRequestData, + DeleteServiceRequestData, + UpdateServiceRequestData, + ListEmailsForNumberRequestData, +} from './services-api'; +import { ApiListPromise } from '@sinch/sdk-client'; + +export class ServicesApiFixture implements Partial> { + + /** + * Fixture associated to function create + */ + public create: jest.Mock, [CreateServiceRequestData]> = jest.fn(); + /** + * Fixture associated to function get + */ + public get: jest.Mock, [GetServiceRequestData]> = jest.fn(); + /** + * Fixture associated to function listEmailsForNumber + */ + public listEmailsForNumber: jest.Mock, [ListEmailsForNumberRequestData]> = jest.fn(); + /** + * Fixture associated to function listNumbers + */ + public listNumbers: + jest.Mock, [ListNumbersForServiceRequestData]> = jest.fn(); + /** + * Fixture associated to function list + */ + public list: jest.Mock, [ListServicesRequestData]> = jest.fn(); + /** + * Fixture associated to function delete + */ + public delete: jest.Mock, [DeleteServiceRequestData]> = jest.fn(); + /** + * Fixture associated to function update + */ + public update: jest.Mock, [UpdateServiceRequestData]> = jest.fn(); +} + diff --git a/packages/fax/src/rest/v3/services/services-api.ts b/packages/fax/src/rest/v3/services/services-api.ts new file mode 100644 index 00000000..642aaf26 --- /dev/null +++ b/packages/fax/src/rest/v3/services/services-api.ts @@ -0,0 +1,313 @@ +import { + ServicePhoneNumber, + ServiceRequest, + ServiceResponse, +} from '../../../models'; +import { + ApiListPromise, + buildPageResultPromise, + createIteratorMethodsForPagination, + PaginatedApiProperties, + PaginationEnum, + RequestBody, + SinchClientParameters, +} from '@sinch/sdk-client'; +import { FaxDomainApi } from '../fax-domain-api'; + +export interface CreateServiceRequestData { + /** */ + 'createServiceRequestBody'?: ServiceRequest; +} +export interface GetServiceRequestData { + /** The service ID you want to update. */ + 'serviceId': string; +} +export interface ListEmailsForNumberRequestData { + /** The phone number you want to get emails for. */ + 'phoneNumber': string; + /** The serviceId containing the numbers you want to list. */ + 'serviceId': string; + /** Number of items to return on each page. */ + 'pageSize'?: number; + /** Optional. The page to fetch. If not specified, the first page will be returned. */ + 'page'?: string; +} +export interface ListNumbersForServiceRequestData { + /** The serviceId containing the numbers you want to list. */ + 'serviceId': string; + /** Number of items to return on each page. */ + 'pageSize'?: number; + /** Optional. The page to fetch. If not specified, the first page will be returned. */ + 'page'?: string; +} +export interface ListServicesRequestData { + /** Number of services to return on each request. */ + 'pageSize'?: number; + /** Optional. The page to fetch. If not specified, the first page will be returned. */ + 'page'?: string; +} +export interface DeleteServiceRequestData { + /** The serviceId you want to remove from your project. */ + 'serviceId': string; +} +export interface UpdateServiceRequestData { + /** The service ID you want to update. */ + 'serviceId': string; + /** */ + 'updateServiceRequestBody'?: ServiceRequest; +} + +export class ServicesApi extends FaxDomainApi { + + /** + * Initialize your interface + * + * @param {SinchClientParameters} sinchClientParameters - The parameters used to initialize the API Client. + */ + constructor(sinchClientParameters: SinchClientParameters) { + super(sinchClientParameters, 'ServicesApi'); + } + + /** + * Create a service + * Creates a new service that you can use to set default configuration values. + * @param { CreateServiceRequestData } data - The data to provide to the API call. + */ + public async create(data: CreateServiceRequestData): Promise { + this.client = this.getSinchClient(); + const getParams = this.client.extractQueryParams(data, [] as never[]); + const headers: { [key: string]: string | undefined } = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + + const body: RequestBody = data['createServiceRequestBody'] + ? JSON.stringify(data['createServiceRequestBody']) + : '{}'; + const basePathUrl = `${this.client.apiClientOptions.basePath}/v3/projects/${this.client.apiClientOptions.projectId}/services`; + + const requestOptions = await this.client.prepareOptions(basePathUrl, 'POST', getParams, headers, body || undefined); + const url = this.client.prepareUrl(requestOptions.basePath, requestOptions.queryParams); + + return this.client.processCall({ + url, + requestOptions, + apiName: this.apiName, + operationId: 'CreateService', + }); + } + + /** + * Get a service + * Get a service resource. + * @param { GetServiceRequestData } data - The data to provide to the API call. + */ + public async get(data: GetServiceRequestData): Promise { + this.client = this.getSinchClient(); + const getParams = this.client.extractQueryParams(data, [] as never[]); + const headers: { [key: string]: string | undefined } = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + + const body: RequestBody = ''; + const basePathUrl = `${this.client.apiClientOptions.basePath}/v3/projects/${this.client.apiClientOptions.projectId}/services/${data['serviceId']}`; + + const requestOptions = await this.client.prepareOptions(basePathUrl, 'GET', getParams, headers, body || undefined); + const url = this.client.prepareUrl(requestOptions.basePath, requestOptions.queryParams); + + return this.client.processCall({ + url, + requestOptions, + apiName: this.apiName, + operationId: 'GetService', + }); + } + + /** + * List emails for a number + * List any emails for a number. + * @param { ListEmailsForNumberRequestData } data - The data to provide to the API call. + * @return {ApiListPromise} - The list of emails for a given number + */ + public listEmailsForNumber(data: ListEmailsForNumberRequestData): ApiListPromise { + this.client = this.getSinchClient(); + const getParams = this.client.extractQueryParams(data, ['pageSize', 'page']); + const headers: { [key: string]: string | undefined } = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + + const body: RequestBody = ''; + const basePathUrl = `${this.client.apiClientOptions.basePath}/v3/projects/${this.client.apiClientOptions.projectId}/services/${data['serviceId']}/numbers/${data['phoneNumber']}/emails`; + + const requestOptionsPromise = this.client.prepareOptions(basePathUrl, 'GET', getParams, headers, body || undefined); + + const operationProperties: PaginatedApiProperties = { + pagination: PaginationEnum.PAGE3, + apiName: this.apiName, + operationId: 'GetEmailsForNumber', + dataKey: 'emails', + }; + + // Create the promise containing the response wrapped as a PageResult + const listPromise = buildPageResultPromise( + this.client, + requestOptionsPromise, + operationProperties); + + // Add properties to the Promise to offer the possibility to use it as an iterator + Object.assign( + listPromise, + createIteratorMethodsForPagination( + this.client, requestOptionsPromise, listPromise, operationProperties), + ); + + return listPromise as ApiListPromise; + } + + /** + * List numbers for service + * List numbers for a service. + * @param { ListNumbersForServiceRequestData } data - The data to provide to the API call. + * @return {ApiListPromise} + */ + public listNumbers(data: ListNumbersForServiceRequestData): ApiListPromise { + this.client = this.getSinchClient(); + data['pageSize'] = data['pageSize'] !== undefined ? data['pageSize'] : 20; + const getParams = this.client.extractQueryParams(data, ['pageSize', 'page']); + const headers: { [key: string]: string | undefined } = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + + const body: RequestBody = ''; + const basePathUrl = `${this.client.apiClientOptions.basePath}/v3/projects/${this.client.apiClientOptions.projectId}/services/${data['serviceId']}/numbers`; + + const requestOptionsPromise = this.client.prepareOptions(basePathUrl, 'GET', getParams, headers, body || undefined); + + const operationProperties: PaginatedApiProperties = { + pagination: PaginationEnum.PAGE3, + apiName: this.apiName, + operationId: 'ListNumbersForService', + dataKey: 'numbers', + }; + + // Create the promise containing the response wrapped as a PageResult + const listPromise = buildPageResultPromise( + this.client, + requestOptionsPromise, + operationProperties); + + // Add properties to the Promise to offer the possibility to use it as an iterator + Object.assign( + listPromise, + createIteratorMethodsForPagination( + this.client, requestOptionsPromise, listPromise, operationProperties), + ); + + return listPromise as ApiListPromise; + } + + /** + * List services + * Get a list of services for a project. + * @param { ListServicesRequestData } data - The data to provide to the API call. + * @return {ApiListPromise} + */ + public list(data: ListServicesRequestData): ApiListPromise { + this.client = this.getSinchClient(); + data['pageSize'] = data['pageSize'] !== undefined ? data['pageSize'] : 20; + const getParams = this.client.extractQueryParams(data, ['pageSize', 'page']); + const headers: { [key: string]: string | undefined } = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + + const body: RequestBody = ''; + const basePathUrl = `${this.client.apiClientOptions.basePath}/v3/projects/${this.client.apiClientOptions.projectId}/services`; + + const requestOptionsPromise = this.client.prepareOptions(basePathUrl, 'GET', getParams, headers, body || undefined); + + const operationProperties: PaginatedApiProperties = { + pagination: PaginationEnum.PAGE3, + apiName: this.apiName, + operationId: 'ListServices', + dataKey: 'services', + }; + + // Create the promise containing the response wrapped as a PageResult + const listPromise = buildPageResultPromise( + this.client, + requestOptionsPromise, + operationProperties); + + // Add properties to the Promise to offer the possibility to use it as an iterator + Object.assign( + listPromise, + createIteratorMethodsForPagination( + this.client, requestOptionsPromise, listPromise, operationProperties), + ); + + return listPromise as ApiListPromise; + } + + /** + * Remove a service + * Removes a service from your project. + * @param { DeleteServiceRequestData } data - The data to provide to the API call. + */ + public async delete(data: DeleteServiceRequestData): Promise { + this.client = this.getSinchClient(); + const getParams = this.client.extractQueryParams(data, [] as never[]); + const headers: { [key: string]: string | undefined } = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + + const body: RequestBody = ''; + const basePathUrl = `${this.client.apiClientOptions.basePath}/v3/projects/${this.client.apiClientOptions.projectId}/services/${data['serviceId']}`; + + const requestOptions + = await this.client.prepareOptions(basePathUrl, 'DELETE', getParams, headers, body || undefined); + const url = this.client.prepareUrl(requestOptions.basePath, requestOptions.queryParams); + + return this.client.processCall({ + url, + requestOptions, + apiName: this.apiName, + operationId: 'RemoveService', + }); + } + + /** + * Update a Service + * Update settings on the service. + * @param { UpdateServiceRequestData } data - The data to provide to the API call. + */ + public async update(data: UpdateServiceRequestData): Promise { + this.client = this.getSinchClient(); + const getParams = this.client.extractQueryParams(data, [] as never[]); + const headers: { [key: string]: string | undefined } = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + + const body: RequestBody = data['updateServiceRequestBody'] + ? JSON.stringify(data['updateServiceRequestBody']) + : '{}'; + const basePathUrl = `${this.client.apiClientOptions.basePath}/v3/projects/${this.client.apiClientOptions.projectId}/services/${data['serviceId']}`; + + const requestOptions + = await this.client.prepareOptions(basePathUrl, 'PATCH', getParams, headers, body || undefined); + const url = this.client.prepareUrl(requestOptions.basePath, requestOptions.queryParams); + + return this.client.processCall({ + url, + requestOptions, + apiName: this.apiName, + operationId: 'UpdateService', + }); + } + +} diff --git a/packages/fax/tests/rest/v3/emails/emails-api.test.ts b/packages/fax/tests/rest/v3/emails/emails-api.test.ts new file mode 100644 index 00000000..f5e26a3d --- /dev/null +++ b/packages/fax/tests/rest/v3/emails/emails-api.test.ts @@ -0,0 +1,172 @@ +import { SinchClientParameters } from '@sinch/sdk-client'; +import { + AddEmailToNumbersRequestData, + DeleteEmailRequestData, + Email, + ListEmailsForProjectRequestData, ListNumbersByEmailRequestData, ServicePhoneNumber, UpdateEmailRequestData, +} from '../../../../src'; +import { EmailsApi, EmailsApiFixture } from '../../../../src'; + +describe('EmailsApi', () => { + let emailsApi: EmailsApi; + let fixture: EmailsApiFixture; + let credentials: SinchClientParameters; + + beforeEach(() => { + fixture = new EmailsApiFixture(); + credentials = { + projectId: 'PROJECT_ID', + keyId: 'KEY_ID', + keySecret: 'KEY_SECRET', + }; + emailsApi = new EmailsApi(credentials); + }); + + + describe ('createEmailForProject', () => { + it('should make a POST request to add an email to be used for sending and receiving faxes', async () => { + // Given + const requestData: AddEmailToNumbersRequestData = { + emailRequestBody: { + email: 'user@domain.com', + phoneNumbers: [ + '+14155552222', + ], + }, + }; + const expectedResponse: Email = { + email: 'user@domain.com', + phoneNumbers: [ + '+14155552222', + ], + projectId: 'projectId', + }; + + // When + fixture.addToNumbers.mockResolvedValue(expectedResponse); + emailsApi.addToNumbers = fixture.addToNumbers; + const response = await emailsApi.addToNumbers(requestData); + + // Then + expect(response).toEqual(expectedResponse); + expect(fixture.addToNumbers).toHaveBeenCalledWith(requestData); + }); + }); + + describe ('deleteEmail', () => { + it('should make a DELETE request to delete an email and its numbers association', async () => { + // Given + const requestData: DeleteEmailRequestData = { + email: 'user@domain.com', + }; + const expectedResponse = undefined; + + // When + fixture.delete.mockResolvedValue(expectedResponse); + emailsApi.delete = fixture.delete; + const response = await emailsApi.delete(requestData); + + // Then + expect(response).toEqual(expectedResponse); + expect(fixture.delete).toHaveBeenCalledWith(requestData); + }); + }); + + describe ('getEmailsForProject', () => { + it('should make a GET request to list emails for the project', async () => { + // Given + const requestData: ListEmailsForProjectRequestData = { + pageSize: 2, + }; + const mockData: Email[] = [ + { + email: 'user@domain.com', + phoneNumbers: [ + '+14155552222', + ], + projectId: 'projectId', + }, + ]; + const expectedResponse = { + data: mockData, + hasNextPage: false, + nextPageValue: '', + nextPage: jest.fn(), + }; + + // When + fixture.list.mockResolvedValue(expectedResponse); + emailsApi.list = fixture.list; + const response = await emailsApi.list(requestData); + + // Then + expect(response).toEqual(expectedResponse); + expect(fixture.list).toHaveBeenCalledWith(requestData); + }); + }); + + describe ('getNumbersByEmail', () => { + it('should make a GET request to list the configured numbers for an email', async () => { + // Given + const requestData: ListNumbersByEmailRequestData = { + email: 'user@domain.com', + pageSize: 2, + }; + const mockData: ServicePhoneNumber[] = [ + { + phoneNumber: '+14155552222', + serviceId: 'serviceId', + projectId: 'projectId', + }, + ]; + const expectedResponse = { + data: mockData, + hasNextPage: false, + nextPageValue: '', + nextPage: jest.fn(), + }; + + // When + fixture.listNumbers.mockResolvedValue(expectedResponse); + emailsApi.listNumbers = fixture.listNumbers; + const response = await emailsApi.listNumbers(requestData); + + // Then + expect(response).toEqual(expectedResponse); + expect(response.data).toBeDefined(); + expect(fixture.listNumbers).toHaveBeenCalledWith(requestData); + }); + }); + + describe ('updateEmail', () => { + it('should make a PUT request to set the numbers for an email', async () => { + // Given + const requestData: UpdateEmailRequestData = { + email: 'user@domain.com', + updateEmailRequestBody: { + phoneNumbers: [ + '+14155552222', + '+14155553333', + ], + }, + }; + const expectedResponse: Email = { + email: 'user@domain.com', + phoneNumbers: [ + '+14155552222', + '+14155553333', + ], + projectId: 'projectId', + }; + + // When + fixture.update.mockResolvedValue(expectedResponse); + emailsApi.update = fixture.update; + const response = await emailsApi.update(requestData); + + // Then + expect(response).toEqual(expectedResponse); + expect(fixture.update).toHaveBeenCalledWith(requestData); + }); + }); +}); diff --git a/packages/fax/tests/rest/v3/faxes/faxes-api.test.ts b/packages/fax/tests/rest/v3/faxes/faxes-api.test.ts new file mode 100644 index 00000000..18147263 --- /dev/null +++ b/packages/fax/tests/rest/v3/faxes/faxes-api.test.ts @@ -0,0 +1,195 @@ +import { FileBuffer, SinchClientParameters } from '@sinch/sdk-client'; +import { + DeleteFaxContentRequestData, Fax, + FaxesApi, + FaxesApiFixture, ListFaxesRequestData, + DownloadFaxContentRequestData, + GetFaxRequestData, SendFaxRequestData, +} from '../../../../src'; + +describe('FaxesApi', () => { + let faxesApi: FaxesApi; + let fixture: FaxesApiFixture; + let credentials: SinchClientParameters; + + beforeEach(() => { + fixture = new FaxesApiFixture(); + credentials = { + projectId: 'PROJECT_ID', + keyId: 'KEY_ID', + keySecret: 'KEY_SECRET', + }; + faxesApi = new FaxesApi(credentials); + }); + + + describe ('deleteFaxContentById', () => { + it('should make a DELETE request to delete the content of a fax from storage', async () => { + // Given + const requestData: DeleteFaxContentRequestData = { + id: 'fax_id', + }; + const expectedResponse: void = undefined; + + // When + fixture.deleteContent.mockResolvedValue(expectedResponse); + faxesApi.deleteContent = fixture.deleteContent; + const response = await faxesApi.deleteContent(requestData); + + // Then + expect(response).toEqual(expectedResponse); + expect(fixture.deleteContent).toHaveBeenCalledWith(requestData); + }); + }); + + describe ('getFaxFileById', () => { + it('should make a GET request to download a fax content', async () => { + // Given + const requestData: DownloadFaxContentRequestData = { + id: 'fax_id', + }; + const expectedResponse: FileBuffer = { + fileName: 'default-name.pdf', + buffer: Buffer.from('PDF file content goes here', 'utf-8'), + }; + + // When + fixture.downloadContent.mockResolvedValue(expectedResponse); + faxesApi.downloadContent = fixture.downloadContent; + const response = await faxesApi.downloadContent(requestData); + + // Then + expect(response).toEqual(expectedResponse); + expect(fixture.downloadContent).toHaveBeenCalledWith(requestData); + }); + }); + + describe ('getFaxInfoPerId', () => { + it('should make a GET request to retrieve some fax information', async () => { + // Given + const requestData: GetFaxRequestData = { + id: 'fax_id', + }; + const expectedResponse: Fax = { + id: 'fax_id', + direction: 'OUTBOUND', + to: '+12015555555', + status: 'FAILURE', + headerTimeZone: 'America/New_York', + retryDelaySeconds: 60, + callbackContentType: 'multipart/form-data', + errorType: 'LINE_ERROR', + errorId: 89, + errorCode: 'The call dropped prematurely', + projectId: 'projectId', + serviceId: 'serviceId', + maxRetries: 3, + createTime: new Date('2024-02-27T12:28:09.000Z'), + headerPageNumbers: true, + retryCount: 3, + contentUrl: [ + 'https://developers.sinch.com/fax/fax.pdf', + ], + imageConversionMethod: 'HALFTONE', + hasFile: 'true', + }; + + // When + fixture.get.mockResolvedValue(expectedResponse); + faxesApi.get = fixture.get; + const response = await faxesApi.get(requestData); + + // Then + expect(response).toEqual(expectedResponse); + expect(fixture.get).toHaveBeenCalledWith(requestData); + }); + }); + + describe ('getFaxes', () => { + it('should make a GET request to list faxes sent or received', async () => { + // Given + const requestData: ListFaxesRequestData = { + direction: 'OUTBOUND', + }; + const mockData: Fax[] = [ + { + id: 'fax_id', + direction: 'OUTBOUND', + to: '+12015555555', + status: 'FAILURE', + headerTimeZone: 'America/New_York', + retryDelaySeconds: 60, + callbackContentType: 'multipart/form-data', + errorType: 'LINE_ERROR', + errorId: 89, + errorCode: 'The call dropped prematurely', + projectId: 'projectId', + serviceId: 'serviceId', + maxRetries: 3, + createTime: new Date('2024-02-27T12:28:09.000Z'), + headerPageNumbers: true, + retryCount: 3, + contentUrl: [ + 'https://developers.sinch.com/fax/fax.pdf', + ], + imageConversionMethod: 'HALFTONE', + hasFile: 'true', + }, + ]; + const expectedResponse = { + data: mockData, + hasNextPage: false, + nextPageValue: '', + nextPage: jest.fn(), + }; + + // When + fixture.list.mockResolvedValue(expectedResponse); + faxesApi.list = fixture.list; + const response = await faxesApi.list(requestData); + + // Then + expect(response).toEqual(expectedResponse); + expect(response.data).toBeDefined(); + expect(fixture.list).toHaveBeenCalledWith(requestData); + }); + }); + + describe ('sendFax', () => { + it('should make a POST request to create and send a fax', async () => { + // Given + const requestData: SendFaxRequestData = { + sendFaxRequestBody: { + to: '+12015555555', + }, + }; + const expectedResponse: Fax = { + id: 'fax_id', + direction: 'OUTBOUND', + to: '+12015555555', + status: 'IN_PROGRESS', + headerTimeZone: 'America/New_York', + retryDelaySeconds: 60, + callbackContentType: 'multipart/form-data', + projectId: 'projectId', + serviceId: 'serviceId', + maxRetries: 3, + createTime: new Date('2024-02-27T12:28:09Z'), + headerPageNumbers: true, + contentUrl: [ + 'https://developers.sinch.com/fax/fax.pdf', + ], + imageConversionMethod: 'HALFTONE', + }; + + // When + fixture.send.mockResolvedValue(expectedResponse); + faxesApi.send = fixture.send; + const response = await faxesApi.send(requestData); + + // Then + expect(response).toEqual(expectedResponse); + expect(fixture.send).toHaveBeenCalledWith(requestData); + }); + }); +}); diff --git a/packages/fax/tests/rest/v3/services/services-api.test.ts b/packages/fax/tests/rest/v3/services/services-api.test.ts new file mode 100644 index 00000000..f50cc75c --- /dev/null +++ b/packages/fax/tests/rest/v3/services/services-api.test.ts @@ -0,0 +1,274 @@ +import { SinchClientParameters } from '@sinch/sdk-client'; +import { + CreateServiceRequestData, + GetServiceRequestData, + ListNumbersForServiceRequestData, + ListServicesRequestData, + DeleteServiceRequestData, + ServicePhoneNumber, + ServiceResponse, + ServicesApi, + ServicesApiFixture, + UpdateServiceRequestData, + ListEmailsForNumberRequestData, +} from '../../../../src'; + +describe('ServicesApi', () => { + let servicesApi: ServicesApi; + let fixture: ServicesApiFixture; + let credentials: SinchClientParameters; + + beforeEach(() => { + fixture = new ServicesApiFixture(); + credentials = { + projectId: 'PROJECT_ID', + keyId: 'KEY_ID', + keySecret: 'KEY_SECRET', + }; + servicesApi = new ServicesApi(credentials); + }); + + + describe ('createService', () => { + it('should make a POST request to create a new service', async () => { + // Given + const requestData: CreateServiceRequestData = { + createServiceRequestBody: { + name: 'New service', + incomingWebhookUrl: 'https://yourserver/incomingFax', + webhookContentType: 'multipart/form-data', + defaultForProject: false, + defaultFrom: '+15551235656', + numberOfRetries: 3, + retryDelaySeconds: 60, + imageConversionMethod: 'HALFTONE', + saveOutboundFaxDocuments: true, + saveInboundFaxDocuments: true, + }, + }; + const expectedResponse: ServiceResponse = { + id: 'serviceId', + name: 'New service', + incomingWebhookUrl: 'https://yourserver/incomingFax', + webhookContentType: 'multipart/form-data', + defaultForProject: false, + defaultFrom: '+15551235656', + numberOfRetries: 3, + retryDelaySeconds: 60, + imageConversionMethod: 'HALFTONE', + saveOutboundFaxDocuments: true, + saveInboundFaxDocuments: true, + projectId: 'projectId', + }; + + // When + fixture.create.mockResolvedValue(expectedResponse); + servicesApi.create = fixture.create; + const response = await servicesApi.create(requestData); + + // Then + expect(response).toEqual(expectedResponse); + expect(fixture.create).toHaveBeenCalledWith(requestData); + }); + }); + + describe ('getService', () => { + it('should make a GET request to retrieve a service resource', async () => { + // Given + const requestData: GetServiceRequestData = { + serviceId: 'serviceId', + }; + const expectedResponse: ServiceResponse = { + id: 'serviceId', + name: 'New service', + incomingWebhookUrl: 'https://yourserver/incomingFax', + webhookContentType: 'multipart/form-data', + defaultForProject: false, + defaultFrom: '+15551235656', + numberOfRetries: 3, + retryDelaySeconds: 60, + imageConversionMethod: 'HALFTONE', + saveOutboundFaxDocuments: true, + saveInboundFaxDocuments: true, + projectId: 'projectId', + }; + + // When + fixture.get.mockResolvedValue(expectedResponse); + servicesApi.get = fixture.get; + const response = await servicesApi.get(requestData); + + // Then + expect(response).toEqual(expectedResponse); + expect(fixture.get).toHaveBeenCalledWith(requestData); + }); + }); + + describe ('listEmailsForNumber', () => { + it('should make a GET request to list any emails for a number', async () => { + // Given + const requestData: ListEmailsForNumberRequestData = { + serviceId: 'serviceId', + phoneNumber: '+15551235656', + }; + const mockData: string[] = [ + 'user@example.com', + ]; + const expectedResponse = { + data: mockData, + hasNextPage: false, + nextPageValue: '', + nextPage: jest.fn(), + }; + + // When + fixture.listEmailsForNumber.mockResolvedValue(expectedResponse); + servicesApi.listEmailsForNumber = fixture.listEmailsForNumber; + const response = await servicesApi.listEmailsForNumber(requestData); + + // Then + expect(response).toEqual(expectedResponse); + expect(response.data).toBeDefined(); + expect(fixture.listEmailsForNumber).toHaveBeenCalledWith(requestData); + }); + }); + + describe ('listNumbersForService', () => { + it('should make a GET request to list the numbers associated with a service', async () => { + // Given + const requestData: ListNumbersForServiceRequestData = { + serviceId: 'serviceId', + pageSize: 10, + page: '0', + }; + const mockData: ServicePhoneNumber[] = [ + { + serviceId: 'serviceId', + projectId: 'projectId', + phoneNumber: '+15551235656', + }, + ]; + const expectedResponse = { + data: mockData, + hasNextPage: false, + nextPageValue: '', + nextPage: jest.fn(), + }; + + // When + fixture.listNumbers.mockResolvedValue(expectedResponse); + servicesApi.listNumbers = fixture.listNumbers; + const response = await servicesApi.listNumbers(requestData); + + // Then + expect(response).toEqual(expectedResponse); + expect(response.data).toBeDefined(); + expect(fixture.listNumbers).toHaveBeenCalledWith(requestData); + }); + }); + + describe ('listServices', () => { + it('should make a GET request to list the services for a project', async () => { + // Given + const requestData: ListServicesRequestData = { + pageSize: 10, + page: '0', + }; + const mockData: ServiceResponse[] = [ + { + id: 'serviceId', + name: 'New service', + incomingWebhookUrl: 'https://yourserver/incomingFax', + webhookContentType: 'multipart/form-data', + defaultForProject: false, + defaultFrom: '+15551235656', + numberOfRetries: 3, + retryDelaySeconds: 60, + imageConversionMethod: 'HALFTONE', + saveOutboundFaxDocuments: true, + saveInboundFaxDocuments: true, + projectId: 'projectId', + }, + ]; + const expectedResponse = { + data: mockData, + hasNextPage: false, + nextPageValue: '', + nextPage: jest.fn(), + }; + + // When + fixture.list.mockResolvedValue(expectedResponse); + servicesApi.list = fixture.list; + const response = await servicesApi.list(requestData); + + // Then + expect(response).toEqual(expectedResponse); + expect(response.data).toBeDefined(); + expect(fixture.list).toHaveBeenCalledWith(requestData); + }); + }); + + describe ('removeService', () => { + it('should make a DELETE request to remove a service from a project', async () => { + // Given + const requestData: DeleteServiceRequestData = { + serviceId: 'serviceId', + }; + const expectedResponse: void = undefined; + + // When + fixture.delete.mockResolvedValue(expectedResponse); + servicesApi.delete = fixture.delete; + const response = await servicesApi.delete(requestData); + + // Then + expect(response).toEqual(expectedResponse); + expect(fixture.delete).toHaveBeenCalledWith(requestData); + }); + }); + + describe ('updateService', () => { + it('should make a PATCH request to update settings on a service', async () => { + // Given + const requestData: UpdateServiceRequestData = { + serviceId: 'serviceId', + updateServiceRequestBody: { + name: 'Updated service', + incomingWebhookUrl: 'https://your-new-server/incomingFax', + webhookContentType: 'application/json', + defaultForProject: true, + defaultFrom: '+15551234567', + numberOfRetries: 5, + retryDelaySeconds: 30, + imageConversionMethod: 'MONOCHROME', + saveOutboundFaxDocuments: false, + saveInboundFaxDocuments: false, + }, + }; + const expectedResponse: ServiceResponse = { + id: 'serviceId', + name: 'Updated service', + incomingWebhookUrl: 'https://your-new-server/incomingFax', + webhookContentType: 'application/json', + defaultForProject: true, + defaultFrom: '+15551234567', + numberOfRetries: 5, + retryDelaySeconds: 30, + imageConversionMethod: 'MONOCHROME', + saveOutboundFaxDocuments: false, + saveInboundFaxDocuments: false, + projectId: 'projectId', + }; + + // When + fixture.update.mockResolvedValue(expectedResponse); + servicesApi.update = fixture.update; + const response = await servicesApi.update(requestData); + + // Then + expect(response).toEqual(expectedResponse); + expect(fixture.update).toHaveBeenCalledWith(requestData); + }); + }); +}); diff --git a/packages/sdk-client/src/api/api-client.ts b/packages/sdk-client/src/api/api-client.ts index cef60263..5a34a176 100644 --- a/packages/sdk-client/src/api/api-client.ts +++ b/packages/sdk-client/src/api/api-client.ts @@ -1,11 +1,14 @@ import { RequestBody, RequestOptions } from '../plugins/core/request-plugin'; import { ApiClientOptions } from './api-client-options'; import { Headers } from 'node-fetch'; +import FormData = require('form-data'); +import { FileBuffer } from './api-interface'; export enum PaginationEnum { NONE, TOKEN, - PAGE + PAGE, + PAGE3 } export interface ApiListPromise extends Promise>, AsyncIterableIterator { } @@ -182,7 +185,7 @@ export class ApiClient { } /** - * Process HTTP call + * Process HTTP call with Pagination * @abstract * @template T * @param {ApiCallParametersWithPagination} _httpCallParameters - Parameters for the HTTP call. @@ -193,6 +196,29 @@ export class ApiClient { throw new Error('Abstract method must be implemented'); } + /** + * Process HTTP call to download a PDF file + * @abstract + * @template T + * @param {ApiCallParameters} _httpCallParameters - Parameters for the HTTP call. + * @return {Promise} A promise that resolves to the result of the HTTP call. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + processFileCall(_httpCallParameters: ApiCallParameters): Promise { + throw new Error('Abstract method must be implemented'); + } + + /** + * Receives an object containing key/value pairs + * Encodes this object to match application/x-www-urlencoded or multipart/form-data + * @abstract + * @param {any} _data + * @param {string} _type + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + processFormData(_data: any, _type: string): FormData | string { + throw new Error('Abstract method must be implemented'); + } } diff --git a/packages/sdk-client/src/api/api-interface.ts b/packages/sdk-client/src/api/api-interface.ts index 51d4370a..16968b99 100644 --- a/packages/sdk-client/src/api/api-interface.ts +++ b/packages/sdk-client/src/api/api-interface.ts @@ -7,3 +7,10 @@ export interface Api { /** API Client used to process the calls to the API */ client?: ApiClient; } + +export interface FileBuffer { + /** Name of the file extracted from the 'content-disposition' header */ + fileName: string; + /** File content as Buffer */ + buffer: Buffer; +} diff --git a/packages/sdk-client/src/client/api-client-helpers.ts b/packages/sdk-client/src/client/api-client-helpers.ts index a7adcdf7..5a11d2e5 100644 --- a/packages/sdk-client/src/client/api-client-helpers.ts +++ b/packages/sdk-client/src/client/api-client-helpers.ts @@ -91,5 +91,9 @@ export const reviveDates = (input: any): any => { }; const isDateString = (value: any): boolean => { - return typeof value === 'string' && !isNaN(Date.parse(value)); + if (typeof value === 'string' && value.length >= 10) { + const date = new Date(value); + return !isNaN(date.getTime()) && date.toISOString().slice(0, 10) === value.slice(0,10); + } + return false; }; diff --git a/packages/sdk-client/src/client/api-client-pagination-helper.ts b/packages/sdk-client/src/client/api-client-pagination-helper.ts index c61a296b..7e6d976f 100644 --- a/packages/sdk-client/src/client/api-client-pagination-helper.ts +++ b/packages/sdk-client/src/client/api-client-pagination-helper.ts @@ -1,5 +1,6 @@ import { - ApiCallParametersWithPagination, ApiClient, + ApiCallParametersWithPagination, + ApiClient, ApiListPromise, AutoPaginationMethods, PageResult, @@ -166,6 +167,9 @@ export function hasMore( const pageSize: number = requestedPageSize ? parseInt(requestedPageSize) : response.page_size; return checkIfThereAreMorePages(response, pageSize); } + if (context.pagination === PaginationEnum.PAGE3) { + return response.pageNumber < response.totalPages; + } throw new Error(`The operation ${context.operationId} is not meant to be paginated.`); } @@ -181,6 +185,11 @@ export function calculateNextPage( const nextPage = currentPage + 1; return nextPage.toString(); } + if (context.pagination === PaginationEnum.PAGE3) { + const currentPage: number = response.pageNumber || 1; + const nextPage = currentPage + 1; + return nextPage.toString(); + } throw new Error(`The operation ${context.operationId} is not meant to be paginated.`); } diff --git a/packages/sdk-client/src/client/api-fetch-client.ts b/packages/sdk-client/src/client/api-fetch-client.ts index af344cab..20f86787 100644 --- a/packages/sdk-client/src/client/api-fetch-client.ts +++ b/packages/sdk-client/src/client/api-fetch-client.ts @@ -9,9 +9,13 @@ import { ErrorContext, GenericError, ApiCallParameters, - ResponseJSONParseError, ApiCallParametersWithPagination, PageResult, + ResponseJSONParseError, + ApiCallParametersWithPagination, + PageResult, + FileBuffer, } from '../api'; import fetch, { Response, Headers } from 'node-fetch'; +import FormData = require('form-data'); import { buildErrorContext, manageExpiredToken, reviveDates } from './api-client-helpers'; import { buildPaginationContext, @@ -227,4 +231,75 @@ export class ApiFetchClient extends ApiClient { ) : []; } + + /** @inheritdoc */ + public async processFileCall( + props: ApiCallParameters, + ): Promise { + // Read the "Origin" header if existing, for logging purposes + const origin = (props.requestOptions.headers as Headers).get('Origin'); + const errorContext: ErrorContext = buildErrorContext(props, origin); + + // Declare variables + let response: Response | undefined; + let body: Buffer | undefined; + let fileName: string | undefined; + + // Execute call + try { + // Send the request with the refresh token mechanism + response = await this.sinchFetch(props, errorContext); + body = await response.buffer(); + fileName = this.extractFileName(response.headers); + } catch (error: any) { + this.buildFetchError(error, errorContext); + } + + if (!body || !fileName) { + throw new Error('An error occurred while downloading the file'); + } + + return { + fileName, + buffer: body, + }; + } + + private extractFileName(headers: Headers) { + const contentDisposition = headers.get('content-disposition'); + let fileName = 'default-name.pdf'; + if (contentDisposition) { + const match = contentDisposition.match(/filename="([^"]+)"/); + if (match && match[1]) { + fileName = match[1]; + } + } + return fileName; + } + + /** @inheritDoc */ + public processFormData(data: any, type: string): FormData | string { + + let encodedData: FormData | string; + + if (type === 'multipart/form-data') { + const formData: FormData = new FormData(); + for (const i in data) { + if (Object.prototype.hasOwnProperty.call(data, i)) { + formData.append(i, data[i]); + } + } + encodedData = formData; + } else { + const formData: string[] = []; + for (const i in data) { + if (Object.prototype.hasOwnProperty.call(data, i)) { + formData.push(`${i}=${encodeURIComponent(data[i])}`); + } + } + encodedData = formData.join('&'); + } + + return encodedData; + } } diff --git a/packages/sdk-client/tests/client/api-client-helpers.test.ts b/packages/sdk-client/tests/client/api-client-helpers.test.ts index 5c0e3dfe..4aac59a0 100644 --- a/packages/sdk-client/tests/client/api-client-helpers.test.ts +++ b/packages/sdk-client/tests/client/api-client-helpers.test.ts @@ -16,8 +16,11 @@ describe('API client helpers', () => { date3: '2024-02-04T20:22:00.123Z', }, ], - otherProp: 'otherValue', + unsupportedDate: '-0000', + otherProp: 'otherValueWithMoreThan10Characters', otherNumber: 0, + otherDecimalString: '0.07', + otherLongDecimalString: '1234567890', otherBoolean: true, otherUndefined: undefined, otherNull: null, @@ -35,8 +38,11 @@ describe('API client helpers', () => { date3: new Date('2024-02-04T20:22:00.123Z'), }, ], - otherProp: 'otherValue', + unsupportedDate: '-0000', + otherProp: 'otherValueWithMoreThan10Characters', otherNumber: 0, + otherDecimalString: '0.07', + otherLongDecimalString: '1234567890', otherBoolean: true, otherUndefined: undefined, otherNull: null, diff --git a/packages/sdk-core/package.json b/packages/sdk-core/package.json index 2527951f..fa748760 100644 --- a/packages/sdk-core/package.json +++ b/packages/sdk-core/package.json @@ -30,6 +30,7 @@ }, "dependencies": { "@sinch/conversation": "^0.0.2", + "@sinch/fax": "^0.0.2", "@sinch/numbers": "^0.0.2", "@sinch/sms": "^0.0.2", "@sinch/verification": "^0.0.2", diff --git a/packages/sdk-core/src/index.ts b/packages/sdk-core/src/index.ts index 3f6bd3a3..d40dd410 100644 --- a/packages/sdk-core/src/index.ts +++ b/packages/sdk-core/src/index.ts @@ -1,5 +1,6 @@ export * from './sinch-client'; export * from '@sinch/conversation'; +export * from '@sinch/fax'; export * from '@sinch/numbers'; export * from '@sinch/sms'; export * from '@sinch/verification'; diff --git a/packages/sdk-core/src/sinch-client.ts b/packages/sdk-core/src/sinch-client.ts index 97c23c5d..78aa50b2 100644 --- a/packages/sdk-core/src/sinch-client.ts +++ b/packages/sdk-core/src/sinch-client.ts @@ -1,4 +1,5 @@ import { ConversationService } from '@sinch/conversation'; +import { FaxService } from '@sinch/fax'; import { NumbersService } from '@sinch/numbers'; import { SmsService } from '@sinch/sms'; import { VerificationService } from '@sinch/verification'; @@ -9,6 +10,7 @@ import { SinchClientParameters } from '@sinch/sdk-client'; export class SinchClient { public readonly conversation: ConversationService; + public readonly fax: FaxService; public readonly numbers: NumbersService; public readonly sms: SmsService; public readonly verification: VerificationService; @@ -23,6 +25,9 @@ export class SinchClient { // Initialize the "Conversation" API this.conversation = new ConversationService(params); + // Initialize the "Fax" API + this.fax = new FaxService(params); + // Initialize the "Numbers" API this.numbers = new NumbersService(params); diff --git a/yarn.lock b/yarn.lock index 3b213fd6..59d4f424 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2034,7 +2034,7 @@ "@types/range-parser" "*" "@types/send" "*" -"@types/express@^4.17.17": +"@types/express@*", "@types/express@^4.17.17": version "4.17.21" resolved "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz" integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== @@ -2116,6 +2116,13 @@ resolved "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz" integrity sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag== +"@types/multer@^1.4.11": + version "1.4.11" + resolved "https://registry.yarnpkg.com/@types/multer/-/multer-1.4.11.tgz#c70792670513b4af1159a2b60bf48cc932af55c5" + integrity sha512-svK240gr6LVWvv3YGyhLlA+6LRRWA4mnGIU7RcNmgjBYFl6665wcXrRfxGp5tEPVHUNm5FMcmq7too9bxCwX/w== + dependencies: + "@types/express" "*" + "@types/node-fetch@^2.6.6": version "2.6.9" resolved "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.9.tgz"