diff --git a/docs/useCases.md b/docs/useCases.md index 6faa4ca7..8384f3b0 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -64,6 +64,8 @@ The different use cases currently available in the package are classified below, - [Get Dataverse Backend Version](#get-dataverse-backend-version) - [Get Maximum Embargo Duration In Months](#get-maximum-embargo-duration-in-months) - [Get ZIP Download Limit](#get-zip-download-limit) +- [Contact](#Contact) + - [Send Feedback to Object Contacts](#send-feedback-to-object-contacts) ## Collections @@ -1392,3 +1394,40 @@ getZipDownloadLimit.execute().then((downloadLimit: number) => { ``` _See [use case](../src/info/domain/useCases/GetZipDownloadLimit.ts) implementation_. + +## Contact + +#### Send Feedback to Object Contacts + +Returns a [Contact](../src/contactInfo/domain/models/Contact.ts) object, which contains contact return information, showing fromEmail, subject, body. + +##### Example call: + +```typescript +import { submitContactInfo } from '@iqss/dataverse-client-javascript' + +/* ... */ + +const contactDTO: ContactDTO = { + targedId: 1 + subject: 'Data Question', + body: 'Please help me understand your data. Thank you!', + fromEmail: 'test@gmail.com' +} + +submitContactInfo.execute(contactDTO) + +/* ... */ +``` + +_See [use case](../src/info/domain/useCases/submitContactInfo.ts) implementation_. + +The above example would submit feedback to all contacts of a object where the object targetId = 1. + +In ContactDTO, it takes the following information: + +- **targetId**: the numeric identifier of the collection, dataset, or datafile. Persistent ids and collection aliases are not supported. (Optional) +- **identifier**: the alias of a collection or the persistence id of a dataset or datafile. (Optional) +- **subject**: the email subject line. +- **body**: the email body to send. +- **fromEmail**: the email to list in the reply-to field. diff --git a/src/contactInfo/domain/dtos/ContactDTO.ts b/src/contactInfo/domain/dtos/ContactDTO.ts new file mode 100644 index 00000000..4fa956b9 --- /dev/null +++ b/src/contactInfo/domain/dtos/ContactDTO.ts @@ -0,0 +1,7 @@ +export interface ContactDTO { + targetId?: number + identifier?: string + subject: string + body: string + fromEmail: string +} diff --git a/src/contactInfo/domain/models/Contact.ts b/src/contactInfo/domain/models/Contact.ts new file mode 100644 index 00000000..e4f54897 --- /dev/null +++ b/src/contactInfo/domain/models/Contact.ts @@ -0,0 +1,5 @@ +export interface Contact { + fromEmail: string + body: string + subject: string +} diff --git a/src/contactInfo/domain/repositories/IContactRepository.ts b/src/contactInfo/domain/repositories/IContactRepository.ts new file mode 100644 index 00000000..c56d2bbc --- /dev/null +++ b/src/contactInfo/domain/repositories/IContactRepository.ts @@ -0,0 +1,6 @@ +import { Contact } from '../models/Contact' +import { ContactDTO } from '../dtos/ContactDTO' + +export interface IContactRepository { + submitContactInfo(contactDTO: ContactDTO): Promise +} diff --git a/src/contactInfo/domain/useCases/SubmitContactInfo.ts b/src/contactInfo/domain/useCases/SubmitContactInfo.ts new file mode 100644 index 00000000..c55771b8 --- /dev/null +++ b/src/contactInfo/domain/useCases/SubmitContactInfo.ts @@ -0,0 +1,23 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { ContactDTO } from '../dtos/ContactDTO' +import { Contact } from '../models/Contact' +import { IContactRepository } from '../repositories/IContactRepository' + +export class SubmitContactInfo implements UseCase { + private contactRepository: IContactRepository + + constructor(contactRepository: IContactRepository) { + this.contactRepository = contactRepository + } + + /** + * Submits contact information and returns a Contact model containing the submitted data. + * + * @param {ContactDTO} contactDTO - The contact information to be submitted. + * @returns {Promise} A promise resolving to a list of contact. + */ + + async execute(contactDTO: ContactDTO): Promise { + return await this.contactRepository.submitContactInfo(contactDTO) + } +} diff --git a/src/contactInfo/index.ts b/src/contactInfo/index.ts new file mode 100644 index 00000000..a34eb082 --- /dev/null +++ b/src/contactInfo/index.ts @@ -0,0 +1,9 @@ +import { SubmitContactInfo } from './domain/useCases/SubmitContactInfo' +import { ContactRepository } from './infra/repositories/ContactRepository' + +const contactRepository = new ContactRepository() +const submitContactInfo = new SubmitContactInfo(contactRepository) + +export { submitContactInfo } +export { Contact } from './domain/models/Contact' +export { ContactDTO } from './domain/dtos/ContactDTO' diff --git a/src/contactInfo/infra/repositories/ContactRepository.ts b/src/contactInfo/infra/repositories/ContactRepository.ts new file mode 100644 index 00000000..ba330872 --- /dev/null +++ b/src/contactInfo/infra/repositories/ContactRepository.ts @@ -0,0 +1,23 @@ +import { ApiRepository } from '../../../core/infra/repositories/ApiRepository' +import { Contact } from '../../domain/models/Contact' +import { IContactRepository } from '../../domain/repositories/IContactRepository' +import { ContactDTO } from '../../domain/dtos/ContactDTO' + +export class ContactRepository extends ApiRepository implements IContactRepository { + public async submitContactInfo(contactDTO: ContactDTO): Promise { + return this.doPost(`/sendfeedback`, contactDTO) + .then((response) => { + const responseData = response.data + const contact: Contact[] = responseData.data.map((item: Contact) => ({ + fromEmail: item.fromEmail, + subject: item.subject, + body: item.body + })) + + return contact + }) + .catch((error) => { + throw error + }) + } +} diff --git a/src/index.ts b/src/index.ts index 63562fda..2abdf089 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,3 +6,4 @@ export * from './datasets' export * from './collections' export * from './metadataBlocks' export * from './files' +export * from './contactInfo' diff --git a/test/functional/contact/SubmitContactInfo.test.ts b/test/functional/contact/SubmitContactInfo.test.ts new file mode 100644 index 00000000..78baade8 --- /dev/null +++ b/test/functional/contact/SubmitContactInfo.test.ts @@ -0,0 +1,80 @@ +import { ApiConfig, submitContactInfo, ContactDTO, WriteError, Contact } from '../../../src' +import { TestConstants } from '../../testHelpers/TestConstants' +import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' + +describe('submitContactInfo', () => { + beforeAll(async () => { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + }) + + test('should return Contact result successfully with targetId', async () => { + const contactDTO: ContactDTO = { + targetId: 1, + subject: 'Data Question', + body: 'Please help me understand your data. Thank you!', + fromEmail: 'example@dataverse.org' + } + + let contactInfo: Contact[] = [] + try { + contactInfo = await submitContactInfo.execute(contactDTO) + } catch (error) { + throw new Error('Contact info should be submitted') + } finally { + expect(contactInfo).toBeDefined() + expect(contactInfo[0].fromEmail).toEqual('example@dataverse.org') + expect(contactInfo[0].subject).toEqual(expect.any(String)) + expect(contactInfo[0].body).toEqual(expect.any(String)) + } + }) + + test('should return a Contact when targetId is not provided', async () => { + const contactDTO: ContactDTO = { + subject: 'General Inquiry', + body: 'I have a general question.', + fromEmail: 'example@dataverse.org' + } + + let contactInfo: Contact[] = [] + try { + contactInfo = await submitContactInfo.execute(contactDTO) + } catch (error) { + throw new Error('Contact info should be submitted even if target id is missing') + } finally { + expect(contactInfo).toBeDefined() + expect(contactInfo[0].fromEmail).toEqual('example@dataverse.org') + expect(contactInfo[0].subject).toEqual(expect.any(String)) + expect(contactInfo[0].body).toEqual(expect.any(String)) + } + }) + + test('should return Contact when contact info is successfully submitted with identifier', async () => { + const test2ContactDTO: ContactDTO = { + identifier: 'root', + subject: 'Data Question', + body: 'Please help me understand your data. Thank you!', + fromEmail: 'example@dataverse.org' + } + const contactInfo = await submitContactInfo.execute(test2ContactDTO) + + expect(contactInfo).toBeDefined() + expect(contactInfo[0].fromEmail).toEqual(test2ContactDTO.fromEmail) + expect(contactInfo[0].subject).toEqual(expect.any(String)) + expect(contactInfo[0].body).toEqual(expect.any(String)) + }) + + test('should return error if the target id is unexisted', async () => { + const contactDTO: ContactDTO = { + targetId: 0, + subject: '', + body: '', + fromEmail: 'example@dataverse.org' + } + const expectedError = new WriteError(`[400] Feedback target object not found.`) + await expect(submitContactInfo.execute(contactDTO)).rejects.toThrow(expectedError) + }) +}) diff --git a/test/functional/metadataBlocks/GetAllFacetableMetadataFields.test.ts b/test/functional/metadataBlocks/GetAllFacetableMetadataFields.test.ts index 7b290672..44dc67f9 100644 --- a/test/functional/metadataBlocks/GetAllFacetableMetadataFields.test.ts +++ b/test/functional/metadataBlocks/GetAllFacetableMetadataFields.test.ts @@ -12,15 +12,15 @@ describe('execute', () => { }) test('should return all facetable metadata fields', async () => { - let metadataFieldInfos: MetadataFieldInfo[] = null + let metadataFieldInfos: MetadataFieldInfo[] | null = null try { metadataFieldInfos = await getAllFacetableMetadataFields.execute() } catch (error) { throw new Error('Should not raise an error') } finally { - expect(metadataFieldInfos.length).toBe(59) - expect(metadataFieldInfos[0].name).toBe('authorName') - expect(metadataFieldInfos[0].displayName).toBe('Author Name') + expect(metadataFieldInfos?.length).toBe(64) + expect(metadataFieldInfos?.[0].name).toBe('authorName') + expect(metadataFieldInfos?.[0].displayName).toBe('Author Name') } }) }) diff --git a/test/functional/metadataBlocks/GetAllMetadataBlocks.test.ts b/test/functional/metadataBlocks/GetAllMetadataBlocks.test.ts index 27c4ce7f..8b72a9d3 100644 --- a/test/functional/metadataBlocks/GetAllMetadataBlocks.test.ts +++ b/test/functional/metadataBlocks/GetAllMetadataBlocks.test.ts @@ -12,15 +12,15 @@ describe('execute', () => { }) test('should successfully return metadatablocks', async () => { - let metadataBlocks: MetadataBlock[] = null + let metadataBlocks: MetadataBlock[] | null = null try { metadataBlocks = await getAllMetadataBlocks.execute() } catch (error) { throw new Error('Should not raise an error') } finally { expect(metadataBlocks).not.toBeNull() - expect(metadataBlocks.length).toBe(6) - expect(metadataBlocks[0].metadataFields.title.name).toBe('title') + expect(metadataBlocks?.length).toBe(7) + expect(metadataBlocks?.[0].metadataFields.title.name).toBe('title') } }) }) diff --git a/test/integration/contact/ContactRepository.test.ts b/test/integration/contact/ContactRepository.test.ts new file mode 100644 index 00000000..fa5883ad --- /dev/null +++ b/test/integration/contact/ContactRepository.test.ts @@ -0,0 +1,76 @@ +import { ContactRepository } from '../../../src/contactInfo/infra/repositories/ContactRepository' +import { ApiConfig, Contact, ContactDTO, WriteError } from '../../../src' +import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' +import { TestConstants } from '../../testHelpers/TestConstants' + +describe('submitContactInfo', () => { + beforeAll(async () => { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + }) + + const testContactDTO: ContactDTO = { + targetId: 1, + subject: 'Data Question', + body: 'Please help me understand your data. Thank you!', + fromEmail: 'example@dataverse.org' + } + + const test2ContactDTO: ContactDTO = { + identifier: 'root', + subject: 'Data Question', + body: 'Please help me understand your data. Thank you!', + fromEmail: 'example@dataverse.org' + } + + const sut: ContactRepository = new ContactRepository() + + test('should return Contact when contact info is successfully submitted', async () => { + const contactInfo = await sut.submitContactInfo(test2ContactDTO) + + expect(contactInfo).toBeDefined() + expect(contactInfo[0].fromEmail).toEqual(testContactDTO.fromEmail) + expect(contactInfo[0].subject).toEqual(expect.any(String)) + expect(contactInfo[0].body).toEqual(expect.any(String)) + }) + + test('should return Contact when contact info is successfully submitted if accessed by identifier', async () => { + const contactInfo = await sut.submitContactInfo(testContactDTO) + + expect(contactInfo).toBeDefined() + expect(contactInfo[0].fromEmail).toEqual(test2ContactDTO.fromEmail) + expect(contactInfo[0].subject).toEqual(expect.any(String)) + expect(contactInfo[0].body).toEqual(expect.any(String)) + }) + + test('should return a Contact when targetId is not provided', async () => { + const contactDTOWithoutTargetId: Partial = { + subject: 'General Inquiry', + body: 'I have a general question.', + fromEmail: 'example@dataverse.org' + } + + const contactInfo: Contact[] = await sut.submitContactInfo( + contactDTOWithoutTargetId as ContactDTO + ) + + expect(contactInfo).toBeDefined() + expect(contactInfo[0].fromEmail).toEqual(contactDTOWithoutTargetId.fromEmail) + expect(contactInfo[0].subject).toEqual(expect.any(String)) + expect(contactInfo[0].body).toEqual(expect.any(String)) + }) + + test('should return error if the target id is unexisted', async () => { + const invalidContactDTO: ContactDTO = { + targetId: 0, + subject: '', + body: '', + fromEmail: '' + } + const expectedError = new WriteError(`[400] Feedback target object not found.`) + await expect(sut.submitContactInfo(invalidContactDTO)).rejects.toThrow(expectedError) + }) +}) diff --git a/test/integration/metadataBlocks/MetadataBlocksRepository.test.ts b/test/integration/metadataBlocks/MetadataBlocksRepository.test.ts index 3cf9a002..0adab583 100644 --- a/test/integration/metadataBlocks/MetadataBlocksRepository.test.ts +++ b/test/integration/metadataBlocks/MetadataBlocksRepository.test.ts @@ -59,7 +59,7 @@ describe('MetadataBlocksRepository', () => { describe('getAllMetadataBlocks', () => { test('should return all metadata blocks', async () => { const actual = await sut.getAllMetadataBlocks() - expect(actual.length).toBe(6) + expect(actual.length).toBe(7) expect(actual[0].name).toBe(citationMetadataBlockName) expect(actual[0].metadataFields.title.name).toBe('title') }) diff --git a/test/integration/metadataBlocks/MetadataFieldsInfoRepository.test.ts b/test/integration/metadataBlocks/MetadataFieldsInfoRepository.test.ts index ffd2e394..de94eb7c 100644 --- a/test/integration/metadataBlocks/MetadataFieldsInfoRepository.test.ts +++ b/test/integration/metadataBlocks/MetadataFieldsInfoRepository.test.ts @@ -19,7 +19,7 @@ describe('getAllFacetableMetadataFields', () => { test('should return all facetable metadata fields', async () => { const actual = await sut.getAllFacetableMetadataFields() - expect(actual.length).toBe(59) + expect(actual.length).toBe(64) expect(actual[0].name).toBe('authorName') expect(actual[0].displayName).toBe('Author Name') }) diff --git a/test/unit/contact/SubmitContactInfo.test.ts b/test/unit/contact/SubmitContactInfo.test.ts new file mode 100644 index 00000000..821bf9bb --- /dev/null +++ b/test/unit/contact/SubmitContactInfo.test.ts @@ -0,0 +1,103 @@ +import { WriteError, Contact, ContactDTO } from '../../../src' +import { SubmitContactInfo } from '../../../src/contactInfo/domain/useCases/SubmitContactInfo' +import { IContactRepository } from '../../../src/contactInfo/domain/repositories/IContactRepository' +import { TestConstants } from '../../testHelpers/TestConstants' + +describe('execute submit information to contacts', () => { + test('should return a Contact when repository call is successful', async () => { + const fromEmail = 'example@dataverse.org' + + const contactDTO: ContactDTO = { + targetId: 6, + subject: 'Data Question', + body: 'Please help me understand your data. Thank you!', + fromEmail: fromEmail + } + + const collectionAlias = 'collection-1' + const baseUrl = TestConstants.TEST_API_URL + '/dataverse/' + const bodyMessage = + 'You have just been sent the following message from ' + + fromEmail + + ' via the Root hosted dataverse named "' + + collectionAlias + + '":\n' + + '\n' + + '---\n' + + '\n' + + 'Please help me understand your data. Thank you!\n' + + '\n' + + '---\n' + + '\n' + + 'Root Support\n' + + 'null\n' + + '\n' + + 'Go to dataverse ' + + baseUrl + + collectionAlias + + '\n' + + '\n' + + 'You received this email because you have been listed as a contact for the dataverse. If you believe this was an error, please contact Root Support at null. To respond directly to the individual who sent the message, simply reply to this email.' + + const expectedResponse: Contact[] = [ + { + fromEmail: contactDTO.fromEmail, + subject: 'Root contact: ' + contactDTO.subject, + body: bodyMessage + } + ] + + const contactRepositoryStub = {} + + contactRepositoryStub.submitContactInfo = jest.fn().mockResolvedValue(expectedResponse) + const sut = new SubmitContactInfo(contactRepositoryStub) + const actual = await sut.execute(contactDTO) + expect(actual).toEqual(expectedResponse) + expect(contactRepositoryStub.submitContactInfo).toHaveBeenCalledWith(contactDTO) + }) + + test('should return a Contact when targetId is not provided', async () => { + const fromEmail = 'test@gmail.com' + + const contactDTO: ContactDTO = { + subject: 'Data Question', + body: 'Please help me understand your data. Thank you!', + fromEmail: fromEmail + } + + const bodyMessage = + 'Root Support,\n\nThe following message was sent from ' + + fromEmail + + '.\n\n---\n\nPlease help me understand your data. Thank you!\n\n---\n\nMessage sent from Support contact form.' + const expectedResponse: Contact[] = [ + { + fromEmail: contactDTO.fromEmail, + subject: 'Root Support Request: ' + contactDTO.subject, + body: bodyMessage + } + ] + + const contactRepositoryStub = {} + + contactRepositoryStub.submitContactInfo = jest.fn().mockResolvedValue(expectedResponse) + const sut = new SubmitContactInfo(contactRepositoryStub) + const actual = await sut.execute(contactDTO) + expect(actual).toEqual(expectedResponse) + expect(contactRepositoryStub.submitContactInfo).toHaveBeenCalledWith(contactDTO) + }) + + test('should return error result once there is a invalid targetId', async () => { + const contactDTO: ContactDTO = { + targetId: 0, + subject: '', + body: '', + fromEmail: '' + } + const contactRepositoryStub = {} + const error = new WriteError(`[400] Feedback target object not found.`) + contactRepositoryStub.submitContactInfo = jest.fn().mockRejectedValue(error) + const sut = new SubmitContactInfo(contactRepositoryStub) + await expect(sut.execute(contactDTO)).rejects.toThrow(error) + expect(contactRepositoryStub.submitContactInfo).toHaveBeenCalledWith(contactDTO) + }) +})