diff --git a/.github/workflows/copy_labels.yml b/.github/workflows/copy_labels.yml new file mode 100644 index 00000000..b4c99b06 --- /dev/null +++ b/.github/workflows/copy_labels.yml @@ -0,0 +1,26 @@ +name: Copy labels from issue to pull request + +on: + pull_request: + types: [opened] + +jobs: + copy-labels: + runs-on: ubuntu-latest + name: Copy labels from linked issues + steps: + - name: copy-labels + uses: michalvankodev/copy-issue-labels@v1.3.0 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + labels-to-exclude: | + Size: 3 + Size: 10 + Size: 20 + Size: 33 + Size: 80 + Original Size: 3 + Original Size: 10 + Original Size: 20 + Original Size: 33 + Original Size: 80 diff --git a/docs/useCases.md b/docs/useCases.md index 26317d1c..e0c8e44d 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -19,6 +19,7 @@ The different use cases currently available in the package are classified below, - [Create a Collection](#create-a-collection) - [Update a Collection](#update-a-collection) - [Publish a Collection](#publish-a-collection) + - [Delete a Collection](#delete-a-collection) - [Update Collection Featured Items](#update-collection-featured-items) - [Delete Collection Featured Items](#delete-collection-featured-items) - [Datasets](#Datasets) @@ -315,6 +316,24 @@ The `collectionIdOrAlias` is a generic collection identifier, which can be eithe _See [use case](../src/collections/domain/useCases/PublishCollection.ts)_ definition. +### Delete a Collection + +```typescript +import { deleteCollection } from '@iqss/dataverse-client-javascript' + +/* ... */ + +const collectionIdOrAlias = 12345 + +deleteCollection.execute(collectionIdOrAlias) + +/* ... */ +``` + +The `collectionIdOrAlias` is a generic collection identifier, which can be either a string (for queries by CollectionAlias), or a number (for queries by CollectionId). + +_See [use case](../src/collections/domain/useCases/DeleteCollection.ts)_ definition. + #### Update Collection Featured Items Updates all featured items, given a collection identifier and a CollectionFeaturedItemsDTO. diff --git a/src/collections/domain/repositories/ICollectionsRepository.ts b/src/collections/domain/repositories/ICollectionsRepository.ts index 85f6c42f..be784204 100644 --- a/src/collections/domain/repositories/ICollectionsRepository.ts +++ b/src/collections/domain/repositories/ICollectionsRepository.ts @@ -14,6 +14,7 @@ export interface ICollectionsRepository { parentCollectionId: number | string ): Promise publishCollection(collectionIdOrAlias: number | string): Promise + deleteCollection(collectionIdOrAlias: number | string): Promise getCollectionFacets(collectionIdOrAlias: number | string): Promise getCollectionUserPermissions( collectionIdOrAlias: number | string diff --git a/src/collections/domain/useCases/DeleteCollection.ts b/src/collections/domain/useCases/DeleteCollection.ts new file mode 100644 index 00000000..0148c307 --- /dev/null +++ b/src/collections/domain/useCases/DeleteCollection.ts @@ -0,0 +1,20 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { ICollectionsRepository } from '../repositories/ICollectionsRepository' + +export class DeleteCollection implements UseCase { + private collectionsRepository: ICollectionsRepository + + constructor(collectionsRepository: ICollectionsRepository) { + this.collectionsRepository = collectionsRepository + } + + /** + * Deletes the Dataverse collection whose database ID or alias is given: + * + * @param {number | string} [collectionIdOrAlias] - A generic collection identifier, which can be either a string (for queries by CollectionAlias), or a number (for queries by CollectionId) + * @returns {Promise} -This method does not return anything upon successful completion. + */ + async execute(collectionIdOrAlias: number | string): Promise { + return await this.collectionsRepository.deleteCollection(collectionIdOrAlias) + } +} diff --git a/src/collections/index.ts b/src/collections/index.ts index e3a50dce..f65f121a 100644 --- a/src/collections/index.ts +++ b/src/collections/index.ts @@ -9,6 +9,7 @@ import { GetCollectionFeaturedItems } from './domain/useCases/GetCollectionFeatu import { CollectionsRepository } from './infra/repositories/CollectionsRepository' import { UpdateCollectionFeaturedItems } from './domain/useCases/UpdateCollectionFeaturedItems' import { DeleteCollectionFeaturedItems } from './domain/useCases/DeleteCollectionFeaturedItems' +import { DeleteCollection } from './domain/useCases/DeleteCollection' const collectionsRepository = new CollectionsRepository() @@ -22,6 +23,7 @@ const updateCollection = new UpdateCollection(collectionsRepository) const getCollectionFeaturedItems = new GetCollectionFeaturedItems(collectionsRepository) const updateCollectionFeaturedItems = new UpdateCollectionFeaturedItems(collectionsRepository) const deleteCollectionFeaturedItems = new DeleteCollectionFeaturedItems(collectionsRepository) +const deleteCollection = new DeleteCollection(collectionsRepository) export { getCollection, @@ -33,7 +35,8 @@ export { updateCollection, getCollectionFeaturedItems, updateCollectionFeaturedItems, - deleteCollectionFeaturedItems + deleteCollectionFeaturedItems, + deleteCollection } export { Collection, CollectionInputLevel } from './domain/models/Collection' export { CollectionFacet } from './domain/models/CollectionFacet' diff --git a/src/collections/infra/repositories/CollectionsRepository.ts b/src/collections/infra/repositories/CollectionsRepository.ts index 239313b6..b1b1820e 100644 --- a/src/collections/infra/repositories/CollectionsRepository.ts +++ b/src/collections/infra/repositories/CollectionsRepository.ts @@ -102,6 +102,7 @@ export class CollectionsRepository extends ApiRepository implements ICollections throw error }) } + public async publishCollection(collectionIdOrAlias: string | number): Promise { return this.doPost( `/${this.collectionsResourceName}/${collectionIdOrAlias}/actions/:publish`, @@ -113,6 +114,14 @@ export class CollectionsRepository extends ApiRepository implements ICollections }) } + public async deleteCollection(collectionIdOrAlias: number | string): Promise { + return this.doDelete(`/${this.collectionsResourceName}/${collectionIdOrAlias}`) + .then(() => undefined) + .catch((error) => { + throw error + }) + } + public async getCollectionUserPermissions( collectionIdOrAlias: number | string ): Promise { diff --git a/test/functional/collections/DeleteCollection.test.ts b/test/functional/collections/DeleteCollection.test.ts new file mode 100644 index 00000000..65862023 --- /dev/null +++ b/test/functional/collections/DeleteCollection.test.ts @@ -0,0 +1,55 @@ +import { + ApiConfig, + ReadError, + WriteError, + createCollection, + deleteCollection, + getCollection +} from '../../../src' +import { TestConstants } from '../../testHelpers/TestConstants' +import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' +import { createCollectionDTO } from '../../testHelpers/collections/collectionHelper' + +describe('execute', () => { + beforeEach(async () => { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + }) + + test('should successfully delete a collection', async () => { + const testCollectionAlias = 'deleteCollection-functional-test' + const testNewCollection = createCollectionDTO(testCollectionAlias) + await createCollection.execute(testNewCollection) + + expect.assertions(1) + try { + await deleteCollection.execute(testCollectionAlias) + } catch (error) { + throw new Error('Collection should be deleted') + } finally { + const expectedError = new ReadError( + `[404] Can't find dataverse with identifier='${testCollectionAlias}'` + ) + await expect(getCollection.execute(testCollectionAlias)).rejects.toThrow(expectedError) + } + }) + + test('should throw an error when the collection does not exist', async () => { + expect.assertions(2) + let writeError: WriteError | undefined = undefined + try { + await deleteCollection.execute(TestConstants.TEST_DUMMY_COLLECTION_ID) + throw new Error('Use case should throw an error') + } catch (error) { + writeError = error as WriteError + } finally { + expect(writeError).toBeInstanceOf(WriteError) + expect(writeError?.message).toEqual( + `There was an error when writing the resource. Reason was: [404] Can't find dataverse with identifier='${TestConstants.TEST_DUMMY_COLLECTION_ID}'` + ) + } + }) +}) diff --git a/test/integration/collections/CollectionsRepository.test.ts b/test/integration/collections/CollectionsRepository.test.ts index b8f43967..99d45064 100644 --- a/test/integration/collections/CollectionsRepository.test.ts +++ b/test/integration/collections/CollectionsRepository.test.ts @@ -47,6 +47,7 @@ describe('CollectionsRepository', () => { const sut: CollectionsRepository = new CollectionsRepository() let testCollectionId: number const currentYear = new Date().getFullYear() + beforeAll(async () => { ApiConfig.init( TestConstants.TEST_API_URL, @@ -146,6 +147,7 @@ describe('CollectionsRepository', () => { expect(createdCollection.name).toBe(newCollectionDTO.name) }) }) + describe('createCollection', () => { const testCreateCollectionAlias1 = 'createCollection-test-1' const testCreateCollectionAlias2 = 'createCollection-test-2' @@ -247,6 +249,35 @@ describe('CollectionsRepository', () => { }) }) + describe('deleteCollection', () => { + test('should delete collection successfully', async () => { + const collectionAlias = 'deleteCollection-test' + const collectionDTO = createCollectionDTO(collectionAlias) + await sut.createCollection(collectionDTO) + + const createdCollection = await sut.getCollection(collectionAlias) + + expect(createdCollection.alias).toBe(collectionAlias) + + const deleteResult = await sut.deleteCollection(collectionAlias) + const expectedError = new ReadError( + `[404] Can't find dataverse with identifier='${collectionAlias}'` + ) + + expect(deleteResult).toBeUndefined() + await expect(sut.getCollection(collectionAlias)).rejects.toThrow(expectedError) + }) + + test('should return error when collection does not exist', async () => { + const expectedError = new WriteError( + `[404] Can't find dataverse with identifier='${TestConstants.TEST_DUMMY_COLLECTION_ID}'` + ) + await expect(sut.deleteCollection(TestConstants.TEST_DUMMY_COLLECTION_ID)).rejects.toThrow( + expectedError + ) + }) + }) + describe('getCollectionFacets', () => { test('should return collection facets given a valid collection alias', async () => { const actual = await sut.getCollectionFacets(testCollectionAlias) diff --git a/test/unit/collections/CollectionsRepository.test.ts b/test/unit/collections/CollectionsRepository.test.ts index 0c4bfb94..3f1ae325 100644 --- a/test/unit/collections/CollectionsRepository.test.ts +++ b/test/unit/collections/CollectionsRepository.test.ts @@ -569,4 +569,87 @@ describe('CollectionsRepository', () => { expect(error).toBeInstanceOf(Error) }) }) + + describe('deleteCollection', () => { + const deleteTestCollectionAlias = 'deleteCollection-unit-test' + const deleteTestCollectionId = 123 + + describe('by numeric id', () => { + const expectedApiEndpoint = `${TestConstants.TEST_API_URL}/dataverses/${deleteTestCollectionId}` + + test('should delete a collection when providing a valid id', async () => { + jest.spyOn(axios, 'delete').mockResolvedValue({ data: { status: 'OK' } }) + + // API Key auth + await sut.deleteCollection(deleteTestCollectionId) + + expect(axios.delete).toHaveBeenCalledWith( + expectedApiEndpoint, + TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_API_KEY + ) + + // Session cookie auth + ApiConfig.init(TestConstants.TEST_API_URL, DataverseApiAuthMechanism.SESSION_COOKIE) + + await sut.deleteCollection(deleteTestCollectionId) + + expect(axios.delete).toHaveBeenCalledWith( + expectedApiEndpoint, + TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_SESSION_COOKIE + ) + }) + + test('should return error result on error response', async () => { + jest.spyOn(axios, 'delete').mockRejectedValue(TestConstants.TEST_ERROR_RESPONSE) + + let error = undefined as unknown as WriteError + await sut.deleteCollection(deleteTestCollectionId).catch((e) => (error = e)) + + expect(axios.delete).toHaveBeenCalledWith( + expectedApiEndpoint, + TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_API_KEY + ) + expect(error).toBeInstanceOf(WriteError) + }) + }) + + describe('by alias id', () => { + const expectedApiEndpoint = `${TestConstants.TEST_API_URL}/dataverses/${deleteTestCollectionAlias}` + + test('should delete a collection when providing a valid alias', async () => { + jest.spyOn(axios, 'delete').mockResolvedValue({ data: { status: 'OK' } }) + + // API Key auth + await sut.deleteCollection(deleteTestCollectionAlias) + + expect(axios.delete).toHaveBeenCalledWith( + expectedApiEndpoint, + TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_API_KEY + ) + + // Session cookie auth + ApiConfig.init(TestConstants.TEST_API_URL, DataverseApiAuthMechanism.SESSION_COOKIE) + + await sut.deleteCollection(deleteTestCollectionAlias) + + expect(axios.delete).toHaveBeenCalledWith( + expectedApiEndpoint, + TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_SESSION_COOKIE + ) + }) + + test('should return error result on error response', async () => { + jest.spyOn(axios, 'delete').mockRejectedValue(TestConstants.TEST_ERROR_RESPONSE) + + let error = undefined as unknown as WriteError + await sut.deleteCollection(deleteTestCollectionAlias).catch((e) => (error = e)) + + expect(axios.delete).toHaveBeenCalledWith( + expectedApiEndpoint, + TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_API_KEY + ) + expect(error).toBeInstanceOf(WriteError) + }) + }) + }) }) diff --git a/test/unit/collections/DeleteCollection.test.ts b/test/unit/collections/DeleteCollection.test.ts new file mode 100644 index 00000000..52cff4d9 --- /dev/null +++ b/test/unit/collections/DeleteCollection.test.ts @@ -0,0 +1,23 @@ +import { ICollectionsRepository } from '../../../src/collections/domain/repositories/ICollectionsRepository' +import { WriteError } from '../../../src' +import { DeleteCollection } from '../../../src/collections/domain/useCases/DeleteCollection' + +describe('execute', () => { + test('should return undefined on repository success', async () => { + const collectionRepositoryStub: ICollectionsRepository = {} as ICollectionsRepository + collectionRepositoryStub.deleteCollection = jest.fn().mockResolvedValue(undefined) + const testDeleteCollection = new DeleteCollection(collectionRepositoryStub) + + const actual = await testDeleteCollection.execute(1) + + expect(actual).toEqual(undefined) + }) + + test('should return error result on repository error', async () => { + const collectionRepositoryStub: ICollectionsRepository = {} as ICollectionsRepository + collectionRepositoryStub.deleteCollection = jest.fn().mockRejectedValue(new WriteError()) + const testDeleteCollection = new DeleteCollection(collectionRepositoryStub) + + await expect(testDeleteCollection.execute(1)).rejects.toThrow(WriteError) + }) +})