From b743a4e168c3b991090a69ad5ed2a85b17ee0f36 Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Tue, 24 Jun 2025 13:49:54 -0400 Subject: [PATCH 1/8] add use case and unit test --- src/roles/domain/models/Role.ts | 7 +++++ .../domain/repositories/IRolesRepository.ts | 5 ++++ .../transformers/roleTransformers.ts | 14 ++++++++++ .../domain/useCases/GetUserSelectableRoles.ts | 16 ++++++++++++ .../infra/repositories/RolesRepository.ts | 15 +++++++++++ test/testHelpers/roles/roleHelper.ts | 26 +++++++++++++++++++ .../unit/roles/GetUserSelectableRoles.test.ts | 25 ++++++++++++++++++ 7 files changed, 108 insertions(+) create mode 100644 src/roles/domain/models/Role.ts create mode 100644 src/roles/domain/repositories/IRolesRepository.ts create mode 100644 src/roles/domain/repositories/transformers/roleTransformers.ts create mode 100644 src/roles/domain/useCases/GetUserSelectableRoles.ts create mode 100644 src/roles/infra/repositories/RolesRepository.ts create mode 100644 test/testHelpers/roles/roleHelper.ts create mode 100644 test/unit/roles/GetUserSelectableRoles.test.ts diff --git a/src/roles/domain/models/Role.ts b/src/roles/domain/models/Role.ts new file mode 100644 index 00000000..b133d9a5 --- /dev/null +++ b/src/roles/domain/models/Role.ts @@ -0,0 +1,7 @@ +export interface Role { + id: number + name: string + alias: string + description: string + permissions: string[] +} diff --git a/src/roles/domain/repositories/IRolesRepository.ts b/src/roles/domain/repositories/IRolesRepository.ts new file mode 100644 index 00000000..c968e6f5 --- /dev/null +++ b/src/roles/domain/repositories/IRolesRepository.ts @@ -0,0 +1,5 @@ +import { Role } from '../models/Role' + +export interface IRolesRepository { + getUserSelectableRoles(): Promise +} diff --git a/src/roles/domain/repositories/transformers/roleTransformers.ts b/src/roles/domain/repositories/transformers/roleTransformers.ts new file mode 100644 index 00000000..3e564322 --- /dev/null +++ b/src/roles/domain/repositories/transformers/roleTransformers.ts @@ -0,0 +1,14 @@ +import { AxiosResponse } from 'axios' +import { Role } from '../../models/Role' + +export const transformRolesUserSelectableResponseToRoles = (response: AxiosResponse): Role[] => { + const roleUserSelectablePayload = response.data.data + + return roleUserSelectablePayload.map((role: any) => ({ + id: role.id, + name: role.name, + alias: role.alias, + description: role.description, + permissions: role.permissions + })) +} diff --git a/src/roles/domain/useCases/GetUserSelectableRoles.ts b/src/roles/domain/useCases/GetUserSelectableRoles.ts new file mode 100644 index 00000000..d9d87fd7 --- /dev/null +++ b/src/roles/domain/useCases/GetUserSelectableRoles.ts @@ -0,0 +1,16 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { Role } from '../models/Role' +import { IRolesRepository } from '../repositories/IRolesRepository' + +export class GetUserSelectableRoles implements UseCase { + constructor(private readonly rolesRepository: IRolesRepository) {} + + /** + * Returns an array of Roles, for the currently logged in user. + * + * @returns {Promise} - A promise that resolves to an array of Role instances that the user can select. + */ + async execute(): Promise { + return (await this.rolesRepository.getUserSelectableRoles()) as Role[] + } +} diff --git a/src/roles/infra/repositories/RolesRepository.ts b/src/roles/infra/repositories/RolesRepository.ts new file mode 100644 index 00000000..6230d9c3 --- /dev/null +++ b/src/roles/infra/repositories/RolesRepository.ts @@ -0,0 +1,15 @@ +import { ApiRepository } from '../../../core/infra/repositories/ApiRepository' +import { IRolesRepository } from '../../domain/repositories/IRolesRepository' +import { Role } from '../../domain/models/Role' +import { transformRolesUserSelectableResponseToRoles } from '../../domain/repositories/transformers/roleTransformers' + +export class RolesRepository extends ApiRepository implements IRolesRepository { + private readonly rolesResourceName: string = 'roles' + public async getUserSelectableRoles(): Promise { + return this.doGet(`/${this.rolesResourceName}/userSelectable`, true) + .then((response) => transformRolesUserSelectableResponseToRoles(response.data.data)) + .catch((error) => { + throw error + }) + } +} diff --git a/test/testHelpers/roles/roleHelper.ts b/test/testHelpers/roles/roleHelper.ts new file mode 100644 index 00000000..9cc26b46 --- /dev/null +++ b/test/testHelpers/roles/roleHelper.ts @@ -0,0 +1,26 @@ +import { Role } from '../../../src/roles/domain/models/Role' + +export const createRoleModel = (): Role => { + return { + id: 1, + name: 'admin', + alias: 'Admin', + description: + 'A person who has all permissions for dataverses, datasets, and files, including approving requests for restricted data.', + permissions: [ + 'AddDataverse', + 'AddDataset', + 'ViewUnpublishedDataverse', + 'ViewUnpublishedDataset' + ] + } +} +export const createRoleModelArray = (count: number): Role[] => { + return Array.from({ length: count }, (_, index) => ({ + id: index + 1, + name: `role${index + 1}`, + alias: `Role ${index + 1}`, + description: `Description for role ${index + 1}`, + permissions: [`Permission${index + 1}`] + })) +} diff --git a/test/unit/roles/GetUserSelectableRoles.test.ts b/test/unit/roles/GetUserSelectableRoles.test.ts new file mode 100644 index 00000000..7637e3f7 --- /dev/null +++ b/test/unit/roles/GetUserSelectableRoles.test.ts @@ -0,0 +1,25 @@ +import { ReadError } from '../../../src' +import { IRolesRepository } from '../../../src/roles/domain/repositories/IRolesRepository' +import { createRoleModelArray } from '../../testHelpers/roles/roleHelper' +import { GetUserSelectableRoles } from '../../../src/roles/domain/useCases/GetUserSelectableRoles' + +describe('execute', () => { + test('should return roles array on repository success', async () => { + const rolesRepositoryStub: IRolesRepository = {} as IRolesRepository + const testRoles = createRoleModelArray(5) + rolesRepositoryStub.getUserSelectableRoles = jest.fn().mockResolvedValue(testRoles) + const sut = new GetUserSelectableRoles(rolesRepositoryStub) + + const actual = await sut.execute() + + expect(actual).toEqual(testRoles) + }) + + test('should return error result on repository error', async () => { + const rolesRepositoryStub: IRolesRepository = {} as IRolesRepository + rolesRepositoryStub.getUserSelectableRoles = jest.fn().mockRejectedValue(new ReadError()) + const sut = new GetUserSelectableRoles(rolesRepositoryStub) + + await expect(sut.execute()).rejects.toThrow(ReadError) + }) +}) From aa762a8c376ccf45a1b4e0977b26ee2befb9d6e5 Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Tue, 24 Jun 2025 14:46:00 -0400 Subject: [PATCH 2/8] add integration useCase --- .../transformers/roleTransformers.ts | 2 + .../infra/repositories/RolesRepository.ts | 2 +- .../integration/roles/RolesRepository.test.ts | 23 +++++ test/testHelpers/roles/roleHelper.ts | 90 +++++++++++++++++++ 4 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 test/integration/roles/RolesRepository.test.ts diff --git a/src/roles/domain/repositories/transformers/roleTransformers.ts b/src/roles/domain/repositories/transformers/roleTransformers.ts index 3e564322..8a508f7a 100644 --- a/src/roles/domain/repositories/transformers/roleTransformers.ts +++ b/src/roles/domain/repositories/transformers/roleTransformers.ts @@ -2,7 +2,9 @@ import { AxiosResponse } from 'axios' import { Role } from '../../models/Role' export const transformRolesUserSelectableResponseToRoles = (response: AxiosResponse): Role[] => { + console.log('transformRolesUserSelectableResponseToRoles', response) const roleUserSelectablePayload = response.data.data + console.log('transformRolesUserSelectableResponseToRoles', response.data.data) return roleUserSelectablePayload.map((role: any) => ({ id: role.id, diff --git a/src/roles/infra/repositories/RolesRepository.ts b/src/roles/infra/repositories/RolesRepository.ts index 6230d9c3..1ec7f3af 100644 --- a/src/roles/infra/repositories/RolesRepository.ts +++ b/src/roles/infra/repositories/RolesRepository.ts @@ -7,7 +7,7 @@ export class RolesRepository extends ApiRepository implements IRolesRepository { private readonly rolesResourceName: string = 'roles' public async getUserSelectableRoles(): Promise { return this.doGet(`/${this.rolesResourceName}/userSelectable`, true) - .then((response) => transformRolesUserSelectableResponseToRoles(response.data.data)) + .then((response) => transformRolesUserSelectableResponseToRoles(response)) .catch((error) => { throw error }) diff --git a/test/integration/roles/RolesRepository.test.ts b/test/integration/roles/RolesRepository.test.ts new file mode 100644 index 00000000..89a26843 --- /dev/null +++ b/test/integration/roles/RolesRepository.test.ts @@ -0,0 +1,23 @@ +import { + ApiConfig, + DataverseApiAuthMechanism +} from '../../../src/core/infra/repositories/ApiConfig' +import { TestConstants } from '../../testHelpers/TestConstants' +import { RolesRepository } from '../../../src/roles/infra/repositories/RolesRepository' +import { createSuperAdminRoleArray } from '../../testHelpers/roles/roleHelper' + +describe('RolesRepository', () => { + const sut: RolesRepository = new RolesRepository() + + describe('getUserSelectableRoles', () => { + test('should return list of selectable roles for authenticated user', async () => { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + const actual = await sut.getUserSelectableRoles() + expect(actual).toStrictEqual(createSuperAdminRoleArray()) + }) + }) +}) diff --git a/test/testHelpers/roles/roleHelper.ts b/test/testHelpers/roles/roleHelper.ts index 9cc26b46..cf48cc3c 100644 --- a/test/testHelpers/roles/roleHelper.ts +++ b/test/testHelpers/roles/roleHelper.ts @@ -24,3 +24,93 @@ export const createRoleModelArray = (count: number): Role[] => { permissions: [`Permission${index + 1}`] })) } + +export const createSuperAdminRoleArray = (): Role[] => { + return [ + { + alias: 'admin', + name: 'Admin', + permissions: [ + 'AddDataverse', + 'AddDataset', + 'ViewUnpublishedDataverse', + 'ViewUnpublishedDataset', + 'DownloadFile', + 'EditDataverse', + 'EditDataset', + 'ManageDataversePermissions', + 'ManageDatasetPermissions', + 'ManageFilePermissions', + 'PublishDataverse', + 'PublishDataset', + 'DeleteDataverse', + 'DeleteDatasetDraft' + ], + description: + 'A person who has all permissions for dataverses, datasets, and files, including approving requests for restricted data.', + id: 1 + }, + { + alias: 'fileDownloader', + name: 'File Downloader', + permissions: ['DownloadFile'], + description: 'A person who can download a published file.', + id: 2 + }, + { + alias: 'fullContributor', + name: 'Dataverse + Dataset Creator', + permissions: ['AddDataverse', 'AddDataset'], + description: 'A person who can add subdataverses and datasets within a dataverse.', + id: 3 + }, + { + alias: 'dvContributor', + name: 'Dataverse Creator', + permissions: ['AddDataverse'], + description: 'A person who can add subdataverses within a dataverse.', + id: 4 + }, + { + alias: 'dsContributor', + name: 'Dataset Creator', + permissions: ['AddDataset'], + description: 'A person who can add datasets within a dataverse.', + id: 5 + }, + { + alias: 'contributor', + name: 'Contributor', + permissions: ['ViewUnpublishedDataset', 'DownloadFile', 'EditDataset', 'DeleteDatasetDraft'], + description: + 'For datasets, a person who can edit License + Terms, and then submit them for review.', + id: 6 + }, + { + alias: 'curator', + name: 'Curator', + permissions: [ + 'AddDataverse', + 'AddDataset', + 'ViewUnpublishedDataverse', + 'ViewUnpublishedDataset', + 'DownloadFile', + 'EditDataset', + 'ManageDatasetPermissions', + 'ManageFilePermissions', + 'PublishDataset', + 'DeleteDatasetDraft' + ], + description: + 'For datasets, a person who can edit License + Terms, edit Permissions, and publish datasets.', + id: 7 + }, + { + alias: 'member', + name: 'Member', + permissions: ['ViewUnpublishedDataverse', 'ViewUnpublishedDataset', 'DownloadFile'], + description: 'A person who can view both unpublished dataverses and datasets.', + id: 8 + } + ] +} From 37e0e8b2bea5c96e7ff41534aa2541ec138af557 Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Tue, 24 Jun 2025 15:01:03 -0400 Subject: [PATCH 3/8] add documentation --- docs/useCases.md | 25 +++++++++++++++++++ .../domain/useCases/GetUserSelectableRoles.ts | 4 +-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/docs/useCases.md b/docs/useCases.md index ae3aaae0..f1feac41 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -75,6 +75,9 @@ The different use cases currently available in the package are classified below, - [Delete Current API Token](#delete-current-api-token) - [Recreate Current API Token](#recreate-current-api-token) - [Register User](#register-user) +- [Roles](#Roles) + - [Roles read use cases](#roles-read-use-cases) + - [Get User Selectable Roles](#get-user-selectable-roles) - [Info](#Info) - [Get Dataverse Backend Version](#get-dataverse-backend-version) - [Get Maximum Embargo Duration In Months](#get-maximum-embargo-duration-in-months) @@ -1842,6 +1845,28 @@ registerUser.execute(userDTO) _See [use case](../src/users/domain/useCases/RegisterUser.ts) implementation_. +## Roles + +### Get User Selectable Roles + +Returns a [Role](../src/roles/domain/models/Role.ts) array that the calling user can use as filters when searching within their data. + +##### Example call: + +```typescript +import { getUserSelectableRoles } from '@iqss/dataverse-client-javascript' + +/* ... */ + +getUserSelectableRoles.execute().then((roles: Role[]) => { + /* ... */ +}) + +/* ... */ +``` + +_See [use case](../src/roles/domain/useCases/GetUserSelectableRoles.ts) implementation_. + ## Info #### Get Dataverse Backend Version diff --git a/src/roles/domain/useCases/GetUserSelectableRoles.ts b/src/roles/domain/useCases/GetUserSelectableRoles.ts index d9d87fd7..111f4f1e 100644 --- a/src/roles/domain/useCases/GetUserSelectableRoles.ts +++ b/src/roles/domain/useCases/GetUserSelectableRoles.ts @@ -6,9 +6,9 @@ export class GetUserSelectableRoles implements UseCase { constructor(private readonly rolesRepository: IRolesRepository) {} /** - * Returns an array of Roles, for the currently logged in user. + * Returns the appropriate roles that the calling user can use as filters when searching within their data. * - * @returns {Promise} - A promise that resolves to an array of Role instances that the user can select. + * @returns {Promise} - A promise that resolves to an array of Role instances. */ async execute(): Promise { return (await this.rolesRepository.getUserSelectableRoles()) as Role[] From 2854a5cefb52a1780c6b2642653d4496aaa7d45f Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Tue, 24 Jun 2025 15:25:21 -0400 Subject: [PATCH 4/8] remove debug logs --- src/roles/domain/repositories/transformers/roleTransformers.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/roles/domain/repositories/transformers/roleTransformers.ts b/src/roles/domain/repositories/transformers/roleTransformers.ts index 8a508f7a..3e564322 100644 --- a/src/roles/domain/repositories/transformers/roleTransformers.ts +++ b/src/roles/domain/repositories/transformers/roleTransformers.ts @@ -2,9 +2,7 @@ import { AxiosResponse } from 'axios' import { Role } from '../../models/Role' export const transformRolesUserSelectableResponseToRoles = (response: AxiosResponse): Role[] => { - console.log('transformRolesUserSelectableResponseToRoles', response) const roleUserSelectablePayload = response.data.data - console.log('transformRolesUserSelectableResponseToRoles', response.data.data) return roleUserSelectablePayload.map((role: any) => ({ id: role.id, From 8dcdffcc9e508a6a01487369ae8041859bf640c1 Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Tue, 24 Jun 2025 16:20:01 -0400 Subject: [PATCH 5/8] add index.ts --- src/roles/index.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/roles/index.ts diff --git a/src/roles/index.ts b/src/roles/index.ts new file mode 100644 index 00000000..9018d82b --- /dev/null +++ b/src/roles/index.ts @@ -0,0 +1,10 @@ +import { RolesRepository } from './infra/repositories/RolesRepository' +import { GetUserSelectableRoles } from './domain/useCases/GetUserSelectableRoles' + +const rolesRepository = new RolesRepository() + +const getCurrentAuthenticatedUser = new GetUserSelectableRoles(rolesRepository) + +export { getCurrentAuthenticatedUser } + +export { Role } from './domain/models/Role' From 02e283d92830bc4f55cd2eba04580326ee9f5277 Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Tue, 24 Jun 2025 16:38:01 -0400 Subject: [PATCH 6/8] fix useCase name --- src/roles/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/roles/index.ts b/src/roles/index.ts index 9018d82b..8bd9e276 100644 --- a/src/roles/index.ts +++ b/src/roles/index.ts @@ -3,8 +3,8 @@ import { GetUserSelectableRoles } from './domain/useCases/GetUserSelectableRoles const rolesRepository = new RolesRepository() -const getCurrentAuthenticatedUser = new GetUserSelectableRoles(rolesRepository) +const getUserSelectableRoles = new GetUserSelectableRoles(rolesRepository) -export { getCurrentAuthenticatedUser } +export { getUserSelectableRoles } export { Role } from './domain/models/Role' From 0bc609031f3aa47bd45be33858d50d8f5ae3e9ff Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Wed, 25 Jun 2025 09:15:24 -0400 Subject: [PATCH 7/8] add roles directory to export list --- src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.ts b/src/index.ts index 2abdf089..89a79af6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ export * from './core' export * from './info' export * from './users' +export * from './roles' export * from './auth' export * from './datasets' export * from './collections' From 87d384a4ffc0690262aeb426a0732afa90356ddc Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Thu, 26 Jun 2025 19:51:38 -0400 Subject: [PATCH 8/8] add RolePayload.ts --- src/roles/domain/repositories/transformers/RolePayload.ts | 7 +++++++ .../domain/repositories/transformers/roleTransformers.ts | 5 +++-- 2 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 src/roles/domain/repositories/transformers/RolePayload.ts diff --git a/src/roles/domain/repositories/transformers/RolePayload.ts b/src/roles/domain/repositories/transformers/RolePayload.ts new file mode 100644 index 00000000..4a2028a2 --- /dev/null +++ b/src/roles/domain/repositories/transformers/RolePayload.ts @@ -0,0 +1,7 @@ +export interface RolePayload { + id: number + name: string + alias: string + description: string + permissions: string[] +} diff --git a/src/roles/domain/repositories/transformers/roleTransformers.ts b/src/roles/domain/repositories/transformers/roleTransformers.ts index 3e564322..97d4ab42 100644 --- a/src/roles/domain/repositories/transformers/roleTransformers.ts +++ b/src/roles/domain/repositories/transformers/roleTransformers.ts @@ -1,10 +1,11 @@ import { AxiosResponse } from 'axios' import { Role } from '../../models/Role' +import { RolePayload } from './RolePayload' export const transformRolesUserSelectableResponseToRoles = (response: AxiosResponse): Role[] => { - const roleUserSelectablePayload = response.data.data + const roleUserSelectablePayload = response.data.data as RolePayload[] - return roleUserSelectablePayload.map((role: any) => ({ + return roleUserSelectablePayload.map((role: RolePayload) => ({ id: role.id, name: role.name, alias: role.alias,