diff --git a/CHANGELOG.md b/CHANGELOG.md index ad4a912c..422b8bb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,8 +15,10 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel - Files: Added `getFileCitationByFormat` use case, repository method, and `FileCitationFormat` enum to support Dataverse file citation exports in `EndNote`, `RIS`, `BibTeX`, `CSL`, and `Internal` formats. - Datasets: Added `getDatasetReviews` use case and repository method to support Dataverse endpoint `GET /datasets/{identifier}/reviews`, for retrieving review datasets associated with a dataset by persistent id or numeric id. - Datasets: Added `exportDatasetMetadata` use case, repository method, and `ExportedDatasetMetadata` response type to support exporting dataset metadata by numeric id or persistent id through Dataverse endpoint `GET /datasets/export`. +- Datasets: Added `assignRoleOnDataset` and `unassignRoleOnDataset` use cases. - Collections: Added `allowedDatasetTypes` field to the [Collection](./src/collections/domain/models/Collection.ts) model. This field is optional and only populated the feature is enabled on the installation and configured on the collection. - Collections: Added theme information when retrieving a collection using `getCollection`. +- Collections: Added `assignRoleOnCollection` and `unassignRoleOnCollection` use cases. ### Changed diff --git a/docs/useCases.md b/docs/useCases.md index 2df0b987..079a9c8a 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -29,6 +29,8 @@ The different use cases currently available in the package are classified below, - [Update Collection Featured Items](#update-collection-featured-items) - [Delete Collection Featured Items](#delete-collection-featured-items) - [Delete a Collection Featured Item](#delete-a-collection-featured-item) + - [Assign a Role on a Collection](#assign-a-role-on-a-collection) + - [Unassign a Role on a Collection](#unassign-a-role-on-a-collection) - [Templates](#Templates) - [Templates read use cases](#templates-read-use-cases) - [Get a Template](#get-a-template) @@ -75,6 +77,8 @@ The different use cases currently available in the package are classified below, - [Link Dataset Type with Metadata Blocks](#link-dataset-type-with-metadata-blocks) - [Set Available Licenses For Dataset Type](#set-available-licenses-for-dataset-type) - [Delete a Dataset Type](#delete-a-dataset-type) + - [Assign a Role on a Dataset](#assign-a-role-on-a-dataset) + - [Unassign a Role on a Dataset](#unassign-a-role-on-a-dataset) - [Files](#Files) - [Files read use cases](#files-read-use-cases) - [Get a File](#get-a-file) @@ -732,6 +736,49 @@ deleteCollectionFeaturedItem.execute(featuredItemId) _See [use case](../src/collections/domain/useCases/DeleteCollectionFeaturedItem.ts)_ definition. +#### Assign a Role on a Collection + +Assigns a role on a collection, given a collection identifier, a role assignee and a role alias. + +##### Example call: + +```typescript +import { assignRoleOnCollection } from '@iqss/dataverse-client-javascript' + +/* ... */ + +const collectionIdOrAlias = 12345 +const roleAssignee = "@myUser" +const roleAlias = "curator" + +assignRoleOnCollection.execute(collectionIdOrAlias, roleAssignee, roleAlias) + +/* ... */ +``` + +_See [use case](../src/collections/domain/useCases/AssignRoleOnCollection.ts)_ definition. + +#### Unassign a Role on a Collection + +Unassigns a role on a collection, given a collection identifier and a role assignment identifier. + +##### Example call: + +```typescript +import { unassignRoleOnCollection } from '@iqss/dataverse-client-javascript' + +/* ... */ + +const collectionIdOrAlias = 12345 +const roleAssignmentId = 67890 + +unassignRoleOnCollection.execute(collectionIdOrAlias, roleAssignmentId) + +/* ... */ +``` + +_See [use case](../src/collections/domain/useCases/UnassignRoleOnCollection.ts)_ definition. + ## Templates ### Templates Read Use Cases @@ -1890,6 +1937,49 @@ _See [use case](../src/datasets/domain/useCases/GetDatasetUploadLimits.ts) imple If the backend does not define any quota limits for the dataset, the returned object can be empty (`{}`). +#### Assign a Role on a Dataset + +Assigns a role on a dataset, given a dataset identifier, a role assignee and a role alias. + +##### Example call: + +```typescript +import { assignRoleOnDataset } from '@iqss/dataverse-client-javascript' + +/* ... */ + +const datasetId = 1 +const roleAssignee = "@myUser" +const roleAlias = "curator" + +assignRoleOnDataset.execute(datasetId, roleAssignee, roleAlias) + +/* ... */ +``` + +_See [use case](../src/datasets/domain/useCases/AssignRoleOnDataset.ts)_ definition. + +#### Unassign a Role on a Dataset + +Unassigns a role on a dataset, given a dataset identifier and a role assignment identifier. + +##### Example call: + +```typescript +import { unassignRoleOnDataset } from '@iqss/dataverse-client-javascript' + +/* ... */ + +const datasetId = 1 +const roleAssignmentId = 67890 + +unassignRoleOnDataset.execute(datasetId, roleAssignmentId) + +/* ... */ +``` + +_See [use case](../src/datasets/domain/useCases/UnassignRoleOnDataset.ts)_ definition. + ## Files ### Files read use cases diff --git a/src/collections/domain/repositories/ICollectionsRepository.ts b/src/collections/domain/repositories/ICollectionsRepository.ts index ed6cf4ff..97775d7c 100644 --- a/src/collections/domain/repositories/ICollectionsRepository.ts +++ b/src/collections/domain/repositories/ICollectionsRepository.ts @@ -39,6 +39,15 @@ export interface ICollectionsRepository { getCollectionUserPermissions( collectionIdOrAlias: number | string ): Promise + assignRoleOnCollection( + collectionIdOrAlias: number | string, + roleAssignee: string, + roleAlias: string + ): Promise + unassignRoleOnCollection( + collectionIdOrAlias: number | string, + roleAssignmentId: number + ): Promise getCollectionItems( collectionId?: string, limit?: number, diff --git a/src/collections/domain/useCases/AssignRoleOnCollection.ts b/src/collections/domain/useCases/AssignRoleOnCollection.ts new file mode 100644 index 00000000..e688677d --- /dev/null +++ b/src/collections/domain/useCases/AssignRoleOnCollection.ts @@ -0,0 +1,28 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { ICollectionsRepository } from '../repositories/ICollectionsRepository' +import { ROOT_COLLECTION_ID } from '../models/Collection' + +export class AssignRoleOnCollection implements UseCase { + private collectionsRepository: ICollectionsRepository + + constructor(collectionsRepository: ICollectionsRepository) { + this.collectionsRepository = collectionsRepository + } + + /** + * Assigns a new role to someone on the given Dataverse collection. + * + * @param {number | string} [collectionIdOrAlias = ':root'] - A generic collection identifier, which can be either a string (for queries by CollectionAlias), or a number (for queries by CollectionId) + * If this parameter is not set, the default value is: ':root' + * @param {string} [roleAssignee] - To whom the role should be assigned + * @param {string} [roleAlias] - The alias of the role to be assigned + * @returns {Promise} + */ + async execute( + collectionIdOrAlias: number | string = ROOT_COLLECTION_ID, + roleAssignee: string, + roleAlias: string + ): Promise { + return await this.collectionsRepository.assignRoleOnCollection(collectionIdOrAlias, roleAssignee, roleAlias) + } +} diff --git a/src/collections/domain/useCases/UnassignRoleOnCollection.ts b/src/collections/domain/useCases/UnassignRoleOnCollection.ts new file mode 100644 index 00000000..9a752a78 --- /dev/null +++ b/src/collections/domain/useCases/UnassignRoleOnCollection.ts @@ -0,0 +1,26 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { ICollectionsRepository } from '../repositories/ICollectionsRepository' +import { ROOT_COLLECTION_ID } from '../models/Collection' + +export class UnassignRoleOnCollection implements UseCase { + private collectionsRepository: ICollectionsRepository + + constructor(collectionsRepository: ICollectionsRepository) { + this.collectionsRepository = collectionsRepository + } + + /** + * Deletes the given role assignment on the given Dataverse collection. + * + * @param {number | string} [collectionIdOrAlias = ':root'] - A generic collection identifier, which can be either a string (for queries by CollectionAlias), or a number (for queries by CollectionId) + * If this parameter is not set, the default value is: ':root' + * @param {number} [roleAssignmentId] - To numeric identifier of the role assignment + * @returns {Promise} + */ + async execute( + collectionIdOrAlias: number | string = ROOT_COLLECTION_ID, + roleAssignmentId: number + ): Promise { + return await this.collectionsRepository.unassignRoleOnCollection(collectionIdOrAlias, roleAssignmentId) + } +} diff --git a/src/collections/index.ts b/src/collections/index.ts index b62ed9e8..efd366c8 100644 --- a/src/collections/index.ts +++ b/src/collections/index.ts @@ -20,6 +20,8 @@ import { GetCollectionsForLinking } from './domain/useCases/GetCollectionsForLin import { SetCollectionStorageDriver } from './domain/useCases/SetCollectionStorageDriver' import { DeleteCollectionStorageDriver } from './domain/useCases/DeleteCollectionStorageDriver' import { GetAllowedCollectionStorageDrivers } from './domain/useCases/GetAllowedCollectionStorageDrivers' +import { AssignRoleOnCollection } from './domain/useCases/AssignRoleOnCollection' +import { UnassignRoleOnCollection } from './domain/useCases/UnassignRoleOnCollection' const collectionsRepository = new CollectionsRepository() @@ -46,6 +48,8 @@ const deleteCollectionStorageDriver = new DeleteCollectionStorageDriver(collecti const getAllowedCollectionStorageDrivers = new GetAllowedCollectionStorageDrivers( collectionsRepository ) +const assignRoleOnCollection = new AssignRoleOnCollection(collectionsRepository) +const unassignRoleOnCollection = new UnassignRoleOnCollection(collectionsRepository) export { getCollection, @@ -68,7 +72,9 @@ export { getCollectionsForLinking, setCollectionStorageDriver, deleteCollectionStorageDriver, - getAllowedCollectionStorageDrivers + getAllowedCollectionStorageDrivers, + assignRoleOnCollection, + unassignRoleOnCollection } export { Collection, CollectionInputLevel, CollectionTheme } 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 ccf22729..217c0669 100644 --- a/src/collections/infra/repositories/CollectionsRepository.ts +++ b/src/collections/infra/repositories/CollectionsRepository.ts @@ -225,6 +225,32 @@ export class CollectionsRepository extends ApiRepository implements ICollections }) } + public async assignRoleOnCollection( + collectionIdOrAlias: number | string, + roleAssignee: string, + roleAlias: string + ): Promise { + return this.doPost(`/${this.collectionsResourceName}/${collectionIdOrAlias}/assignments`, { + assignee: roleAssignee, + role: roleAlias + }) + .then(() => undefined) + .catch((error) => { + throw error + }) + } + + public async unassignRoleOnCollection( + collectionIdOrAlias: number | string, + roleAssignmentId: number + ): Promise { + return this.doDelete(`/${this.collectionsResourceName}/${collectionIdOrAlias}/assignments/${roleAssignmentId}`) + .then(() => undefined) + .catch((error) => { + throw error + }) + } + public async getCollectionItems( collectionId?: string, limit?: number, diff --git a/src/datasets/domain/repositories/IDatasetsRepository.ts b/src/datasets/domain/repositories/IDatasetsRepository.ts index a50ad2ff..07831201 100644 --- a/src/datasets/domain/repositories/IDatasetsRepository.ts +++ b/src/datasets/domain/repositories/IDatasetsRepository.ts @@ -113,4 +113,13 @@ export interface IDatasetsRepository { getDatasetStorageDriver(datasetId: number | string): Promise getDatasetUploadLimits(datasetId: number | string): Promise getDatasetReviews(datasetId: number | string): Promise + assignRoleOnDataset( + datasetId: number | string, + roleAssignee: string, + roleAlias: string + ): Promise + unassignRoleOnDataset( + datasetId: number | string, + roleAssignmentId: number + ): Promise } diff --git a/src/datasets/domain/useCases/AssignRoleOnDataset.ts b/src/datasets/domain/useCases/AssignRoleOnDataset.ts new file mode 100644 index 00000000..92d6e849 --- /dev/null +++ b/src/datasets/domain/useCases/AssignRoleOnDataset.ts @@ -0,0 +1,26 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { IDatasetsRepository } from '../repositories/IDatasetsRepository' + +export class AssignRoleOnDataset implements UseCase { + private datasetsRepository: IDatasetsRepository + + constructor(datasetsRepository: IDatasetsRepository) { + this.datasetsRepository = datasetsRepository + } + + /** + * Assigns a new role to someone on the given dataset. + * + * @param {number | string} [datasetId] - The dataset identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers). + * @param {string} [roleAssignee] - To whom the role should be assigned + * @param {string} [roleAlias] - The alias of the role to be assigned + * @returns {Promise} + */ + async execute( + datasetId: number | string, + roleAssignee: string, + roleAlias: string + ): Promise { + return await this.datasetsRepository.assignRoleOnDataset(datasetId, roleAssignee, roleAlias) + } +} diff --git a/src/datasets/domain/useCases/UnassignRoleOnDataset.ts b/src/datasets/domain/useCases/UnassignRoleOnDataset.ts new file mode 100644 index 00000000..32baf096 --- /dev/null +++ b/src/datasets/domain/useCases/UnassignRoleOnDataset.ts @@ -0,0 +1,24 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { IDatasetsRepository } from '../repositories/IDatasetsRepository' + +export class UnassignRoleOnDataset implements UseCase { + private datasetsRepository: IDatasetsRepository + + constructor(datasetsRepository: IDatasetsRepository) { + this.datasetsRepository = datasetsRepository + } + + /** + * Deletes the given role assignment on the given dataset. + * + * @param {number | string} [datasetId] - The dataset identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers). + * @param {number} [roleAssignmentId] - To numeric identifier of the role assignment + * @returns {Promise} + */ + async execute( + datasetId: number | string, + roleAssignmentId: number + ): Promise { + return await this.datasetsRepository.unassignRoleOnDataset(datasetId, roleAssignmentId) + } +} diff --git a/src/datasets/index.ts b/src/datasets/index.ts index de7a32b4..44e0d740 100644 --- a/src/datasets/index.ts +++ b/src/datasets/index.ts @@ -37,6 +37,8 @@ import { GetDatasetStorageDriver } from './domain/useCases/GetDatasetStorageDriv import { GetDatasetUploadLimits } from './domain/useCases/GetDatasetUploadLimits' import { GetDatasetReviews } from './domain/useCases/GetDatasetReviews' import { ExportDatasetMetadata } from './domain/useCases/ExportDatasetMetadata' +import { AssignRoleOnDataset } from './domain/useCases/AssignRoleOnDataset' +import { UnassignRoleOnDataset } from './domain/useCases/UnassignRoleOnDataset' const datasetsRepository = new DatasetsRepository() @@ -90,6 +92,8 @@ const getDatasetStorageDriver = new GetDatasetStorageDriver(datasetsRepository) const getDatasetUploadLimits = new GetDatasetUploadLimits(datasetsRepository) const getDatasetReviews = new GetDatasetReviews(datasetsRepository) const exportDatasetMetadata = new ExportDatasetMetadata(datasetsRepository) +const assignRoleOnDataset = new AssignRoleOnDataset(datasetsRepository) +const unassignRoleOnDataset = new UnassignRoleOnDataset(datasetsRepository) export { getDataset, @@ -124,7 +128,9 @@ export { getDatasetStorageDriver, getDatasetUploadLimits, getDatasetReviews, - exportDatasetMetadata + exportDatasetMetadata, + assignRoleOnDataset, + unassignRoleOnDataset } export { DatasetNotNumberedVersion } from './domain/models/DatasetNotNumberedVersion' export { ExportedDatasetMetadata } from './domain/models/ExportedDatasetMetadata' diff --git a/src/datasets/infra/repositories/DatasetsRepository.ts b/src/datasets/infra/repositories/DatasetsRepository.ts index 1eba3fa4..9f0d59bb 100644 --- a/src/datasets/infra/repositories/DatasetsRepository.ts +++ b/src/datasets/infra/repositories/DatasetsRepository.ts @@ -568,4 +568,30 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi throw error }) } + + public async assignRoleOnDataset( + datasetId: number | string, + roleAssignee: string, + roleAlias: string + ): Promise { + return this.doPost(this.buildApiEndpoint(this.datasetsResourceName, 'assignments', datasetId), { + assignee: roleAssignee, + role: roleAlias + }) + .then(() => undefined) + .catch((error) => { + throw error + }) + } + + public async unassignRoleOnDataset( + datasetId: number | string, + roleAssignmentId: number + ): Promise { + return this.doDelete(this.buildApiEndpoint(this.datasetsResourceName, `assignments/${roleAssignmentId}`, datasetId)) + .then(() => undefined) + .catch((error) => { + throw error + }) + } } diff --git a/test/unit/collections/AssignRoleOnCollection.test.ts b/test/unit/collections/AssignRoleOnCollection.test.ts new file mode 100644 index 00000000..255dc08d --- /dev/null +++ b/test/unit/collections/AssignRoleOnCollection.test.ts @@ -0,0 +1,25 @@ +import { ICollectionsRepository } from '../../../src/collections/domain/repositories/ICollectionsRepository' +import { WriteError } from '../../../src' +import { AssignRoleOnCollection } from '../../../src/collections/domain/useCases/AssignRoleOnCollection' + +describe('execute', () => { + test('should assign role successfully on repository success', async () => { + const collectionRepositoryStub: ICollectionsRepository = {} as ICollectionsRepository + collectionRepositoryStub.assignRoleOnCollection = jest.fn().mockResolvedValue(undefined) + + const testAssignRoleOnCollection = new AssignRoleOnCollection(collectionRepositoryStub) + + await expect(testAssignRoleOnCollection.execute(1, "@testUser", "curator")).resolves.toBeUndefined() + expect(collectionRepositoryStub.assignRoleOnCollection).toHaveBeenCalledWith(1, "@testUser", "curator") + }) + + test('should throw error on repository failure', async () => { + const collectionRepositoryStub: ICollectionsRepository = {} as ICollectionsRepository + collectionRepositoryStub.assignRoleOnCollection = jest.fn().mockRejectedValue(new WriteError()) + + const testAssignRoleOnCollection = new AssignRoleOnCollection(collectionRepositoryStub) + + await expect(testAssignRoleOnCollection.execute(1, "@testUser", "curator")).rejects.toThrow(WriteError) + expect(collectionRepositoryStub.assignRoleOnCollection).toHaveBeenCalledWith(1, "@testUser", "curator") + }) +}) diff --git a/test/unit/collections/UnassignRoleOnCollection.test.ts b/test/unit/collections/UnassignRoleOnCollection.test.ts new file mode 100644 index 00000000..393f8fef --- /dev/null +++ b/test/unit/collections/UnassignRoleOnCollection.test.ts @@ -0,0 +1,25 @@ +import { ICollectionsRepository } from '../../../src/collections/domain/repositories/ICollectionsRepository' +import { WriteError } from '../../../src' +import { UnassignRoleOnCollection } from '../../../src/collections/domain/useCases/UnassignRoleOnCollection' + +describe('execute', () => { + test('should unassign role successfully on repository success', async () => { + const collectionRepositoryStub: ICollectionsRepository = {} as ICollectionsRepository + collectionRepositoryStub.unassignRoleOnCollection = jest.fn().mockResolvedValue(undefined) + + const testUnassignRoleOnCollection = new UnassignRoleOnCollection(collectionRepositoryStub) + + await expect(testUnassignRoleOnCollection.execute(1, 2)).resolves.toBeUndefined() + expect(collectionRepositoryStub.unassignRoleOnCollection).toHaveBeenCalledWith(1, 2) + }) + + test('should throw error on repository failure', async () => { + const collectionRepositoryStub: ICollectionsRepository = {} as ICollectionsRepository + collectionRepositoryStub.unassignRoleOnCollection = jest.fn().mockRejectedValue(new WriteError()) + + const testUnassignRoleOnCollection = new UnassignRoleOnCollection(collectionRepositoryStub) + + await expect(testUnassignRoleOnCollection.execute(1, 2)).rejects.toThrow(WriteError) + expect(collectionRepositoryStub.unassignRoleOnCollection).toHaveBeenCalledWith(1, 2) + }) +}) diff --git a/test/unit/datasets/AssignRoleOnDataset.test.ts b/test/unit/datasets/AssignRoleOnDataset.test.ts new file mode 100644 index 00000000..bea50ab7 --- /dev/null +++ b/test/unit/datasets/AssignRoleOnDataset.test.ts @@ -0,0 +1,25 @@ +import { IDatasetsRepository } from '../../../src/datasets/domain/repositories/IDatasetsRepository' +import { WriteError } from '../../../src' +import { AssignRoleOnDataset } from '../../../src/datasets/domain/useCases/AssignRoleOnDataset' + +describe('execute', () => { + test('should assign role successfully on repository success', async () => { + const datasetsRepositoryStub: IDatasetsRepository = {} as IDatasetsRepository + datasetsRepositoryStub.assignRoleOnDataset = jest.fn().mockResolvedValue(undefined) + + const testAssignRoleOnDataset = new AssignRoleOnDataset(datasetsRepositoryStub) + + await expect(testAssignRoleOnDataset.execute(1, "@testUser", "curator")).resolves.toBeUndefined() + expect(datasetsRepositoryStub.assignRoleOnDataset).toHaveBeenCalledWith(1, "@testUser", "curator") + }) + + test('should throw error on repository failure', async () => { + const datasetsRepositoryStub: IDatasetsRepository = {} as IDatasetsRepository + datasetsRepositoryStub.assignRoleOnDataset = jest.fn().mockRejectedValue(new WriteError()) + + const testAssignRoleOnDataset = new AssignRoleOnDataset(datasetsRepositoryStub) + + await expect(testAssignRoleOnDataset.execute(1, "@testUser", "curator")).rejects.toThrow(WriteError) + expect(datasetsRepositoryStub.assignRoleOnDataset).toHaveBeenCalledWith(1, "@testUser", "curator") + }) +}) diff --git a/test/unit/datasets/UnassignRoleOnDataset.test.ts b/test/unit/datasets/UnassignRoleOnDataset.test.ts new file mode 100644 index 00000000..a168f6ab --- /dev/null +++ b/test/unit/datasets/UnassignRoleOnDataset.test.ts @@ -0,0 +1,25 @@ +import { IDatasetsRepository } from '../../../src/datasets/domain/repositories/IDatasetsRepository' +import { WriteError } from '../../../src' +import { UnassignRoleOnDataset } from '../../../src/datasets/domain/useCases/UnassignRoleOnDataset' + +describe('execute', () => { + test('should unassign role successfully on repository success', async () => { + const datasetsRepositoryStub: IDatasetsRepository = {} as IDatasetsRepository + datasetsRepositoryStub.unassignRoleOnDataset = jest.fn().mockResolvedValue(undefined) + + const testUnassignRoleOnDataset = new UnassignRoleOnDataset(datasetsRepositoryStub) + + await expect(testUnassignRoleOnDataset.execute(1, 2)).resolves.toBeUndefined() + expect(datasetsRepositoryStub.unassignRoleOnDataset).toHaveBeenCalledWith(1, 2) + }) + + test('should throw error on repository failure', async () => { + const datasetsRepositoryStub: IDatasetsRepository = {} as IDatasetsRepository + datasetsRepositoryStub.unassignRoleOnDataset = jest.fn().mockRejectedValue(new WriteError()) + + const testUnassignRoleOnDataset = new UnassignRoleOnDataset(datasetsRepositoryStub) + + await expect(testUnassignRoleOnDataset.execute(1, 2)).rejects.toThrow(WriteError) + expect(datasetsRepositoryStub.unassignRoleOnDataset).toHaveBeenCalledWith(1, 2) + }) +})