diff --git a/docs/useCases.md b/docs/useCases.md index 2ccee9fc..aa6b56f5 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -85,6 +85,8 @@ The different use cases currently available in the package are classified below, - [Get Application Terms of Use](#get-application-terms-of-use) - [Contact](#Contact) - [Send Feedback to Object Contacts](#send-feedback-to-object-contacts) +- [Search](#Search) + - [Get Search Services](#get-search-services) ## Collections @@ -222,6 +224,8 @@ This use case supports the following optional parameters depending on the search - **limit**: (number) Limit for pagination. - **offset**: (number) Offset for pagination. - **collectionSearchCriteria**: ([CollectionSearchCriteria](../src/collections/domain/models/CollectionSearchCriteria.ts)) Supports filtering the collection items by different properties. +- **searchServiceName**: The search service name on which to execute the search (Optional). +- **showTypeCounts**: If true, the response will include the count per object type (Optional). #### List My Data Collection Items @@ -1991,3 +1995,25 @@ In ContactDTO, it takes the following information: - **subject**: the email subject line. - **body**: the email body to send. - **fromEmail**: the email to list in the reply-to field. + +## Search + +#### Get Search Services + +Returns all [Search Services](../src/search/domain/models/SearchService.ts) available in the installation. + +##### Example call: + +```typescript +import { getSearchServices } from '@iqss/dataverse-client-javascript' + +/* ... */ + +getSearchServices.execute().then((searchServices: SearchService[]) => { + /* ... */ +}) + +/* ... */ +``` + +_See [use case](../src/search/domain/useCases/GetSearchServices.ts) implementation_. diff --git a/src/collections/domain/repositories/ICollectionsRepository.ts b/src/collections/domain/repositories/ICollectionsRepository.ts index 6c78923e..a9ec708d 100644 --- a/src/collections/domain/repositories/ICollectionsRepository.ts +++ b/src/collections/domain/repositories/ICollectionsRepository.ts @@ -27,6 +27,7 @@ export interface ICollectionsRepository { limit?: number, offset?: number, collectionSearchCriteria?: CollectionSearchCriteria, + searchServiceName?: string, showTypeCounts?: boolean ): Promise getMyDataCollectionItems( diff --git a/src/collections/domain/useCases/GetCollectionItems.ts b/src/collections/domain/useCases/GetCollectionItems.ts index 1c85b202..2d8cd10b 100644 --- a/src/collections/domain/useCases/GetCollectionItems.ts +++ b/src/collections/domain/useCases/GetCollectionItems.ts @@ -18,6 +18,7 @@ export class GetCollectionItems implements UseCase { * @param {number} [limit] - Limit for pagination (optional). * @param {number} [offset] - Offset for pagination (optional). * @param {CollectionSearchCriteria} [collectionSearchCriteria] - Supports filtering the collection items by different properties (optional). + * @param {string} [searchServiceName] - The search service name on which to execute the search (optional). * @param {boolean} [showTypeCounts] - If true, the response will include the count per object type (optional). * @returns {Promise} */ @@ -26,6 +27,7 @@ export class GetCollectionItems implements UseCase { limit?: number, offset?: number, collectionSearchCriteria?: CollectionSearchCriteria, + searchServiceName?: string, showTypeCounts = false ): Promise { return await this.collectionsRepository.getCollectionItems( @@ -33,6 +35,7 @@ export class GetCollectionItems implements UseCase { limit, offset, collectionSearchCriteria, + searchServiceName, showTypeCounts ) } diff --git a/src/collections/infra/repositories/CollectionsRepository.ts b/src/collections/infra/repositories/CollectionsRepository.ts index fa0f9872..b200fa37 100644 --- a/src/collections/infra/repositories/CollectionsRepository.ts +++ b/src/collections/infra/repositories/CollectionsRepository.ts @@ -75,7 +75,8 @@ export enum GetCollectionItemsQueryParams { START = 'start', TYPE = 'type', FILTERQUERY = 'fq', - SHOW_TYPE_COUNTS = 'show_type_counts' + SHOW_TYPE_COUNTS = 'show_type_counts', + SEARCH_SERVICE_NAME = 'search_service' } export enum GetMyDataCollectionItemsQueryParams { @@ -168,6 +169,7 @@ export class CollectionsRepository extends ApiRepository implements ICollections limit?: number, offset?: number, collectionSearchCriteria?: CollectionSearchCriteria, + searchServiceName?: string, showTypeCounts?: boolean ): Promise { const queryParams = new URLSearchParams({ @@ -193,6 +195,10 @@ export class CollectionsRepository extends ApiRepository implements ICollections queryParams.set(GetCollectionItemsQueryParams.SHOW_TYPE_COUNTS, 'true') } + if (searchServiceName) { + queryParams.set(GetCollectionItemsQueryParams.SEARCH_SERVICE_NAME, searchServiceName) + } + if (collectionSearchCriteria) { this.applyCollectionSearchCriteriaToQueryParams(queryParams, collectionSearchCriteria) } diff --git a/src/index.ts b/src/index.ts index 89a79af6..2fb70d9e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,3 +8,4 @@ export * from './collections' export * from './metadataBlocks' export * from './files' export * from './contactInfo' +export * from './search' diff --git a/src/search/domain/models/SearchService.ts b/src/search/domain/models/SearchService.ts new file mode 100644 index 00000000..b895878e --- /dev/null +++ b/src/search/domain/models/SearchService.ts @@ -0,0 +1,4 @@ +export interface SearchService { + name: string + displayName: string +} diff --git a/src/search/domain/repositories/ISearchServicesRepository.ts b/src/search/domain/repositories/ISearchServicesRepository.ts new file mode 100644 index 00000000..f41a477a --- /dev/null +++ b/src/search/domain/repositories/ISearchServicesRepository.ts @@ -0,0 +1,5 @@ +import { SearchService } from '../models/SearchService' + +export interface ISearchServicesRepository { + getSearchServices(): Promise +} diff --git a/src/search/domain/useCases/GetSearchServices.ts b/src/search/domain/useCases/GetSearchServices.ts new file mode 100644 index 00000000..9fa0a0c0 --- /dev/null +++ b/src/search/domain/useCases/GetSearchServices.ts @@ -0,0 +1,20 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { SearchService } from '../models/SearchService' +import { ISearchServicesRepository } from '../repositories/ISearchServicesRepository' + +export class GetSearchServices implements UseCase { + private searchServicesRepository: ISearchServicesRepository + + constructor(searchServicesRepository: ISearchServicesRepository) { + this.searchServicesRepository = searchServicesRepository + } + + /** + * Returns all search services available in the installation. + * + * @returns {Promise} + */ + async execute(): Promise { + return await this.searchServicesRepository.getSearchServices() + } +} diff --git a/src/search/index.ts b/src/search/index.ts new file mode 100644 index 00000000..56735ae0 --- /dev/null +++ b/src/search/index.ts @@ -0,0 +1,10 @@ +import { GetSearchServices } from './domain/useCases/GetSearchServices' +import { SearchServicesRepository } from './infra/repositories/SearchServicesRepository' + +const searchServicesRepository = new SearchServicesRepository() + +const getSearchServices = new GetSearchServices(searchServicesRepository) + +export { getSearchServices } + +export { SearchService } from './domain/models/SearchService' diff --git a/src/search/infra/repositories/SearchServicesRepository.ts b/src/search/infra/repositories/SearchServicesRepository.ts new file mode 100644 index 00000000..343fb686 --- /dev/null +++ b/src/search/infra/repositories/SearchServicesRepository.ts @@ -0,0 +1,14 @@ +import { ApiRepository } from '../../../core/infra/repositories/ApiRepository' +import { SearchService } from '../../domain/models/SearchService' +import { ISearchServicesRepository } from '../../domain/repositories/ISearchServicesRepository' +import { transformSearchServicesResponseToSearchServices } from './transformers/searchServiceTransformers' + +export class SearchServicesRepository extends ApiRepository implements ISearchServicesRepository { + public async getSearchServices(): Promise { + return this.doGet(`/search/services`) + .then((response) => transformSearchServicesResponseToSearchServices(response)) + .catch((error) => { + throw error + }) + } +} diff --git a/src/search/infra/repositories/transformers/SearchServicePayload.ts b/src/search/infra/repositories/transformers/SearchServicePayload.ts new file mode 100644 index 00000000..b5b45fc4 --- /dev/null +++ b/src/search/infra/repositories/transformers/SearchServicePayload.ts @@ -0,0 +1,4 @@ +export interface SearchServicePayload { + name: string + displayName: string +} diff --git a/src/search/infra/repositories/transformers/searchServiceTransformers.ts b/src/search/infra/repositories/transformers/searchServiceTransformers.ts new file mode 100644 index 00000000..2d379e7c --- /dev/null +++ b/src/search/infra/repositories/transformers/searchServiceTransformers.ts @@ -0,0 +1,24 @@ +import { AxiosResponse } from 'axios' +import { SearchService } from '../../../domain/models/SearchService' +import { SearchServicePayload } from './SearchServicePayload' + +export const transformSearchServicesResponseToSearchServices = ( + response: AxiosResponse +): SearchService[] => { + const searchServicesPayload = response.data.data.services + const searchServices: SearchService[] = [] + searchServicesPayload.forEach(function (searchServicePayload: SearchServicePayload) { + searchServices.push(transformSearchServicePayloadToSearchService(searchServicePayload)) + }) + + return searchServices +} + +const transformSearchServicePayloadToSearchService = ( + searchServicePayload: SearchServicePayload +): SearchService => { + return { + name: searchServicePayload.name, + displayName: searchServicePayload.displayName + } +} diff --git a/test/functional/search/GetSearchServices.test.ts b/test/functional/search/GetSearchServices.test.ts new file mode 100644 index 00000000..af70aae9 --- /dev/null +++ b/test/functional/search/GetSearchServices.test.ts @@ -0,0 +1,23 @@ +import { ApiConfig, getSearchServices } from '../../../src' +import { TestConstants } from '../../testHelpers/TestConstants' +import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' +import { SearchService } from '../../../src/search/domain/models/SearchService' + +describe('execute', () => { + beforeEach(async () => { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + }) + + test('should successfully return search services', async () => { + const searchServices: SearchService[] = await getSearchServices.execute() + + expect(searchServices).toBeDefined() + expect(searchServices.length).toBe(1) + expect(searchServices[0].name).toBe('solr') + expect(searchServices[0].displayName).toBe('Dataverse Standard Search') + }) +}) diff --git a/test/integration/collections/CollectionsRepository.test.ts b/test/integration/collections/CollectionsRepository.test.ts index d869834b..b215f681 100644 --- a/test/integration/collections/CollectionsRepository.test.ts +++ b/test/integration/collections/CollectionsRepository.test.ts @@ -784,6 +784,7 @@ describe('CollectionsRepository', () => { undefined, undefined, undefined, + undefined, true ) expect(actual.countPerObjectType?.collections).toBe(1) diff --git a/test/integration/search/SearchServicesRepository.test.ts b/test/integration/search/SearchServicesRepository.test.ts new file mode 100644 index 00000000..198eb0b3 --- /dev/null +++ b/test/integration/search/SearchServicesRepository.test.ts @@ -0,0 +1,25 @@ +import { ApiConfig } from '../../../src' +import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' +import { SearchServicesRepository } from '../../../src/search/infra/repositories/SearchServicesRepository' +import { TestConstants } from '../../testHelpers/TestConstants' + +describe('SearchServicesRepository', () => { + const sut: SearchServicesRepository = new SearchServicesRepository() + + beforeAll(async () => { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + }) + + describe('getSearchServices', () => { + test('should return search services', async () => { + const actual = await sut.getSearchServices() + expect(actual.length).toEqual(1) + expect(actual[0].name).toEqual('solr') + expect(actual[0].displayName).toEqual('Dataverse Standard Search') + }) + }) +}) diff --git a/test/testHelpers/search/searchServiceHelper.ts b/test/testHelpers/search/searchServiceHelper.ts new file mode 100644 index 00000000..afa55193 --- /dev/null +++ b/test/testHelpers/search/searchServiceHelper.ts @@ -0,0 +1,8 @@ +import { SearchService } from '../../../src/search/domain/models/SearchService' + +export const createSearchServiceModelArray = (count: number): SearchService[] => { + return Array.from({ length: count }, (_, index) => ({ + name: `role${index + 1}`, + displayName: `Role ${index + 1}` + })) +} diff --git a/test/unit/collections/GetCollectionItems.test.ts b/test/unit/collections/GetCollectionItems.test.ts index 33c8ecbc..79159da7 100644 --- a/test/unit/collections/GetCollectionItems.test.ts +++ b/test/unit/collections/GetCollectionItems.test.ts @@ -55,6 +55,7 @@ describe('execute', () => { undefined, undefined, undefined, + undefined, false ) expect(actual).toEqual(testItemSubset) @@ -71,6 +72,7 @@ describe('execute', () => { limit, undefined, undefined, + undefined, false ) expect(actual).toEqual(testItemSubset) @@ -87,6 +89,7 @@ describe('execute', () => { undefined, offset, undefined, + undefined, false ) expect(actual).toEqual(testItemSubset) @@ -103,7 +106,7 @@ describe('execute', () => { undefined, undefined, searchCriteria, - false + undefined ) expect(collectionRepositoryStub.getCollectionItems).toHaveBeenCalledWith( @@ -111,6 +114,7 @@ describe('execute', () => { undefined, undefined, searchCriteria, + undefined, false ) expect(actual).toEqual(testItemSubset) @@ -136,6 +140,7 @@ describe('execute', () => { undefined, undefined, undefined, + undefined, showTypeCounts ) @@ -144,6 +149,7 @@ describe('execute', () => { undefined, undefined, undefined, + undefined, showTypeCounts ) expect(actual).toEqual(testItemSubsetWithCount) @@ -164,6 +170,7 @@ describe('execute', () => { limit, offset, searchCriteria, + undefined, false ) @@ -172,6 +179,7 @@ describe('execute', () => { limit, offset, searchCriteria, + undefined, false ) expect(actual).toEqual(testItemSubset) diff --git a/test/unit/search/GetSearchServices.test.ts b/test/unit/search/GetSearchServices.test.ts new file mode 100644 index 00000000..ea3e9a1b --- /dev/null +++ b/test/unit/search/GetSearchServices.test.ts @@ -0,0 +1,25 @@ +import { ReadError } from '../../../src' +import { ISearchServicesRepository } from '../../../src/search/domain/repositories/ISearchServicesRepository' +import { GetSearchServices } from '../../../src/search/domain/useCases/GetSearchServices' +import { createSearchServiceModelArray } from '../../testHelpers/search/searchServiceHelper' + +describe('execute', () => { + test('should return search services array on repository success', async () => { + const searchServicesRepositoryStub: ISearchServicesRepository = {} as ISearchServicesRepository + const testServices = createSearchServiceModelArray(5) + searchServicesRepositoryStub.getSearchServices = jest.fn().mockResolvedValue(testServices) + const sut = new GetSearchServices(searchServicesRepositoryStub) + + const actual = await sut.execute() + + expect(actual).toEqual(testServices) + }) + + test('should return error result on repository error', async () => { + const searchServicesRepositoryStub: ISearchServicesRepository = {} as ISearchServicesRepository + searchServicesRepositoryStub.getSearchServices = jest.fn().mockRejectedValue(new ReadError()) + const sut = new GetSearchServices(searchServicesRepositoryStub) + + await expect(sut.execute()).rejects.toThrow(ReadError) + }) +})