diff --git a/docs/useCases.md b/docs/useCases.md index e8d7be92..2b2af4a9 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -14,6 +14,7 @@ The different use cases currently available in the package are classified below, - [Get Collection Facets](#get-collection-facets) - [Get User Permissions on a Collection](#get-user-permissions-on-a-collection) - [List All Collection Items](#list-all-collection-items) + - [List My Data Collection Items](#list-my-data-collection-items) - [Get Collection Featured Items](#get-collection-featured-items) - [Collections write use cases](#collections-write-use-cases) - [Create a Collection](#create-a-collection) @@ -215,6 +216,62 @@ This use case supports the following optional parameters depending on the search - **offset**: (number) Offset for pagination. - **collectionSearchCriteria**: ([CollectionSearchCriteria](../src/collections/domain/models/CollectionSearchCriteria.ts)) Supports filtering the collection items by different properties. +#### List My Data Collection Items + +Returns an instance of [CollectionItemSubset](../src/collections/domain/models/CollectionItemSubset.ts) that contains reduced information for each collection item for which the calling user has a role. + +##### Example call: + +```typescript +import { getMyDataCollectionItems } from '@iqss/dataverse-client-javascript' + +/* ... */ + +const roleIds = [1, 2] +const collectionItemTypes = [CollectionItemType.DATASET, CollectionItemType.FILE] +const publishingStatuses = [ + PublicationStatus.Published, + PublicationStatus.Draft, + PublicationStatus.Unpublished +] +const limit = 10 +const selectedPage = 1 +const searchText = 'search text' +const otherUserName = 'otherUserName' + +getMyDataCollectionItems + .execute( + roleIds, + collectionItemTypes, + publishingStatuses, + limit, + selectedPage, + searchText, + otherUserName + ) + .then((subset: CollectionItemSubset) => { + /* ... */ + }) + +/* ... */ +``` + +_See [use case](../src/collections/domain/useCases/GetMyDataCollectionItems.ts) implementation_. +This use case requires the following parameters: + +- **roleIds** is an array of user role identifiers to filter the results. At least one roleId must be specified. +- **collectionItemTypes** is an array of collection item types to filter the results. At least one collectionItemType must be specified. +- **publishingStatuses** is an array of publishing statuses to filter the results. At least one publishingStatus must be specified. + +This use case supports the following optional parameters depending on the search goals: + +- **limit**: (number) Limit of items per page for pagination. (default is 10) +- **selectedPage**: (number) the page of results to be returned. (default is 1) +- **searchText** is an optional string to filter the results by. +- **otherUserName** is an optional string to return the collection items of another user. If not set, the calling user will be used. _Only superusers can use this parameter_. + +The `CollectionItemSubset`returned instance contains a property called `totalItemCount` which is necessary for pagination. + #### Get Collection Featured Items Returns a [CollectionFeaturedItem](../src/collections/domain/models/CollectionFeaturedItem.ts) array containing the featured items of the requested collection, given the collection identifier or alias. diff --git a/src/collections/domain/models/CollectionItemSubset.ts b/src/collections/domain/models/CollectionItemSubset.ts index 279b2e98..59bb3fb5 100644 --- a/src/collections/domain/models/CollectionItemSubset.ts +++ b/src/collections/domain/models/CollectionItemSubset.ts @@ -15,13 +15,13 @@ export interface CollectionItemsFacet { labels: CollectionItemsFacetLabel[] } -interface CollectionItemsFacetLabel { +export interface CollectionItemsFacetLabel { name: string count: number } interface CountPerObjectType { - dataverses: number + collections: number datasets: number files: number } diff --git a/src/collections/domain/models/CollectionPreview.ts b/src/collections/domain/models/CollectionPreview.ts index 85c0948a..3ec01646 100644 --- a/src/collections/domain/models/CollectionPreview.ts +++ b/src/collections/domain/models/CollectionPreview.ts @@ -12,4 +12,5 @@ export interface CollectionPreview { publicationStatuses: PublicationStatus[] releaseOrCreateDate: Date imageUrl?: string + userRoles?: string[] } diff --git a/src/collections/domain/repositories/ICollectionsRepository.ts b/src/collections/domain/repositories/ICollectionsRepository.ts index be784204..c8fc6549 100644 --- a/src/collections/domain/repositories/ICollectionsRepository.ts +++ b/src/collections/domain/repositories/ICollectionsRepository.ts @@ -6,6 +6,8 @@ import { CollectionFeaturedItem } from '../models/CollectionFeaturedItem' import { CollectionItemSubset } from '../models/CollectionItemSubset' import { CollectionSearchCriteria } from '../models/CollectionSearchCriteria' import { CollectionUserPermissions } from '../models/CollectionUserPermissions' +import { PublicationStatus } from '../../../../src/core/domain/models/PublicationStatus' +import { CollectionItemType } from '../../../../src/collections/domain/models/CollectionItemType' export interface ICollectionsRepository { getCollection(collectionIdOrAlias: number | string): Promise @@ -25,6 +27,15 @@ export interface ICollectionsRepository { offset?: number, collectionSearchCriteria?: CollectionSearchCriteria ): Promise + getMyDataCollectionItems( + roleIds: number[], + collectionItemTypes: CollectionItemType[], + publicationStatuses: PublicationStatus[], + limit?: number, + selectedPage?: number, + searchText?: string, + otherUserName?: string + ): Promise updateCollection( collectionIdOrAlias: number | string, updatedCollection: CollectionDTO diff --git a/src/collections/domain/useCases/GetMyDataCollectionItems.ts b/src/collections/domain/useCases/GetMyDataCollectionItems.ts new file mode 100644 index 00000000..5af00187 --- /dev/null +++ b/src/collections/domain/useCases/GetMyDataCollectionItems.ts @@ -0,0 +1,45 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { CollectionItemSubset } from '../models/CollectionItemSubset' +import { ICollectionsRepository } from '../repositories/ICollectionsRepository' +import { CollectionItemType } from '../../../../src/collections/domain/models/CollectionItemType' +import { PublicationStatus } from '../../../../src/core/domain/models/PublicationStatus' + +export class GetMyDataCollectionItems implements UseCase { + private collectionsRepository: ICollectionsRepository + + constructor(collectionsRepository: ICollectionsRepository) { + this.collectionsRepository = collectionsRepository + } + + /** + * Returns an instance of MyDataCollectionItemSubset that contains the items for which the user has the specified role or roles + * + * @param {number[]} [roleIds] - the ids of the roles to filter the items by. + * @param {CollectionItemType[]} [collectionItemTypes] - the types of items to filter by. + * @param {PublicationStatus[]} [publicationStatuses] - the publication statuses to filter by. + * @param {number} [limit] - Limit number of items to return for pagination (optional). + * @param {number} [selectedPage] - Offset (starting point) for pagination (optional). + * @param {string} [searchText] - filter by searching for this text in the results (optional). + * @param {string} [otherUserName] - filter by searching for this text in the results (optional). + * * @returns {Promise} + */ + async execute( + roleIds: number[], + collectionItemTypes: CollectionItemType[], + publicationStatuses: PublicationStatus[], + limit?: number, + selectedPage?: number, + searchText?: string, + otherUserName?: string + ): Promise { + return this.collectionsRepository.getMyDataCollectionItems( + roleIds, + collectionItemTypes, + publicationStatuses, + limit, + selectedPage, + searchText, + otherUserName + ) + } +} diff --git a/src/collections/index.ts b/src/collections/index.ts index f65f121a..07e79ad3 100644 --- a/src/collections/index.ts +++ b/src/collections/index.ts @@ -10,6 +10,7 @@ import { CollectionsRepository } from './infra/repositories/CollectionsRepositor import { UpdateCollectionFeaturedItems } from './domain/useCases/UpdateCollectionFeaturedItems' import { DeleteCollectionFeaturedItems } from './domain/useCases/DeleteCollectionFeaturedItems' import { DeleteCollection } from './domain/useCases/DeleteCollection' +import { GetMyDataCollectionItems } from './domain/useCases/GetMyDataCollectionItems' const collectionsRepository = new CollectionsRepository() @@ -18,6 +19,7 @@ const createCollection = new CreateCollection(collectionsRepository) const getCollectionFacets = new GetCollectionFacets(collectionsRepository) const getCollectionUserPermissions = new GetCollectionUserPermissions(collectionsRepository) const getCollectionItems = new GetCollectionItems(collectionsRepository) +const getMyDataCollectionItems = new GetMyDataCollectionItems(collectionsRepository) const publishCollection = new PublishCollection(collectionsRepository) const updateCollection = new UpdateCollection(collectionsRepository) const getCollectionFeaturedItems = new GetCollectionFeaturedItems(collectionsRepository) @@ -31,6 +33,7 @@ export { getCollectionFacets, getCollectionUserPermissions, getCollectionItems, + getMyDataCollectionItems, publishCollection, updateCollection, getCollectionFeaturedItems, diff --git a/src/collections/infra/repositories/CollectionsRepository.ts b/src/collections/infra/repositories/CollectionsRepository.ts index b3897f22..4123d181 100644 --- a/src/collections/infra/repositories/CollectionsRepository.ts +++ b/src/collections/infra/repositories/CollectionsRepository.ts @@ -3,7 +3,8 @@ import { ICollectionsRepository } from '../../domain/repositories/ICollectionsRe import { transformCollectionFacetsResponseToCollectionFacets, transformCollectionItemsResponseToCollectionItemSubset, - transformCollectionResponseToCollection + transformCollectionResponseToCollection, + transformMyDataResponseToCollectionItemSubset } from './transformers/collectionTransformers' import { Collection, ROOT_COLLECTION_ID } from '../../domain/models/Collection' import { CollectionDTO } from '../../domain/dtos/CollectionDTO' @@ -21,6 +22,8 @@ import { CollectionFeaturedItem } from '../../domain/models/CollectionFeaturedIt import { transformCollectionFeaturedItemsPayloadToCollectionFeaturedItems } from './transformers/collectionFeaturedItemsTransformer' import { CollectionFeaturedItemsDTO } from '../../domain/dtos/CollectionFeaturedItemsDTO' import { ApiConstants } from '../../../core/infra/repositories/ApiConstants' +import { PublicationStatus } from '../../../../src/core/domain/models/PublicationStatus' +import { ReadError } from '../../../core/domain/repositories/ReadError' export interface NewCollectionRequestPayload { alias: string @@ -63,6 +66,16 @@ export enum GetCollectionItemsQueryParams { SHOW_TYPE_COUNTS = 'show_type_counts' } +export enum GetMyDataCollectionItemsQueryParams { + SEARCH_TEXT = 'mydata_search_term', + PER_PAGE = 'per_page', + SELECTED_PAGE = 'selected_page', + ROLE_ID = 'role_ids', + TYPE = 'dvobject_types', + PUBLISHED_STATES = 'published_states', + USER_IDENTIFIER = 'userIdentifier' +} + export class CollectionsRepository extends ApiRepository implements ICollectionsRepository { private readonly collectionsResourceName: string = 'dataverses' @@ -188,6 +201,76 @@ export class CollectionsRepository extends ApiRepository implements ICollections }) } + public async getMyDataCollectionItems( + roleIds: number[], + collectionItemTypes: CollectionItemType[], + publicationStatuses: PublicationStatus[], + limit?: number, + selectedPage?: number, + searchText?: string, + userIdentifier?: string + ): Promise { + const queryParams = new URLSearchParams() + + if (limit) { + queryParams.set(GetMyDataCollectionItemsQueryParams.PER_PAGE, limit.toString()) + } + + if (selectedPage) { + queryParams.set(GetMyDataCollectionItemsQueryParams.SELECTED_PAGE, selectedPage.toString()) + } + + if (searchText) { + queryParams.set( + GetMyDataCollectionItemsQueryParams.SEARCH_TEXT, + encodeURIComponent(searchText) + ) + } + + roleIds.forEach((roleId) => { + queryParams.append(GetMyDataCollectionItemsQueryParams.ROLE_ID, roleId.toString()) + }) + if (userIdentifier) { + queryParams.set(GetMyDataCollectionItemsQueryParams.USER_IDENTIFIER, userIdentifier) + } + + collectionItemTypes.forEach((itemType) => { + let mappedItemType: string + + switch (itemType) { + case CollectionItemType.COLLECTION: + mappedItemType = 'Dataverse' + break + case CollectionItemType.DATASET: + mappedItemType = 'Dataset' + break + case CollectionItemType.FILE: + mappedItemType = 'DataFile' + break + } + queryParams.append(GetMyDataCollectionItemsQueryParams.TYPE, mappedItemType) + }) + + publicationStatuses.forEach((publicationStatus) => { + queryParams.append( + GetMyDataCollectionItemsQueryParams.PUBLISHED_STATES, + publicationStatus.toString() + ) + }) + const result = await this.doGet('/mydata/retrieve', true, queryParams) + .then((response) => { + if (response.data.success !== true) { + throw new ReadError(response.data.error_message) + } + return transformMyDataResponseToCollectionItemSubset(response) + }) + .catch((error) => { + throw error + }) + + return result + } + private createCreateOrUpdateRequestBody( collectionDTO: CollectionDTO ): NewCollectionRequestPayload { diff --git a/src/collections/infra/repositories/transformers/MyDataCollectionPreviewPayload.ts b/src/collections/infra/repositories/transformers/MyDataCollectionPreviewPayload.ts new file mode 100644 index 00000000..bc0d4b6c --- /dev/null +++ b/src/collections/infra/repositories/transformers/MyDataCollectionPreviewPayload.ts @@ -0,0 +1,14 @@ +export interface MyDataCollectionPreviewPayload { + name: string + parentDataverseName: string + identifier: string + parentDataverseIdentifier: string + url: string + image_url: string + description: string + type?: string + publicationStatuses: string[] + affiliation: string + published_at: string + user_roles: string[] +} diff --git a/src/collections/infra/repositories/transformers/MyDataCountPerObjectTypePayload.ts b/src/collections/infra/repositories/transformers/MyDataCountPerObjectTypePayload.ts new file mode 100644 index 00000000..fb27d4de --- /dev/null +++ b/src/collections/infra/repositories/transformers/MyDataCountPerObjectTypePayload.ts @@ -0,0 +1,5 @@ +export interface MyDataCountPerObjectTypePayload { + dataverses_count: number + datasets_count: number + files_count: number +} diff --git a/src/collections/infra/repositories/transformers/MyDataPublicationStatusCountsPayload.ts b/src/collections/infra/repositories/transformers/MyDataPublicationStatusCountsPayload.ts new file mode 100644 index 00000000..ea9d989b --- /dev/null +++ b/src/collections/infra/repositories/transformers/MyDataPublicationStatusCountsPayload.ts @@ -0,0 +1,7 @@ +export interface MyDataPublicationStatusCountsPayload { + deaccessioned_count: number + draft_count: number + in_review_count: number + published_count: number + unpublished_count: number +} diff --git a/src/collections/infra/repositories/transformers/collectionPreviewsTransformers.ts b/src/collections/infra/repositories/transformers/collectionPreviewsTransformers.ts index bbbf1af1..25c21c6c 100644 --- a/src/collections/infra/repositories/transformers/collectionPreviewsTransformers.ts +++ b/src/collections/infra/repositories/transformers/collectionPreviewsTransformers.ts @@ -2,6 +2,7 @@ import { PublicationStatus } from '../../../../core/domain/models/PublicationSta import { CollectionItemType } from '../../../../collections/domain/models/CollectionItemType' import { CollectionPreview } from '../../../domain/models/CollectionPreview' import { CollectionPreviewPayload } from './CollectionPreviewPayload' +import { MyDataCollectionPreviewPayload } from './MyDataCollectionPreviewPayload' export const transformCollectionPreviewPayloadToCollectionPreview = ( collectionPreviewPayload: CollectionPreviewPayload @@ -25,3 +26,27 @@ export const transformCollectionPreviewPayloadToCollectionPreview = ( releaseOrCreateDate: new Date(collectionPreviewPayload.published_at) } } + +export const transformMyDataCollectionPreviewPayloadToCollectionPreview = ( + collectionPreviewPayload: MyDataCollectionPreviewPayload +): CollectionPreview => { + const publicationStatuses: PublicationStatus[] = [] + collectionPreviewPayload.publicationStatuses.forEach((element) => { + publicationStatuses.push(element as unknown as PublicationStatus) + }) + return { + type: CollectionItemType.COLLECTION, + name: collectionPreviewPayload.name, + parentName: collectionPreviewPayload.parentDataverseName, + alias: collectionPreviewPayload.identifier, + parentAlias: collectionPreviewPayload.parentDataverseIdentifier, + description: collectionPreviewPayload.description, + publicationStatuses: publicationStatuses, + affiliation: collectionPreviewPayload.affiliation, + ...(collectionPreviewPayload.image_url && { + imageUrl: collectionPreviewPayload.image_url + }), + releaseOrCreateDate: new Date(collectionPreviewPayload.published_at), + userRoles: collectionPreviewPayload.user_roles + } +} diff --git a/src/collections/infra/repositories/transformers/collectionTransformers.ts b/src/collections/infra/repositories/transformers/collectionTransformers.ts index 33c6e9b7..aed10e29 100644 --- a/src/collections/infra/repositories/transformers/collectionTransformers.ts +++ b/src/collections/infra/repositories/transformers/collectionTransformers.ts @@ -10,21 +10,36 @@ import { CollectionFacet } from '../../../domain/models/CollectionFacet' import { CollectionFacetPayload } from './CollectionFacetPayload' import { CollectionItemsFacet, + CollectionItemsFacetLabel, CollectionItemSubset } from '../../../domain/models/CollectionItemSubset' import { DatasetPreview } from '../../../../datasets' import { FilePreview } from '../../../../files' import { DatasetPreviewPayload } from '../../../../datasets/infra/repositories/transformers/DatasetPreviewPayload' import { FilePreviewPayload } from '../../../../files/infra/repositories/transformers/FilePreviewPayload' -import { transformDatasetPreviewPayloadToDatasetPreview } from '../../../../datasets/infra/repositories/transformers/datasetPreviewsTransformers' -import { transformFilePreviewPayloadToFilePreview } from '../../../../files/infra/repositories/transformers/filePreviewTransformers' -import { transformCollectionPreviewPayloadToCollectionPreview } from './collectionPreviewsTransformers' +import { + transformDatasetPreviewPayloadToDatasetPreview, + transformMyDataDatasetPreviewPayloadToDatasetPreview +} from '../../../../datasets/infra/repositories/transformers/datasetPreviewsTransformers' +import { + transformFilePreviewPayloadToFilePreview, + transformMyDataFilePreviewPayloadToFilePreview +} from '../../../../files/infra/repositories/transformers/filePreviewTransformers' +import { + transformCollectionPreviewPayloadToCollectionPreview, + transformMyDataCollectionPreviewPayloadToCollectionPreview +} from './collectionPreviewsTransformers' import { CollectionPreviewPayload } from './CollectionPreviewPayload' import { CollectionPreview } from '../../../domain/models/CollectionPreview' import { CollectionContact } from '../../../domain/models/CollectionContact' import { CollectionType } from '../../../domain/models/CollectionType' import { CollectionItemsFacetPayload } from './CollectionItemsFacetsPayload' import { CollectionItemsCountPerObjectTypePayload } from './CollectionItemsCountPerObjectTypePayload' +import { MyDataFilePreviewPayload } from '../../../../files/infra/repositories/transformers/MyDataFilePreviewPayload' +import { MyDataDatasetPreviewPayload } from '../../../../datasets/infra/repositories/transformers/MyDataDatasetPreviewPayload' +import { MyDataCollectionPreviewPayload } from './MyDataCollectionPreviewPayload' +import { MyDataCountPerObjectTypePayload } from './MyDataCountPerObjectTypePayload' +import { MyDataPublicationStatusCountsPayload } from './MyDataPublicationStatusCountsPayload' export const transformCollectionResponseToCollection = (response: AxiosResponse): Collection => { const collectionPayload = response.data.data @@ -119,7 +134,7 @@ export const transformCollectionItemsResponseToCollectionItemSubset = ( ) const countPerObjectType = { - dataverses: countPerObjectTypePayload['Dataverses'], + collections: countPerObjectTypePayload['Dataverses'], datasets: countPerObjectTypePayload['Datasets'], files: countPerObjectTypePayload['Files'] } @@ -132,6 +147,60 @@ export const transformCollectionItemsResponseToCollectionItemSubset = ( } } +export const transformMyDataResponseToCollectionItemSubset = ( + response: AxiosResponse +): CollectionItemSubset => { + const responseDataPayload = response.data.data + const itemsPayload = responseDataPayload.items + const countPerObjectTypePayload = responseDataPayload[ + 'dvobject_counts' + ] as MyDataCountPerObjectTypePayload + + const items: (DatasetPreview | FilePreview | CollectionPreview)[] = [] + + itemsPayload.forEach(function ( + itemPayload: + | MyDataCollectionPreviewPayload + | MyDataDatasetPreviewPayload + | MyDataFilePreviewPayload + ) { + if (itemPayload.type === 'file') { + items.push( + transformMyDataFilePreviewPayloadToFilePreview(itemPayload as MyDataFilePreviewPayload) + ) + } else if (itemPayload.type === 'dataset') { + items.push( + transformMyDataDatasetPreviewPayloadToDatasetPreview( + itemPayload as MyDataDatasetPreviewPayload + ) + ) + } else if (itemPayload.type === 'dataverse') { + items.push( + transformMyDataCollectionPreviewPayloadToCollectionPreview( + itemPayload as unknown as MyDataCollectionPreviewPayload + ) + ) + } + }) + + const countPerObjectType = { + collections: countPerObjectTypePayload['dataverses_count'], + datasets: countPerObjectTypePayload['datasets_count'], + files: countPerObjectTypePayload['files_count'] + } + const publishingFacet: CollectionItemsFacet = transformPublicationStatusResponseToFacet( + responseDataPayload.pubstatus_counts as MyDataPublicationStatusCountsPayload + ) + const facets: CollectionItemsFacet[] = [publishingFacet] + + return { + items, + facets, + totalItemCount: responseDataPayload.pagination.numResults, + countPerObjectType + } +} + const transformContactsPayloadToContacts = ( contactsPayload: CollectionContactPayload[] ): CollectionContact[] => { @@ -140,3 +209,20 @@ const transformContactsPayloadToContacts = ( displayOrder: contactPayload.displayOrder })) } + +const transformPublicationStatusResponseToFacet = ( + publicationStatusCounts: MyDataPublicationStatusCountsPayload +): CollectionItemsFacet => { + const labels: CollectionItemsFacetLabel[] = [ + { name: 'Published', count: publicationStatusCounts.published_count }, + { name: 'Unpublished', count: publicationStatusCounts.unpublished_count }, + { name: 'Draft', count: publicationStatusCounts.draft_count }, + { name: 'In Review', count: publicationStatusCounts.in_review_count }, + { name: 'Deaccessioned', count: publicationStatusCounts.deaccessioned_count } + ] + return { + name: 'publicationStatus', + friendlyName: 'Publication Status', + labels + } +} diff --git a/src/core/domain/models/PublicationStatus.ts b/src/core/domain/models/PublicationStatus.ts index a56386f3..d2266979 100644 --- a/src/core/domain/models/PublicationStatus.ts +++ b/src/core/domain/models/PublicationStatus.ts @@ -1,5 +1,7 @@ export enum PublicationStatus { Published = 'Published', Unpublished = 'Unpublished', - Draft = 'Draft' + Draft = 'Draft', + Deaccessioned = 'Deaccessioned', + InReview = 'In Review' } diff --git a/src/datasets/domain/models/DatasetPreview.ts b/src/datasets/domain/models/DatasetPreview.ts index f139ec0d..5ac1da00 100644 --- a/src/datasets/domain/models/DatasetPreview.ts +++ b/src/datasets/domain/models/DatasetPreview.ts @@ -14,4 +14,5 @@ export interface DatasetPreview { parentCollectionName: string parentCollectionAlias: string imageUrl?: string + userRoles?: string[] } diff --git a/src/datasets/infra/repositories/transformers/MyDataDatasetPreviewPayload.ts b/src/datasets/infra/repositories/transformers/MyDataDatasetPreviewPayload.ts new file mode 100644 index 00000000..6b11529d --- /dev/null +++ b/src/datasets/infra/repositories/transformers/MyDataDatasetPreviewPayload.ts @@ -0,0 +1,21 @@ +export interface MyDataDatasetPreviewPayload { + name: string + type: string + global_id: string + description: string + publisher: string + citationHtml: string + identifier_of_dataverse: string + name_of_dataverse: string + publicationStatuses: string[] + majorVersion: number + minorVersion: number + versionId: number + versionState: string + createdAt: string + updatedAt: string + publication_statuses: string[] + user_roles: string[] + image_url?: string + published_at?: string +} diff --git a/src/datasets/infra/repositories/transformers/datasetPreviewsTransformers.ts b/src/datasets/infra/repositories/transformers/datasetPreviewsTransformers.ts index 2f87ac19..26d59c66 100644 --- a/src/datasets/infra/repositories/transformers/datasetPreviewsTransformers.ts +++ b/src/datasets/infra/repositories/transformers/datasetPreviewsTransformers.ts @@ -5,6 +5,7 @@ import { DatasetPreviewSubset } from '../../../domain/models/DatasetPreviewSubse import { DatasetPreviewPayload } from './DatasetPreviewPayload' import { PublicationStatus } from '../../../../core/domain/models/PublicationStatus' import { CollectionItemType } from '../../../../collections/domain/models/CollectionItemType' +import { MyDataDatasetPreviewPayload } from './MyDataDatasetPreviewPayload' export const transformDatasetPreviewsResponseToDatasetPreviewSubset = ( response: AxiosResponse @@ -53,3 +54,37 @@ export const transformDatasetPreviewPayloadToDatasetPreview = ( }) } } + +export const transformMyDataDatasetPreviewPayloadToDatasetPreview = ( + datasetPreviewPayload: MyDataDatasetPreviewPayload +): DatasetPreview => { + const publicationStatuses: PublicationStatus[] = [] + datasetPreviewPayload.publicationStatuses.forEach((element) => { + publicationStatuses.push(element as unknown as PublicationStatus) + }) + return { + type: CollectionItemType.DATASET, + persistentId: datasetPreviewPayload.global_id, + title: datasetPreviewPayload.name, + versionId: datasetPreviewPayload.versionId, + versionInfo: { + majorNumber: datasetPreviewPayload.majorVersion, + minorNumber: datasetPreviewPayload.minorVersion, + state: datasetPreviewPayload.versionState as DatasetVersionState, + createTime: new Date(datasetPreviewPayload.createdAt), + lastUpdateTime: new Date(datasetPreviewPayload.updatedAt), + ...(datasetPreviewPayload.published_at && { + releaseTime: new Date(datasetPreviewPayload.published_at) + }) + }, + citation: datasetPreviewPayload.citationHtml, + description: datasetPreviewPayload.description, + publicationStatuses: publicationStatuses, + parentCollectionAlias: datasetPreviewPayload.identifier_of_dataverse, + parentCollectionName: datasetPreviewPayload.name_of_dataverse, + ...(datasetPreviewPayload.image_url && { + imageUrl: datasetPreviewPayload.image_url + }), + userRoles: datasetPreviewPayload.user_roles + } +} diff --git a/src/files/domain/models/FilePreview.ts b/src/files/domain/models/FilePreview.ts index 5179b407..ab74f1d5 100644 --- a/src/files/domain/models/FilePreview.ts +++ b/src/files/domain/models/FilePreview.ts @@ -27,6 +27,7 @@ export interface FilePreview { tabularTags?: string[] variables?: number observations?: number + userRoles?: string[] } export interface FilePreviewChecksum { diff --git a/src/files/infra/repositories/transformers/MyDataFilePreviewPayload.ts b/src/files/infra/repositories/transformers/MyDataFilePreviewPayload.ts new file mode 100644 index 00000000..9aced9e4 --- /dev/null +++ b/src/files/infra/repositories/transformers/MyDataFilePreviewPayload.ts @@ -0,0 +1,40 @@ +import { FilePreviewChecksumPayload } from '../../../../../src/files/infra/repositories/transformers/FilePreviewPayload' + +export interface MyDataFilePreviewPayload { + name: string + type: string + url: string + file_id: string + file_type: string + file_content_type: string + size_in_bytes: number + md5: string + checksum: FilePreviewChecksumPayload + unf: string + dataset_name: string + dataset_id: string + dataset_persistent_id: string + dataset_citation: string + restricted: boolean + canDownloadFile: boolean + matches: string[] + score: number + entity_id: number + publicationStatuses: string[] + releaseOrCreateDate: string + is_draft_state: boolean + is_in_review_state: boolean + is_unpublished_state: boolean + is_published: boolean + is_deaccesioned: boolean + is_valid: boolean + date_to_display_on_card: string + parentIdentifier: string + parentName: string + user_roles: string[] + image_url?: string + variables?: number + observations?: number + file_persistent_id?: string + description?: string +} diff --git a/src/files/infra/repositories/transformers/filePreviewTransformers.ts b/src/files/infra/repositories/transformers/filePreviewTransformers.ts index 0c91ce30..514e58da 100644 --- a/src/files/infra/repositories/transformers/filePreviewTransformers.ts +++ b/src/files/infra/repositories/transformers/filePreviewTransformers.ts @@ -2,6 +2,7 @@ import { CollectionItemType } from '../../../../collections/domain/models/Collec import { PublicationStatus } from '../../../../core/domain/models/PublicationStatus' import { FilePreview } from '../../../domain/models/FilePreview' import { FilePreviewPayload } from './FilePreviewPayload' +import { MyDataFilePreviewPayload } from './MyDataFilePreviewPayload' export const transformFilePreviewPayloadToFilePreview = ( filePreviewPayload: FilePreviewPayload @@ -47,3 +48,44 @@ export const transformFilePreviewPayloadToFilePreview = ( ...(filePreviewPayload.observations && { observations: filePreviewPayload.observations }) } } +export const transformMyDataFilePreviewPayloadToFilePreview = ( + filePreviewPayload: MyDataFilePreviewPayload +): FilePreview => { + const publicationStatuses: PublicationStatus[] = [] + filePreviewPayload.publicationStatuses.forEach((element) => { + publicationStatuses.push(element as unknown as PublicationStatus) + }) + return { + type: CollectionItemType.FILE, + name: filePreviewPayload.name, + url: filePreviewPayload.url, + ...(filePreviewPayload.image_url && { imageUrl: filePreviewPayload.image_url }), + fileId: Number(filePreviewPayload.file_id), + ...(filePreviewPayload.file_persistent_id && { + filePersistentId: filePreviewPayload.file_persistent_id + }), + description: filePreviewPayload.description ?? '', + fileType: filePreviewPayload.file_type, + fileContentType: filePreviewPayload.file_content_type, + sizeInBytes: filePreviewPayload.size_in_bytes, + ...(filePreviewPayload.md5 && { md5: filePreviewPayload.md5 }), + ...(filePreviewPayload.checksum && { + checksum: { + type: filePreviewPayload.checksum.type, + value: filePreviewPayload.checksum.value + } + }), + unf: filePreviewPayload.unf, + datasetName: filePreviewPayload.dataset_name, + datasetId: Number(filePreviewPayload.dataset_id), + datasetPersistentId: filePreviewPayload.dataset_persistent_id, + datasetCitation: filePreviewPayload.dataset_citation, + publicationStatuses: publicationStatuses, + releaseOrCreateDate: new Date(filePreviewPayload.releaseOrCreateDate), + restricted: filePreviewPayload.restricted, + canDownloadFile: filePreviewPayload.canDownloadFile, + ...(filePreviewPayload.variables && { variables: filePreviewPayload.variables }), + ...(filePreviewPayload.observations && { observations: filePreviewPayload.observations }), + userRoles: filePreviewPayload.user_roles + } +} diff --git a/test/functional/collections/GetMyDataCollectionItems.test.ts b/test/functional/collections/GetMyDataCollectionItems.test.ts new file mode 100644 index 00000000..0c303d5e --- /dev/null +++ b/test/functional/collections/GetMyDataCollectionItems.test.ts @@ -0,0 +1,207 @@ +import { + ApiConfig, + createDataset, + CreatedDatasetIdentifiers, + CollectionPreview, + getMyDataCollectionItems, + ReadError +} from '../../../src' +import { TestConstants } from '../../testHelpers/TestConstants' +import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' +import { + createCollectionViaApi, + deleteCollectionViaApi +} from '../../testHelpers/collections/collectionHelper' +import { uploadFileViaApi } from '../../testHelpers/files/filesHelper' +import { deleteUnpublishedDatasetViaApi } from '../../testHelpers/datasets/datasetHelper' +import { CollectionItemType } from '../../../src/collections/domain/models/CollectionItemType' +import { PublicationStatus } from '../../../src/core/domain/models/PublicationStatus' + +const testRoleIds = [1, 2, 3, 4, 5, 6, 7, 8] +const testCollectionItemTypes = [ + CollectionItemType.COLLECTION, + CollectionItemType.DATASET, + CollectionItemType.FILE +] +const testPublishingStatuses = [ + PublicationStatus.Published, + PublicationStatus.Draft, + PublicationStatus.Unpublished +] + +describe('execute', () => { + const testCollectionAlias = 'collectionsRepositoryGetMyDataCollection' + let testDatasetIds: CreatedDatasetIdentifiers + const testTextFile1Name = 'test-file-2.txt' + + beforeAll(async () => { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + await createCollectionViaApi(testCollectionAlias) + try { + testDatasetIds = await createDataset.execute( + TestConstants.TEST_NEW_DATASET_DTO, + testCollectionAlias + ) + } catch (error) { + throw new Error('Tests beforeAll(): Error while creating test dataset') + } + await uploadFileViaApi(testDatasetIds.numericId, testTextFile1Name).catch(() => { + throw new Error(`Tests beforeAll(): Error while uploading file ${testTextFile1Name}`) + }) + }) + + afterAll(async () => { + try { + await deleteUnpublishedDatasetViaApi(testDatasetIds.numericId) + } catch (error) { + throw new Error('Tests afterAll(): Error while deleting test dataset') + } + try { + await deleteCollectionViaApi(testCollectionAlias) + } catch (error) { + throw new Error('Tests afterAll(): Error while deleting test collection') + } + }) + test('should return error message when repository returns empty item subset', async () => { + expect.assertions(2) + let readError: ReadError | undefined = undefined + try { + await getMyDataCollectionItems.execute( + testRoleIds, + testCollectionItemTypes, + [PublicationStatus.Deaccessioned], + undefined, + undefined, + undefined + ) + throw new Error('Use case should throw an error') + } catch (error) { + readError = error as ReadError + } finally { + expect(readError).toBeInstanceOf(ReadError) + expect(readError?.message).toEqual( + 'There was an error when reading the resource. Reason was: Sorry, no results were found.' + ) + } + }), + test('should return items when valid roles,collection types, and publishingStatuses are provided', async () => { + // Give enough time to Solr for indexing + await new Promise((resolve) => setTimeout(resolve, 5000)) + + try { + const actual = await getMyDataCollectionItems.execute( + testRoleIds, + testCollectionItemTypes, + testPublishingStatuses, + undefined, + undefined, + testCollectionAlias + ) + + const actualCollectionPreview = actual.items[0] as CollectionPreview + expect(actualCollectionPreview.alias).toBe(testCollectionAlias) + + expect(actual.totalItemCount).toBe(1) + expect(actual.facets).toEqual([ + { + name: 'publicationStatus', + friendlyName: 'Publication Status', + labels: [ + { + name: 'Published', + count: 0 + }, + { + name: 'Unpublished', + count: 1 + }, + { + name: 'Draft', + count: 0 + }, + { + name: 'In Review', + count: 0 + }, + { + name: 'Deaccessioned', + count: 0 + } + ] + } + ]) + expect(actual.countPerObjectType).toEqual({ + collections: 1, + datasets: 0, + files: 0 + }) + } catch (error) { + console.log(error) + throw new Error('Item subset should be retrieved') + } + }) + + test('should return an error message when no role is specified', async () => { + expect.assertions(2) + let readError: ReadError | undefined = undefined + try { + await getMyDataCollectionItems.execute([], [], [], undefined, undefined, undefined) + throw new Error('Use case should throw an error') + } catch (error) { + readError = error as ReadError + } finally { + expect(readError).toBeInstanceOf(ReadError) + expect(readError?.message).toEqual( + `There was an error when reading the resource. Reason was: No results. Please select at least one Role.` + ) + } + }) + test('should return an error message when no publication status is specified', async () => { + expect.assertions(2) + let readError: ReadError | undefined = undefined + try { + await getMyDataCollectionItems.execute( + testRoleIds, + testCollectionItemTypes, + [], + undefined, + undefined, + undefined + ) + throw new Error('Use case should throw an error') + } catch (error) { + readError = error as ReadError + } finally { + expect(readError).toBeInstanceOf(ReadError) + expect(readError?.message).toEqual( + `There was an error when reading the resource. Reason was: No user found for: "Published, Unpublished, Draft, In Review, Deaccessioned"` + ) + } + }) + test('should an error message when no collection type is specified', async () => { + expect.assertions(2) + let readError: ReadError | undefined = undefined + try { + await getMyDataCollectionItems.execute( + testRoleIds, + testCollectionItemTypes, + [], + undefined, + undefined, + undefined + ) + throw new Error('Use case should throw an error') + } catch (error) { + readError = error as ReadError + } finally { + expect(readError).toBeInstanceOf(ReadError) + expect(readError?.message).toEqual( + `There was an error when reading the resource. Reason was: No user found for: "Published, Unpublished, Draft, In Review, Deaccessioned"` + ) + } + }) +}) diff --git a/test/integration/collections/CollectionsRepository.test.ts b/test/integration/collections/CollectionsRepository.test.ts index 13441469..83b026e2 100644 --- a/test/integration/collections/CollectionsRepository.test.ts +++ b/test/integration/collections/CollectionsRepository.test.ts @@ -11,7 +11,9 @@ import { FilePreview, ReadError, WriteError, - createDataset + createDataset, + getCollection, + createCollection } from '../../../src' import { ApiConfig } from '../../../src' import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' @@ -41,6 +43,7 @@ import { deleteCollectionFeaturedItemsViaApi, deleteCollectionFeaturedItemViaApi } from '../../testHelpers/collections/collectionFeaturedItemsHelper' +import { createApiTokenViaApi } from '../../testHelpers/users/apiTokenHelper' describe('CollectionsRepository', () => { const testCollectionAlias = 'collectionsRepositoryTestCollection' @@ -49,6 +52,7 @@ describe('CollectionsRepository', () => { const currentYear = new Date().getFullYear() beforeAll(async () => { + // create builtin user and pass API key to APiConfig ApiConfig.init( TestConstants.TEST_API_URL, DataverseApiAuthMechanism.API_KEY, @@ -155,6 +159,7 @@ describe('CollectionsRepository', () => { const actualAfterDatasetDeletion = await sut.getCollection(parentCollectionAlias) expect(actualAfterDatasetDeletion.childCount).toBe(0) + await deleteCollectionViaApi(parentCollectionAlias) }) }) @@ -524,7 +529,7 @@ describe('CollectionsRepository', () => { expect(actualCollectionPreview.type).toBe(CollectionItemType.COLLECTION) expect(actual.totalItemCount).toBe(3) - expect(actual.countPerObjectType.dataverses).toBe(1) + expect(actual.countPerObjectType.collections).toBe(1) expect(actual.countPerObjectType.datasets).toBe(1) expect(actual.countPerObjectType.files).toBe(1) @@ -548,7 +553,7 @@ describe('CollectionsRepository', () => { ) expect(actual.totalItemCount).toBe(1) expect((actual.items[0] as FilePreview).name).toBe(expectedFileName) - expect(actual.countPerObjectType.dataverses).toBe(0) + expect(actual.countPerObjectType.collections).toBe(0) expect(actual.countPerObjectType.datasets).toBe(0) expect(actual.countPerObjectType.files).toBe(1) @@ -563,7 +568,7 @@ describe('CollectionsRepository', () => { ) expect(actual.totalItemCount).toBe(1) expect((actual.items[0] as DatasetPreview).title).toBe(expectedDatasetDescription) - expect(actual.countPerObjectType.dataverses).toBe(0) + expect(actual.countPerObjectType.collections).toBe(0) expect(actual.countPerObjectType.datasets).toBe(1) expect(actual.countPerObjectType.files).toBe(0) @@ -578,7 +583,7 @@ describe('CollectionsRepository', () => { expect(actual.totalItemCount).toBe(2) expect((actual.items[0] as DatasetPreview).title).toBe(expectedDatasetDescription) expect((actual.items[1] as CollectionPreview).name).toBe(expectedCollectionsName) - expect(actual.countPerObjectType.dataverses).toBe(1) + expect(actual.countPerObjectType.collections).toBe(1) expect(actual.countPerObjectType.datasets).toBe(1) expect(actual.countPerObjectType.files).toBe(0) @@ -592,7 +597,7 @@ describe('CollectionsRepository', () => { expect(actual.items.length).toBe(1) expect(actual.totalItemCount).toBe(2) expect((actual.items[0] as CollectionPreview).name).toBe(expectedCollectionsName) - expect(actual.countPerObjectType.dataverses).toBe(1) + expect(actual.countPerObjectType.collections).toBe(1) expect(actual.countPerObjectType.datasets).toBe(1) expect(actual.countPerObjectType.files).toBe(0) @@ -609,7 +614,7 @@ describe('CollectionsRepository', () => { expect(actual.totalItemCount).toBe(1) expect((actual.items[0] as CollectionPreview).name).toBe(expectedCollectionsName) expect(actual.facets).toEqual(expectedFacetsFromCollectionOnly) - expect(actual.countPerObjectType.dataverses).toBe(1) + expect(actual.countPerObjectType.collections).toBe(1) expect(actual.countPerObjectType.datasets).toBe(1) expect(actual.countPerObjectType.files).toBe(1) @@ -627,7 +632,7 @@ describe('CollectionsRepository', () => { expect(actual.totalItemCount).toBe(1) expect((actual.items[0] as DatasetPreview).title).toBe(expectedDatasetDescription) expect(actual.facets).toEqual(expectedFacetsFromDatasetOnly) - expect(actual.countPerObjectType.dataverses).toBe(1) + expect(actual.countPerObjectType.collections).toBe(1) expect(actual.countPerObjectType.datasets).toBe(1) expect(actual.countPerObjectType.files).toBe(1) @@ -645,7 +650,7 @@ describe('CollectionsRepository', () => { expect(actual.totalItemCount).toBe(1) expect((actual.items[0] as FilePreview).name).toBe(expectedFileName) expect(actual.facets).toEqual(expectedFacetsFromFileOnly) - expect(actual.countPerObjectType.dataverses).toBe(1) + expect(actual.countPerObjectType.collections).toBe(1) expect(actual.countPerObjectType.datasets).toBe(1) expect(actual.countPerObjectType.files).toBe(1) @@ -665,7 +670,7 @@ describe('CollectionsRepository', () => { expect((actual.items[0] as FilePreview).name).toBe(expectedFileName) expect((actual.items[1] as CollectionPreview).name).toBe(expectedCollectionsName) expect(actual.facets).toEqual(expectedFacetsFromCollectionAndFile) - expect(actual.countPerObjectType.dataverses).toBe(1) + expect(actual.countPerObjectType.collections).toBe(1) expect(actual.countPerObjectType.datasets).toBe(1) expect(actual.countPerObjectType.files).toBe(1) @@ -685,7 +690,7 @@ describe('CollectionsRepository', () => { expect((actual.items[0] as DatasetPreview).type).toBe(CollectionItemType.DATASET) expect((actual.items[1] as CollectionPreview).type).toBe(CollectionItemType.COLLECTION) expect((actual.items[2] as FilePreview).type).toBe(CollectionItemType.FILE) - expect(actual.countPerObjectType.dataverses).toBe(1) + expect(actual.countPerObjectType.collections).toBe(1) expect(actual.countPerObjectType.datasets).toBe(1) expect(actual.countPerObjectType.files).toBe(1) @@ -705,7 +710,7 @@ describe('CollectionsRepository', () => { expect((actual.items[0] as FilePreview).type).toBe(CollectionItemType.FILE) expect((actual.items[1] as CollectionPreview).type).toBe(CollectionItemType.COLLECTION) expect((actual.items[2] as DatasetPreview).type).toBe(CollectionItemType.DATASET) - expect(actual.countPerObjectType.dataverses).toBe(1) + expect(actual.countPerObjectType.collections).toBe(1) expect(actual.countPerObjectType.datasets).toBe(1) expect(actual.countPerObjectType.files).toBe(1) @@ -725,7 +730,7 @@ describe('CollectionsRepository', () => { expect((actual.items[0] as CollectionPreview).type).toBe(CollectionItemType.COLLECTION) expect((actual.items[1] as DatasetPreview).type).toBe(CollectionItemType.DATASET) expect((actual.items[2] as FilePreview).type).toBe(CollectionItemType.FILE) - expect(actual.countPerObjectType.dataverses).toBe(1) + expect(actual.countPerObjectType.collections).toBe(1) expect(actual.countPerObjectType.datasets).toBe(1) expect(actual.countPerObjectType.files).toBe(1) @@ -745,7 +750,7 @@ describe('CollectionsRepository', () => { expect((actual.items[0] as DatasetPreview).type).toBe(CollectionItemType.DATASET) expect((actual.items[1] as FilePreview).type).toBe(CollectionItemType.FILE) expect((actual.items[2] as CollectionPreview).type).toBe(CollectionItemType.COLLECTION) - expect(actual.countPerObjectType.dataverses).toBe(1) + expect(actual.countPerObjectType.collections).toBe(1) expect(actual.countPerObjectType.datasets).toBe(1) expect(actual.countPerObjectType.files).toBe(1) @@ -763,7 +768,7 @@ describe('CollectionsRepository', () => { expect(actual.totalItemCount).toBe(1) expect((actual.items[0] as CollectionPreview).name).toBe(expectedCollectionsName) expect(actual.facets).toEqual(expectedFacetsFromCollectionOnly) - expect(actual.countPerObjectType.dataverses).toBe(1) + expect(actual.countPerObjectType.collections).toBe(1) expect(actual.countPerObjectType.datasets).toBe(0) expect(actual.countPerObjectType.files).toBe(0) @@ -783,7 +788,7 @@ describe('CollectionsRepository', () => { expect(actual.totalItemCount).toBe(1) expect((actual.items[0] as DatasetPreview).title).toBe(expectedDatasetDescription) expect(actual.facets).toEqual(expectedFacetsFromDatasetOnly) - expect(actual.countPerObjectType.dataverses).toBe(0) + expect(actual.countPerObjectType.collections).toBe(0) expect(actual.countPerObjectType.datasets).toBe(1) expect(actual.countPerObjectType.files).toBe(0) @@ -802,7 +807,7 @@ describe('CollectionsRepository', () => { expect(actual.totalItemCount).toBe(1) expect((actual.items[0] as FilePreview).name).toBe(expectedFileName) expect(actual.facets).toEqual(expectedFacetsFromFileOnly) - expect(actual.countPerObjectType.dataverses).toBe(0) + expect(actual.countPerObjectType.collections).toBe(0) expect(actual.countPerObjectType.datasets).toBe(0) expect(actual.countPerObjectType.files).toBe(1) }) @@ -1321,4 +1326,349 @@ describe('CollectionsRepository', () => { expect(featuredItemsResponseAfterDeletion).toStrictEqual([]) }) }) + describe('getMyDataCollectionItems', () => { + let testDatasetIds: CreatedDatasetIdentifiers + + const testTextFile1Name = 'test-file-2.txt' + const testSubCollectionAlias = 'collectionsRepositoryMyDataCollection' + const testCollectionName = 'Scientific Research' + beforeAll(async () => { + const createSuperUser = true + const myDataUserApiToken = await createApiTokenViaApi('myDataUser', createSuperUser) + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + myDataUserApiToken + ) + process.env.TEST_API_KEY = myDataUserApiToken + const collectionDTO = createCollectionDTO(testSubCollectionAlias) + // Calling the createCollection use case here + // because createCollectionViaApi does not create the collection in a way + // that is recognized by then MyData endpoint + await createCollection.execute(collectionDTO, testCollectionAlias).catch((error) => { + console.log(error.message) + throw new Error( + `Tests beforeAll(): Error while creating subcollection ${testSubCollectionAlias}` + ) + }) + try { + testDatasetIds = await createDataset.execute( + TestConstants.TEST_NEW_DATASET_DTO, + testSubCollectionAlias + ) + } catch (error: any) { + console.log(error.message) + throw new Error('Tests beforeAll(): Error while creating test dataset') + } + await uploadFileViaApi(testDatasetIds.numericId, testTextFile1Name).catch(() => { + throw new Error(`Tests beforeAll(): Error while uploading file ${testTextFile1Name}`) + }) + }) + + afterAll(async () => { + try { + await deleteUnpublishedDatasetViaApi(testDatasetIds.numericId) + } catch (error) { + throw new Error( + `Tests afterAll(): Error while deleting test dataset ${testDatasetIds.numericId}` + ) + } + try { + await deleteCollectionViaApi(testSubCollectionAlias) + } catch (error) { + throw new Error( + `Tests afterAll(): Error while deleting subcollection ${testSubCollectionAlias}` + ) + } + }) + test('should return collection items given valid roleIds', async () => { + // Give enough time to Solr for indexing + await new Promise((resolve) => setTimeout(resolve, 5000)) + await getCollection.execute(testSubCollectionAlias).then((collection) => { + expect(collection).toBeDefined() + expect(collection.name).toBe('Test Collection') + expect(collection.alias).toBe(testSubCollectionAlias) + expect(collection.description).toBe('test description') + expect(collection.affiliation).toBe('test affiliation') + }) + // TODO: replace this with API call to get the role ids + const roleIds = [1, 2, 3, 4, 5, 6, 7, 8] + const publicationStatuses = [PublicationStatus.Draft, PublicationStatus.Unpublished] + const collectionItemTypes = [ + CollectionItemType.COLLECTION, + CollectionItemType.DATASET, + CollectionItemType.FILE + ] + let actual = await sut.getMyDataCollectionItems( + roleIds, + collectionItemTypes, + publicationStatuses + ) + const actualFilePreview = actual.items.find( + (item) => item.type === CollectionItemType.FILE + ) as FilePreview + const actualDatasetPreview = actual.items.find( + (item) => item.type === CollectionItemType.DATASET + ) as DatasetPreview + const actualCollectionPreview = actual.items.find( + (item) => item.type === CollectionItemType.COLLECTION + ) as CollectionPreview + + const expectedFileMd5 = '799b5c8c5fdcfbd56c3943f7a6c35326' + const expectedDatasetCitationFragment = `Admin, Dataverse; Owner, Dataverse, ${currentYear}, "Dataset created using the createDataset use case"` + const expectedDatasetDescription = 'Dataset created using the createDataset use case' + const expectedFileName = 'test-file-2.txt' + const expectedCollectionsName = 'Test Collection' + + const expectedFacetsAll = [ + { + name: 'publicationStatus', + friendlyName: 'Publication Status', + labels: [ + { name: 'Published', count: 0 }, + { name: 'Unpublished', count: 3 }, + { name: 'Draft', count: 2 }, + { name: 'In Review', count: 0 }, + { name: 'Deaccessioned', count: 0 } + ] + } + ] + expect(actual.items.length).toBe(3) + expect(actual.totalItemCount).toBe(3) + expect(actual.countPerObjectType.collections).toBe(1) + expect(actual.countPerObjectType.datasets).toBe(1) + expect(actual.countPerObjectType.files).toBe(1) + + expect(actualFilePreview.checksum?.type).toBe('MD5') + expect(actualFilePreview.checksum?.value).toBeDefined() + expect(actualFilePreview.datasetCitation).toContain(expectedDatasetCitationFragment) + expect(actualFilePreview.datasetId).toBe(testDatasetIds.numericId) + expect(actualFilePreview.datasetName).toBe(expectedDatasetDescription) + expect(actualFilePreview.datasetPersistentId).toBe(testDatasetIds.persistentId) + expect(actualFilePreview.description).toBe('') + expect(actualFilePreview.fileContentType).toBe('text/plain') + expect(actualFilePreview.fileId).not.toBeUndefined() + expect(actualFilePreview.fileType).toBe('Plain Text') + expect(actualFilePreview.md5).toBe(expectedFileMd5) + expect(actualFilePreview.name).toBe(expectedFileName) + expect(actualFilePreview.publicationStatuses[0]).toBe(PublicationStatus.Unpublished) + expect(actualFilePreview.publicationStatuses[1]).toBe(PublicationStatus.Draft) + expect(actualFilePreview.sizeInBytes).toBe(12) + expect(actualFilePreview.url).not.toBeUndefined() + expect(actualFilePreview.releaseOrCreateDate).not.toBeUndefined() + expect(actualFilePreview.type).toBe(CollectionItemType.FILE) + expect(actualFilePreview.restricted).toBe(false) + expect(actualFilePreview.canDownloadFile).toBe(true) + + expect(actualDatasetPreview.title).toBe(expectedDatasetDescription) + expect(actualDatasetPreview.citation).toContain(expectedDatasetCitationFragment) + expect(actualDatasetPreview.description).toBe('This is the description of the dataset.') + expect(actualDatasetPreview.persistentId).not.toBeUndefined() + expect(actualDatasetPreview.persistentId).not.toBeUndefined() + expect(actualDatasetPreview.publicationStatuses[0]).toBe(PublicationStatus.Unpublished) + expect(actualDatasetPreview.publicationStatuses[1]).toBe(PublicationStatus.Draft) + expect(actualDatasetPreview.versionId).not.toBeUndefined() + expect(actualDatasetPreview.versionInfo.createTime).not.toBeUndefined() + expect(actualDatasetPreview.versionInfo.lastUpdateTime).not.toBeUndefined() + expect(actualDatasetPreview.versionInfo.majorNumber).toBeUndefined() + expect(actualDatasetPreview.versionInfo.minorNumber).toBeUndefined() + expect(actualDatasetPreview.versionInfo.state).toBe('DRAFT') + expect(actualDatasetPreview.parentCollectionAlias).toBe( + 'collectionsRepositoryMyDataCollection' + ) + expect(actualDatasetPreview.parentCollectionName).toBe(expectedCollectionsName) + expect(actualDatasetPreview.type).toBe(CollectionItemType.DATASET) + + expect(actualCollectionPreview.name).toBe(expectedCollectionsName) + expect(actualCollectionPreview.alias).toBe(testSubCollectionAlias) + expect(actualCollectionPreview.description).toBe('test description') + expect(actualCollectionPreview.imageUrl).toBe(undefined) + expect(actualCollectionPreview.parentAlias).toBe(testCollectionAlias) + expect(actualCollectionPreview.parentName).toBe(testCollectionName) + expect(actualCollectionPreview.publicationStatuses[0]).toBe(PublicationStatus.Unpublished) + expect(actualCollectionPreview.releaseOrCreateDate).not.toBeUndefined() + expect(actualCollectionPreview.affiliation).toBe('test affiliation') + expect(actualCollectionPreview.type).toBe(CollectionItemType.COLLECTION) + + expect(actual.facets).toEqual(expectedFacetsAll) + + // Test limit and selectedPage + actual = await sut.getMyDataCollectionItems( + roleIds, + collectionItemTypes, + publicationStatuses, + 1, + 1 + ) + expect((actual.items[1] as FilePreview).name).toBe(expectedFileName) + expect(actual.items.length).toBe(3) + expect(actual.totalItemCount).toBe(3) + + // Test search text + const fileNameSearchText = 'file-2' + + const actualFileResult = await sut.getMyDataCollectionItems( + roleIds, + collectionItemTypes, + publicationStatuses, + undefined, + undefined, + fileNameSearchText + ) + expect(actualFileResult.totalItemCount).toBe(1) + expect((actualFileResult.items[0] as FilePreview).name).toBe(expectedFileName) + expect(actualFileResult.countPerObjectType.collections).toBe(0) + expect(actualFileResult.countPerObjectType.datasets).toBe(0) + expect(actualFileResult.countPerObjectType.files).toBe(1) + + const datasetSearchText = 'This is the description' + + actual = await sut.getMyDataCollectionItems( + roleIds, + collectionItemTypes, + publicationStatuses, + undefined, + undefined, + datasetSearchText + ) + expect(actual.totalItemCount).toBe(1) + expect((actual.items[0] as DatasetPreview).title).toBe(expectedDatasetDescription) + expect(actual.countPerObjectType.collections).toBe(0) + expect(actual.countPerObjectType.datasets).toBe(1) + expect(actual.countPerObjectType.files).toBe(0) + + // Test search text, limit and offset + // TODO: run this test when the limit param has been fixed in the Dataverse API + /* + actual = await sut.getMyDataCollectionItems( + roleIds, + collectionItemTypes, + publicationStatuses, + 1, + 1, + searchTextForDatasetAndCollection + ) + expect(actual.items.length).toBe(1) + expect(actual.totalItemCount).toBe(2) + expect((actual.items[0] as CollectionPreview).name).toBe(expectedCollectionsName) + expect(actual.countPerObjectType.collections).toBe(1) + expect(actual.countPerObjectType.datasets).toBe(1) + expect(actual.countPerObjectType.files).toBe(0) + */ + + // Test type collection + const searchForCollectionType = [CollectionItemType.COLLECTION] + actual = await sut.getMyDataCollectionItems( + roleIds, + searchForCollectionType, + publicationStatuses, + undefined, + undefined + ) + expect(actual.items.length).toBe(1) + expect(actual.totalItemCount).toBe(1) + expect((actual.items[0] as CollectionPreview).name).toBe(expectedCollectionsName) + expect(actual.facets).toEqual([ + { + name: 'publicationStatus', + friendlyName: 'Publication Status', + labels: [ + { name: 'Published', count: 0 }, + { name: 'Unpublished', count: 1 }, + { name: 'Draft', count: 0 }, + { name: 'In Review', count: 0 }, + { name: 'Deaccessioned', count: 0 } + ] + } + ]) + expect(actual.countPerObjectType.collections).toBe(1) + expect(actual.countPerObjectType.datasets).toBe(0) + expect(actual.countPerObjectType.files).toBe(0) + + // Test type dataset + const searchDatasetType = [CollectionItemType.DATASET] + actual = await sut.getMyDataCollectionItems( + roleIds, + searchDatasetType, + publicationStatuses, + undefined, + undefined + ) + expect(actual.items.length).toBe(1) + expect(actual.totalItemCount).toBe(1) + expect((actual.items[0] as DatasetPreview).title).toBe(expectedDatasetDescription) + expect(actual.facets).toEqual([ + { + name: 'publicationStatus', + friendlyName: 'Publication Status', + labels: [ + { name: 'Published', count: 0 }, + { name: 'Unpublished', count: 1 }, + { name: 'Draft', count: 1 }, + { name: 'In Review', count: 0 }, + { name: 'Deaccessioned', count: 0 } + ] + } + ]) + expect(actual.countPerObjectType.collections).toBe(0) + expect(actual.countPerObjectType.datasets).toBe(1) + expect(actual.countPerObjectType.files).toBe(0) + + // Test type file + const searchFileType = [CollectionItemType.FILE] + + actual = await sut.getMyDataCollectionItems( + roleIds, + searchFileType, + publicationStatuses, + undefined, + undefined + ) + expect(actual.items.length).toBe(1) + expect(actual.totalItemCount).toBe(1) + expect((actual.items[0] as FilePreview).name).toBe(expectedFileName) + expect(actual.facets).toEqual([ + { + name: 'publicationStatus', + friendlyName: 'Publication Status', + labels: [ + { name: 'Published', count: 0 }, + { name: 'Unpublished', count: 1 }, + { name: 'Draft', count: 1 }, + { name: 'In Review', count: 0 }, + { name: 'Deaccessioned', count: 0 } + ] + } + ]) + + expect(actual.countPerObjectType.collections).toBe(0) + expect(actual.countPerObjectType.datasets).toBe(0) + expect(actual.countPerObjectType.files).toBe(1) + + // Test multiple types + const searchForMultiTypes = [CollectionItemType.FILE, CollectionItemType.COLLECTION] + actual = await sut.getMyDataCollectionItems( + roleIds, + searchForMultiTypes, + publicationStatuses, + undefined, + undefined + ) + expect(actual.items.length).toBe(2) + expect(actual.totalItemCount).toBe(2) + expect((actual.items[0] as FilePreview).name).toBe(expectedFileName) + expect((actual.items[1] as CollectionPreview).name).toBe(expectedCollectionsName) + expect(actual.countPerObjectType.collections).toBe(1) + expect(actual.countPerObjectType.datasets).toBe(0) + expect(actual.countPerObjectType.files).toBe(1) + }) + + test('should return error when role, type and publication status params are empty', async () => { + const expectedError = new ReadError('No results. Please select at least one Role.') + + await expect( + sut.getMyDataCollectionItems([], [], [], 0, 0, undefined, undefined) + ).rejects.toThrow(expectedError) + }) + }) }) diff --git a/test/testHelpers/TestConstants.ts b/test/testHelpers/TestConstants.ts index 4a2adfb8..192781b2 100644 --- a/test/testHelpers/TestConstants.ts +++ b/test/testHelpers/TestConstants.ts @@ -2,6 +2,7 @@ import { DatasetDTO } from '../../src/datasets/domain/dtos/DatasetDTO' export class TestConstants { static readonly TEST_API_URL = 'http://localhost:8080/api/v1' + static readonly BUILTIN_USER_KEY = 'builtInS3kretKey' static readonly TEST_DUMMY_API_KEY = 'dummyApiKey' static readonly TEST_DUMMY_PERSISTENT_ID = 'doi:11.1111/AA1/AA1AAA' static readonly TEST_BEARER_TOKEN_LOCAL_STORAGE_KEY = 'DV_TOKEN' diff --git a/test/testHelpers/collections/collectionHelper.ts b/test/testHelpers/collections/collectionHelper.ts index 8ccde481..b19b668f 100644 --- a/test/testHelpers/collections/collectionHelper.ts +++ b/test/testHelpers/collections/collectionHelper.ts @@ -90,7 +90,6 @@ export async function createCollectionViaApi( if (parentCollectionAlias == undefined) { parentCollectionAlias = ':root' } - return await axios .post( `${TestConstants.TEST_API_URL}/dataverses/${parentCollectionAlias}`, diff --git a/test/testHelpers/users/apiTokenHelper.ts b/test/testHelpers/users/apiTokenHelper.ts index 36df6729..b99608b1 100644 --- a/test/testHelpers/users/apiTokenHelper.ts +++ b/test/testHelpers/users/apiTokenHelper.ts @@ -1,7 +1,10 @@ import axios from 'axios' import { TestConstants } from '../TestConstants' -export const createApiTokenViaApi = async (userName: string): Promise => { +export const createApiTokenViaApi = async ( + userName: string, + createSuperUser = false +): Promise => { try { await axios.post( `${TestConstants.TEST_API_URL}/builtin-users?key=burrito&password=${userName}`, @@ -17,11 +20,14 @@ export const createApiTokenViaApi = async (userName: string): Promise => } } ) - return axios + const token = await axios .get(`${TestConstants.TEST_API_URL}/builtin-users/${userName}/api-token?password=${userName}`) .then((response) => response.data.data.message) + if (createSuperUser) { + await axios.put(`${TestConstants.TEST_API_URL}/admin/superuser/${userName}`, 'true') + } + return token } catch (error) { - console.log(error) throw new Error(`Error while creating API token`) } } diff --git a/test/testHelpers/users/builtinUserApiHelper.ts b/test/testHelpers/users/builtinUserApiHelper.ts new file mode 100644 index 00000000..d91e08a3 --- /dev/null +++ b/test/testHelpers/users/builtinUserApiHelper.ts @@ -0,0 +1,27 @@ +import axios from 'axios' +import { TestConstants } from '../TestConstants' + +export const createBuiltInUser = async (userName: string): Promise => { + try { + await axios.post( + `${TestConstants.TEST_API_URL}/builtin-users?key=burrito&password=${userName}`, + JSON.stringify({ + userName: userName, + firstName: 'John', + lastName: 'Doe', + email: `${userName}@test.com` + }), + { + headers: { + 'Content-Type': 'application/json' + } + } + ) + return axios + .get(`${TestConstants.TEST_API_URL}/builtin-users/${userName}/api-token?password=${userName}`) + .then((response) => response.data.data.message) + } catch (error) { + console.log(error) + throw new Error(`Error while creating API token`) + } +} diff --git a/test/unit/collections/CollectionsRepository.test.ts b/test/unit/collections/CollectionsRepository.test.ts index d95075ed..e0092222 100644 --- a/test/unit/collections/CollectionsRepository.test.ts +++ b/test/unit/collections/CollectionsRepository.test.ts @@ -375,7 +375,7 @@ describe('CollectionsRepository', () => { const testTotalCount = 2 const testFacets = createCollectionItemsFacetsModel() const testCountPerObjectType = { - dataverses: 0, + collections: 0, datasets: 1, files: 1 } diff --git a/test/unit/collections/GetCollectionItems.test.ts b/test/unit/collections/GetCollectionItems.test.ts index a1eb9936..cf2e7448 100644 --- a/test/unit/collections/GetCollectionItems.test.ts +++ b/test/unit/collections/GetCollectionItems.test.ts @@ -19,7 +19,7 @@ describe('execute', () => { const testTotalCount = 3 const testFacets = createCollectionItemsFacetsModel() const testCountPerObjectType = { - dataverses: 1, + collections: 1, datasets: 1, files: 1 } diff --git a/test/unit/collections/GetMyDataCollectionItems.test.ts b/test/unit/collections/GetMyDataCollectionItems.test.ts new file mode 100644 index 00000000..edbb3e04 --- /dev/null +++ b/test/unit/collections/GetMyDataCollectionItems.test.ts @@ -0,0 +1,137 @@ +import { ICollectionsRepository } from '../../../src/collections/domain/repositories/ICollectionsRepository' +import { GetMyDataCollectionItems } from '../../../src/collections/domain/useCases/GetMyDataCollectionItems' +import { ReadError } from '../../../src' +import { CollectionItemType } from '../../../src/collections/domain/models/CollectionItemType' +import { createDatasetPreviewModel } from '../../testHelpers/datasets/datasetPreviewHelper' +import { createFilePreviewModel } from '../../testHelpers/files/filePreviewHelper' +import { createCollectionPreviewModel } from '../../testHelpers/collections/collectionPreviewHelper' +import { CollectionItemsFacet } from '../../../src/collections/domain/models/CollectionItemSubset' +import { PublicationStatus } from '../../../src/core/domain/models/PublicationStatus' +import { CollectionItemSubset } from '../../../src/collections/domain/models/CollectionItemSubset' +describe('GetMyDataCollectionItems', () => { + let collectionRepositoryStub: ICollectionsRepository + let testGetMyDataCollectionItems: GetMyDataCollectionItems + + const testRoleIds = [1, 2] + const testCollectionItemTypes = [CollectionItemType.DATASET, CollectionItemType.FILE] + const testPublishingStatuses = [ + PublicationStatus.Published, + PublicationStatus.Draft, + PublicationStatus.Unpublished + ] + const testLimit = 10 + const testPage = 1 + const testSearchText = 'test' + const testOtherUserName = 'testUser' + const testItems = [ + createCollectionPreviewModel(), + createDatasetPreviewModel(), + createFilePreviewModel() + ] + const testFacets = [ + { + name: 'publicationStatus', + friendlyName: 'Publication Status', + labels: [ + { + name: 'Published', + count: 10 + }, + { + name: 'Draft', + count: 5 + }, + { + name: 'Unpublished', + count: 15 + } + ] + } + ] as CollectionItemsFacet[] + const testItemSubset: CollectionItemSubset = { + items: testItems, + facets: testFacets, + totalItemCount: 30, + countPerObjectType: { collections: 10, datasets: 15, files: 5 } + } + beforeEach(() => { + collectionRepositoryStub = {} as ICollectionsRepository + testGetMyDataCollectionItems = new GetMyDataCollectionItems(collectionRepositoryStub) + }) + + test('should return item subset on repository success', async () => { + collectionRepositoryStub.getMyDataCollectionItems = jest.fn().mockResolvedValue(testItemSubset) + + const actual = await testGetMyDataCollectionItems.execute( + testRoleIds, + testCollectionItemTypes, + testPublishingStatuses, + testLimit, + testPage, + testSearchText + ) + + expect(actual).toEqual(testItemSubset) + }) + + test('should return error result on repository error', async () => { + collectionRepositoryStub.getMyDataCollectionItems = jest.fn().mockRejectedValue(new ReadError()) + + await expect( + testGetMyDataCollectionItems.execute( + testRoleIds, + testCollectionItemTypes, + testPublishingStatuses, + testLimit, + testPage, + testSearchText + ) + ).rejects.toThrow(ReadError) + }) + + test('should handle required parameters', async () => { + collectionRepositoryStub.getMyDataCollectionItems = jest.fn().mockResolvedValue(testItemSubset) + + const actual = await testGetMyDataCollectionItems.execute( + testRoleIds, + testCollectionItemTypes, + testPublishingStatuses + ) + + expect(collectionRepositoryStub.getMyDataCollectionItems).toHaveBeenCalledWith( + testRoleIds, + testCollectionItemTypes, + testPublishingStatuses, + undefined, + undefined, + undefined, + undefined + ) + expect(actual).toEqual(testItemSubset) + }) + + test('should handle all parameters', async () => { + collectionRepositoryStub.getMyDataCollectionItems = jest.fn().mockResolvedValue(testItemSubset) + + const actual = await testGetMyDataCollectionItems.execute( + testRoleIds, + testCollectionItemTypes, + testPublishingStatuses, + testLimit, + testPage, + testSearchText, + testOtherUserName + ) + + expect(collectionRepositoryStub.getMyDataCollectionItems).toHaveBeenCalledWith( + testRoleIds, + testCollectionItemTypes, + testPublishingStatuses, + testLimit, + testPage, + testSearchText, + testOtherUserName + ) + expect(actual).toEqual(testItemSubset) + }) +})