diff --git a/docs/useCases.md b/docs/useCases.md index f77a40e1..85002217 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -11,6 +11,8 @@ The different use cases currently available in the package are classified below, - [Collections](#Collections) - [Collections read use cases](#collections-read-use-cases) - [Get a Collection](#get-a-collection) + - [Get Collection Storage Driver](#get-collection-storage-driver) + - [Get Allowed Collection Storage Drivers](#get-allowed-collection-storage-drivers) - [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) @@ -19,6 +21,8 @@ The different use cases currently available in the package are classified below, - [Get Collections for Linking](#get-collections-for-linking) - [Collections write use cases](#collections-write-use-cases) - [Create a Collection](#create-a-collection) + - [Set Collection Storage Driver](#set-collection-storage-driver) + - [Delete Collection Storage Driver](#delete-collection-storage-driver) - [Update a Collection](#update-a-collection) - [Publish a Collection](#publish-a-collection) - [Delete a Collection](#delete-a-collection) @@ -172,6 +176,65 @@ The `collectionIdOrAlias` is a generic collection identifier, which can be eithe If no collection identifier is specified, the default collection identifier; `:root` will be used. If you want to search for a different collection, you must add the collection identifier as a parameter in the use case call. +#### Get Collection Storage Driver + +Returns a [StorageDriver](../src/core/domain/models/StorageDriver.ts) instance describing the collection's assigned storage driver. + +##### Example call: + +```typescript +import { getCollectionStorageDriver } from '@iqss/dataverse-client-javascript' + +/* ... */ + +const collectionIdOrAlias = 'classicLiterature' + +getCollectionStorageDriver.execute(collectionIdOrAlias).then((storageDriver: StorageDriver) => { + /* ... */ +}) + +// Pass true to resolve the effective driver after inheritance/default fallback +getCollectionStorageDriver + .execute(collectionIdOrAlias, true) + .then((storageDriver: StorageDriver) => { + /* ... */ + }) + +/* ... */ +``` + +_See [use case](../src/collections/domain/useCases/GetCollectionStorageDriver.ts) implementation_. + +The `collectionIdOrAlias` is a generic collection identifier, which can be either a string (for queries by CollectionAlias), or a number (for queries by CollectionId). + +The optional `getEffective` parameter defaults to `false`. Set it to `true` to retrieve the effective storage driver after inheritance/default resolution. + +#### Get Allowed Collection Storage Drivers + +Returns an [AllowedStorageDrivers](../src/collections/domain/models/AllowedStorageDrivers.ts) object whose keys are driver labels and whose values are storage driver ids. + +##### Example call: + +```typescript +import { getAllowedCollectionStorageDrivers } from '@iqss/dataverse-client-javascript' + +/* ... */ + +const collectionIdOrAlias = 'classicLiterature' + +getAllowedCollectionStorageDrivers + .execute(collectionIdOrAlias) + .then((storageDrivers: AllowedStorageDrivers) => { + /* ... */ + }) + +/* ... */ +``` + +_See [use case](../src/collections/domain/useCases/GetAllowedCollectionStorageDrivers.ts) implementation_. + +The `collectionIdOrAlias` is a generic collection identifier, which can be either a string (for queries by CollectionAlias), or a number (for queries by CollectionId). + #### Get Collection Facets Returns a [CollectionFacet](../src/collections/domain/models/CollectionFacet.ts) array containing the facets of the requested collection, given the collection identifier or alias. @@ -442,6 +505,57 @@ The above example creates the new collection in the root collection since no col The use case returns a number, which is the identifier of the created collection. +#### Set Collection Storage Driver + +Assigns a storage driver to a collection by driver label and returns the backend success message. + +##### Example call: + +```typescript +import { setCollectionStorageDriver } from '@iqss/dataverse-client-javascript' + +/* ... */ + +const collectionIdOrAlias = 'classicLiterature' +const driverLabel = 'Local Storage' + +setCollectionStorageDriver.execute(collectionIdOrAlias, driverLabel).then((message: string) => { + /* ... */ +}) + +/* ... */ +``` + +_See [use case](../src/collections/domain/useCases/SetCollectionStorageDriver.ts) implementation_. + +The `collectionIdOrAlias` is a generic collection identifier, which can be either a string (for queries by CollectionAlias), or a number (for queries by CollectionId). + +The `driverLabel` parameter must match the storage driver's label, not its id. + +#### Delete Collection Storage Driver + +Clears the directly assigned storage driver from a collection so it falls back to inherited/default storage, and returns the backend success message. + +##### Example call: + +```typescript +import { deleteCollectionStorageDriver } from '@iqss/dataverse-client-javascript' + +/* ... */ + +const collectionIdOrAlias = 'classicLiterature' + +deleteCollectionStorageDriver.execute(collectionIdOrAlias).then((message: string) => { + /* ... */ +}) + +/* ... */ +``` + +_See [use case](../src/collections/domain/useCases/DeleteCollectionStorageDriver.ts) implementation_. + +The `collectionIdOrAlias` is a generic collection identifier, which can be either a string (for queries by CollectionAlias), or a number (for queries by CollectionId). + #### Update a Collection Updates an existing collection, given a collection identifier and a [CollectionDTO](../src/collections/domain/dtos/CollectionDTO.ts) including the updated collection data. @@ -1398,8 +1512,6 @@ _See [use case](../src/datasets/domain/useCases/GetDatasetAvailableCategories.ts The `datasetId` parameter is a number for numeric identifiers or string for persistent identifiers. -# <<<<<<< HEAD - #### Get Dataset Templates Returns a [DatasetTemplate](../src/datasets/domain/models/DatasetTemplate.ts) array containing the dataset templates of the requested collection, given the collection identifier or alias. @@ -1420,7 +1532,7 @@ _See [use case](../src/datasets/domain/useCases/GetDatasetTemplates.ts)_ definit #### Get Dataset Storage Driver -Returns a [StorageDriver](../src/datasets/domain/models/StorageDriver.ts) instance with storage driver configuration for a dataset, including properties like name, type, label, and upload/download capabilities. +Returns a [StorageDriver](../src/core/domain/models/StorageDriver.ts) instance with storage driver configuration for a dataset, including properties like name, type, label, and upload/download capabilities. ##### Example call: @@ -1442,8 +1554,6 @@ _See [use case](../src/datasets/domain/useCases/GetDatasetStorageDriver.ts) impl The `datasetId` parameter can be a string, for persistent identifiers, or a number, for numeric identifiers. -> > > > > > > develop - #### Add a Dataset Type Adds a dataset types that can be used at dataset creation. diff --git a/src/collections/domain/models/AllowedStorageDrivers.ts b/src/collections/domain/models/AllowedStorageDrivers.ts new file mode 100644 index 00000000..8d0dc7ea --- /dev/null +++ b/src/collections/domain/models/AllowedStorageDrivers.ts @@ -0,0 +1 @@ +export type AllowedStorageDrivers = Record diff --git a/src/collections/domain/repositories/ICollectionsRepository.ts b/src/collections/domain/repositories/ICollectionsRepository.ts index bc8960c8..4b7bd15b 100644 --- a/src/collections/domain/repositories/ICollectionsRepository.ts +++ b/src/collections/domain/repositories/ICollectionsRepository.ts @@ -11,10 +11,24 @@ import { PublicationStatus } from '../../../core/domain/models/PublicationStatus import { CollectionItemType } from '../../../collections/domain/models/CollectionItemType' import { CollectionLinks } from '../models/CollectionLinks' import { CollectionSummary } from '../models/CollectionSummary' +import { AllowedStorageDrivers } from '../models/AllowedStorageDrivers' +import { StorageDriver } from '../../../core/domain/models/StorageDriver' import { LinkingObjectType } from '../useCases/GetCollectionsForLinking' export interface ICollectionsRepository { getCollection(collectionIdOrAlias: number | string): Promise + getCollectionStorageDriver( + collectionIdOrAlias: number | string, + getEffective?: boolean + ): Promise + setCollectionStorageDriver( + collectionIdOrAlias: number | string, + driverLabel: string + ): Promise + deleteCollectionStorageDriver(collectionIdOrAlias: number | string): Promise + getAllowedCollectionStorageDrivers( + collectionIdOrAlias: number | string + ): Promise createCollection( collectionDTO: CollectionDTO, parentCollectionId: number | string diff --git a/src/collections/domain/useCases/DeleteCollectionStorageDriver.ts b/src/collections/domain/useCases/DeleteCollectionStorageDriver.ts new file mode 100644 index 00000000..2f3d510c --- /dev/null +++ b/src/collections/domain/useCases/DeleteCollectionStorageDriver.ts @@ -0,0 +1,14 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { ICollectionsRepository } from '../repositories/ICollectionsRepository' + +export class DeleteCollectionStorageDriver implements UseCase { + private collectionsRepository: ICollectionsRepository + + constructor(collectionsRepository: ICollectionsRepository) { + this.collectionsRepository = collectionsRepository + } + + async execute(collectionIdOrAlias: number | string): Promise { + return this.collectionsRepository.deleteCollectionStorageDriver(collectionIdOrAlias) + } +} diff --git a/src/collections/domain/useCases/GetAllowedCollectionStorageDrivers.ts b/src/collections/domain/useCases/GetAllowedCollectionStorageDrivers.ts new file mode 100644 index 00000000..75af0568 --- /dev/null +++ b/src/collections/domain/useCases/GetAllowedCollectionStorageDrivers.ts @@ -0,0 +1,15 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { ICollectionsRepository } from '../repositories/ICollectionsRepository' +import { AllowedStorageDrivers } from '../models/AllowedStorageDrivers' + +export class GetAllowedCollectionStorageDrivers implements UseCase { + private collectionsRepository: ICollectionsRepository + + constructor(collectionsRepository: ICollectionsRepository) { + this.collectionsRepository = collectionsRepository + } + + async execute(collectionIdOrAlias: number | string): Promise { + return this.collectionsRepository.getAllowedCollectionStorageDrivers(collectionIdOrAlias) + } +} diff --git a/src/collections/domain/useCases/GetCollectionStorageDriver.ts b/src/collections/domain/useCases/GetCollectionStorageDriver.ts new file mode 100644 index 00000000..80fc5240 --- /dev/null +++ b/src/collections/domain/useCases/GetCollectionStorageDriver.ts @@ -0,0 +1,18 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { StorageDriver } from '../../../core/domain/models/StorageDriver' +import { ICollectionsRepository } from '../repositories/ICollectionsRepository' + +export class GetCollectionStorageDriver implements UseCase { + private collectionsRepository: ICollectionsRepository + + constructor(collectionsRepository: ICollectionsRepository) { + this.collectionsRepository = collectionsRepository + } + + async execute( + collectionIdOrAlias: number | string, + getEffective = false + ): Promise { + return this.collectionsRepository.getCollectionStorageDriver(collectionIdOrAlias, getEffective) + } +} diff --git a/src/collections/domain/useCases/SetCollectionStorageDriver.ts b/src/collections/domain/useCases/SetCollectionStorageDriver.ts new file mode 100644 index 00000000..ee915da3 --- /dev/null +++ b/src/collections/domain/useCases/SetCollectionStorageDriver.ts @@ -0,0 +1,14 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { ICollectionsRepository } from '../repositories/ICollectionsRepository' + +export class SetCollectionStorageDriver implements UseCase { + private collectionsRepository: ICollectionsRepository + + constructor(collectionsRepository: ICollectionsRepository) { + this.collectionsRepository = collectionsRepository + } + + async execute(collectionIdOrAlias: number | string, driverLabel: string): Promise { + return this.collectionsRepository.setCollectionStorageDriver(collectionIdOrAlias, driverLabel) + } +} diff --git a/src/collections/index.ts b/src/collections/index.ts index 59e2e50b..ae9b423d 100644 --- a/src/collections/index.ts +++ b/src/collections/index.ts @@ -3,6 +3,7 @@ import { GetCollection } from './domain/useCases/GetCollection' import { GetCollectionFacets } from './domain/useCases/GetCollectionFacets' import { GetCollectionUserPermissions } from './domain/useCases/GetCollectionUserPermissions' import { GetCollectionItems } from './domain/useCases/GetCollectionItems' +import { GetCollectionStorageDriver } from './domain/useCases/GetCollectionStorageDriver' import { PublishCollection } from './domain/useCases/PublishCollection' import { UpdateCollection } from './domain/useCases/UpdateCollection' import { GetCollectionFeaturedItems } from './domain/useCases/GetCollectionFeaturedItems' @@ -16,10 +17,14 @@ import { LinkCollection } from './domain/useCases/LinkCollection' import { UnlinkCollection } from './domain/useCases/UnlinkCollection' import { GetCollectionLinks } from './domain/useCases/GetCollectionLinks' import { GetCollectionsForLinking } from './domain/useCases/GetCollectionsForLinking' +import { SetCollectionStorageDriver } from './domain/useCases/SetCollectionStorageDriver' +import { DeleteCollectionStorageDriver } from './domain/useCases/DeleteCollectionStorageDriver' +import { GetAllowedCollectionStorageDrivers } from './domain/useCases/GetAllowedCollectionStorageDrivers' const collectionsRepository = new CollectionsRepository() const getCollection = new GetCollection(collectionsRepository) +const getCollectionStorageDriver = new GetCollectionStorageDriver(collectionsRepository) const createCollection = new CreateCollection(collectionsRepository) const getCollectionFacets = new GetCollectionFacets(collectionsRepository) const getCollectionUserPermissions = new GetCollectionUserPermissions(collectionsRepository) @@ -36,9 +41,15 @@ const linkCollection = new LinkCollection(collectionsRepository) const unlinkCollection = new UnlinkCollection(collectionsRepository) const getCollectionLinks = new GetCollectionLinks(collectionsRepository) const getCollectionsForLinking = new GetCollectionsForLinking(collectionsRepository) +const setCollectionStorageDriver = new SetCollectionStorageDriver(collectionsRepository) +const deleteCollectionStorageDriver = new DeleteCollectionStorageDriver(collectionsRepository) +const getAllowedCollectionStorageDrivers = new GetAllowedCollectionStorageDrivers( + collectionsRepository +) export { getCollection, + getCollectionStorageDriver, createCollection, getCollectionFacets, getCollectionUserPermissions, @@ -54,7 +65,10 @@ export { linkCollection, unlinkCollection, getCollectionLinks, - getCollectionsForLinking + getCollectionsForLinking, + setCollectionStorageDriver, + deleteCollectionStorageDriver, + getAllowedCollectionStorageDrivers } export { Collection, CollectionInputLevel } from './domain/models/Collection' export { CollectionFacet } from './domain/models/CollectionFacet' @@ -66,3 +80,5 @@ export { CollectionSearchCriteria } from './domain/models/CollectionSearchCriter export { FeaturedItem } from './domain/models/FeaturedItem' export { FeaturedItemsDTO } from './domain/dtos/FeaturedItemsDTO' export { CollectionSummary } from './domain/models/CollectionSummary' +export { AllowedStorageDrivers } from './domain/models/AllowedStorageDrivers' +export { StorageDriver } from '../core/domain/models/StorageDriver' diff --git a/src/collections/infra/repositories/CollectionsRepository.ts b/src/collections/infra/repositories/CollectionsRepository.ts index e0e459b0..62029489 100644 --- a/src/collections/infra/repositories/CollectionsRepository.ts +++ b/src/collections/infra/repositories/CollectionsRepository.ts @@ -39,6 +39,8 @@ import { PublicationStatus } from '../../../core/domain/models/PublicationStatus import { ReadError } from '../../../core/domain/repositories/ReadError' import { CollectionLinks } from '../../domain/models/CollectionLinks' import { CollectionSummary } from '../../domain/models/CollectionSummary' +import { AllowedStorageDrivers } from '../../domain/models/AllowedStorageDrivers' +import { StorageDriver } from '../../../core/domain/models/StorageDriver' import { LinkingObjectType } from '../../domain/useCases/GetCollectionsForLinking' export interface NewCollectionRequestPayload { @@ -108,6 +110,62 @@ export class CollectionsRepository extends ApiRepository implements ICollections }) } + public async getCollectionStorageDriver( + collectionIdOrAlias: number | string, + getEffective = false + ): Promise { + return this.doGet( + `/${this.collectionsResourceName}/${collectionIdOrAlias}/storageDriver`, + true, + { + getEffective + } + ) + .then((response) => response.data.data as StorageDriver) + .catch((error) => { + throw error + }) + } + + public async setCollectionStorageDriver( + collectionIdOrAlias: number | string, + driverLabel: string + ): Promise { + return this.doPut( + `/${this.collectionsResourceName}/${collectionIdOrAlias}/storageDriver`, + driverLabel, + undefined, + ApiConstants.CONTENT_TYPE_TEXT_PLAIN + ) + .then((response) => response.data.data.message) + .catch((error) => { + throw error + }) + } + + public async deleteCollectionStorageDriver( + collectionIdOrAlias: number | string + ): Promise { + return this.doDelete(`/${this.collectionsResourceName}/${collectionIdOrAlias}/storageDriver`) + .then((response) => response.data.data.message) + .catch((error) => { + throw error + }) + } + + public async getAllowedCollectionStorageDrivers( + collectionIdOrAlias: number | string + ): Promise { + return this.doGet( + `/${this.collectionsResourceName}/${collectionIdOrAlias}/allowedStorageDrivers`, + true + ) + .then((response) => response.data.data as AllowedStorageDrivers) + .catch((error) => { + throw error + }) + } + public async createCollection( collectionDTO: CollectionDTO, parentCollectionId: number | string = ROOT_COLLECTION_ID diff --git a/src/datasets/domain/models/StorageDriver.ts b/src/core/domain/models/StorageDriver.ts similarity index 79% rename from src/datasets/domain/models/StorageDriver.ts rename to src/core/domain/models/StorageDriver.ts index 9c04b400..308ba635 100644 --- a/src/datasets/domain/models/StorageDriver.ts +++ b/src/core/domain/models/StorageDriver.ts @@ -1,7 +1,7 @@ export interface StorageDriver { name: string - type: string - label: string + type?: string + label?: string directUpload: boolean directDownload: boolean uploadOutOfBand: boolean diff --git a/src/core/index.ts b/src/core/index.ts index e7cb65f8..ddd54a8a 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -3,3 +3,4 @@ export { WriteError } from './domain/repositories/WriteError' export { ApiConfig } from './infra/repositories/ApiConfig' export { DvObjectOwnerNode, DvObjectType } from './domain/models/DvObjectOwnerNode' export { PublicationStatus } from './domain/models/PublicationStatus' +export { StorageDriver } from './domain/models/StorageDriver' diff --git a/src/core/infra/repositories/ApiConstants.ts b/src/core/infra/repositories/ApiConstants.ts index 966841d4..e6509613 100644 --- a/src/core/infra/repositories/ApiConstants.ts +++ b/src/core/infra/repositories/ApiConstants.ts @@ -1,4 +1,5 @@ export class ApiConstants { static readonly CONTENT_TYPE_APPLICATION_JSON = 'application/json' static readonly CONTENT_TYPE_MULTIPART_FORM_DATA = 'multipart/form-data' + static readonly CONTENT_TYPE_TEXT_PLAIN = 'text/plain' } diff --git a/src/datasets/domain/repositories/IDatasetsRepository.ts b/src/datasets/domain/repositories/IDatasetsRepository.ts index 09859777..26c64bcd 100644 --- a/src/datasets/domain/repositories/IDatasetsRepository.ts +++ b/src/datasets/domain/repositories/IDatasetsRepository.ts @@ -16,7 +16,7 @@ import { DatasetType } from '../models/DatasetType' import { TermsOfAccess } from '../models/Dataset' import { DatasetLicenseUpdateRequest } from '../dtos/DatasetLicenseUpdateRequest' import { DatasetTypeDTO } from '../dtos/DatasetTypeDTO' -import { StorageDriver } from '../models/StorageDriver' +import { StorageDriver } from '../../../core/domain/models/StorageDriver' export interface IDatasetsRepository { getDataset( diff --git a/src/datasets/domain/useCases/GetDatasetStorageDriver.ts b/src/datasets/domain/useCases/GetDatasetStorageDriver.ts index 361d1db9..3de13864 100644 --- a/src/datasets/domain/useCases/GetDatasetStorageDriver.ts +++ b/src/datasets/domain/useCases/GetDatasetStorageDriver.ts @@ -1,6 +1,6 @@ import { UseCase } from '../../../core/domain/useCases/UseCase' +import { StorageDriver } from '../../../core/domain/models/StorageDriver' import { IDatasetsRepository } from '../repositories/IDatasetsRepository' -import { StorageDriver } from '../models/StorageDriver' export class GetDatasetStorageDriver implements UseCase { private datasetsRepository: IDatasetsRepository diff --git a/src/datasets/index.ts b/src/datasets/index.ts index 3c081fc6..d378b4fb 100644 --- a/src/datasets/index.ts +++ b/src/datasets/index.ts @@ -154,4 +154,4 @@ export { export { DatasetLinkedCollection } from './domain/models/DatasetLinkedCollection' export { DatasetType } from './domain/models/DatasetType' export { DatasetTypeDTO } from './domain/dtos/DatasetTypeDTO' -export { StorageDriver } from './domain/models/StorageDriver' +export { StorageDriver } from '../core/domain/models/StorageDriver' diff --git a/src/datasets/infra/repositories/DatasetsRepository.ts b/src/datasets/infra/repositories/DatasetsRepository.ts index c29db6c0..32aa66e7 100644 --- a/src/datasets/infra/repositories/DatasetsRepository.ts +++ b/src/datasets/infra/repositories/DatasetsRepository.ts @@ -29,7 +29,7 @@ import { TermsOfAccess } from '../../domain/models/Dataset' import { transformTermsOfAccessToUpdatePayload } from './transformers/termsOfAccessTransformers' import { DatasetLicenseUpdateRequest } from '../../domain/dtos/DatasetLicenseUpdateRequest' import { DatasetTypeDTO } from '../../domain/dtos/DatasetTypeDTO' -import { StorageDriver } from '../../domain/models/StorageDriver' +import { StorageDriver } from '../../../core/domain/models/StorageDriver' export interface GetAllDatasetPreviewsQueryParams { per_page?: number diff --git a/test/environment/.env b/test/environment/.env index e7b54bde..2635bf8c 100644 --- a/test/environment/.env +++ b/test/environment/.env @@ -1,6 +1,6 @@ POSTGRES_VERSION=17 DATAVERSE_DB_USER=dataverse SOLR_VERSION=9.8.0 -DATAVERSE_IMAGE_REGISTRY=docker.io -DATAVERSE_IMAGE_TAG=unstable +DATAVERSE_IMAGE_REGISTRY=ghcr.io +DATAVERSE_IMAGE_TAG=storage-driver-endpoint DATAVERSE_BOOTSTRAP_TIMEOUT=5m diff --git a/test/integration/collections/CollectionsRepository.test.ts b/test/integration/collections/CollectionsRepository.test.ts index 94b62311..56e87553 100644 --- a/test/integration/collections/CollectionsRepository.test.ts +++ b/test/integration/collections/CollectionsRepository.test.ts @@ -2158,4 +2158,74 @@ describe('CollectionsRepository', () => { await expect(sut.getCollectionLinks(invalidCollectionId)).rejects.toThrow(expectedError) }) }) + + describe('collection storage drivers', () => { + const parentCollectionAlias = 'collectionStorageDriverParent' + const childCollectionAlias = 'collectionStorageDriverChild' + const standaloneCollectionAlias = 'collectionStorageDriverStandalone' + + beforeAll(async () => { + await createCollectionViaApi(parentCollectionAlias) + await createCollectionViaApi(childCollectionAlias, parentCollectionAlias) + await createCollectionViaApi(standaloneCollectionAlias) + }) + + afterAll(async () => { + await deleteCollectionViaApi(childCollectionAlias) + await deleteCollectionViaApi(parentCollectionAlias) + await deleteCollectionViaApi(standaloneCollectionAlias) + }) + + test('should return the directly assigned collection storage driver', async () => { + await sut.setCollectionStorageDriver(standaloneCollectionAlias, 'LocalStack') + + const storageDriver = await sut.getCollectionStorageDriver(standaloneCollectionAlias) + + expect(storageDriver.name).toBe('localstack1') + expect(storageDriver.label).toBe('LocalStack') + expect(storageDriver.type).toBe('s3') + expect(storageDriver.directUpload).toBe(true) + expect(storageDriver.directDownload).toBe(true) + expect(storageDriver.uploadOutOfBand).toBe(false) + }) + + test('should return the effective collection storage driver through inheritance', async () => { + await sut.setCollectionStorageDriver(parentCollectionAlias, 'LocalStack') + + const storageDriver = await sut.getCollectionStorageDriver(childCollectionAlias, true) + + expect(storageDriver.name).toBe('localstack1') + expect(storageDriver.label).toBe('LocalStack') + expect(storageDriver.type).toBe('s3') + }) + + test('should set and then clear the collection storage driver', async () => { + const setMessage = await sut.setCollectionStorageDriver( + standaloneCollectionAlias, + 'LocalStack' + ) + expect(setMessage).toContain('LocalStack') + + const assignedDriver = await sut.getCollectionStorageDriver(standaloneCollectionAlias) + expect(assignedDriver.name).toBe('localstack1') + + const deleteMessage = await sut.deleteCollectionStorageDriver(standaloneCollectionAlias) + expect(deleteMessage.toLowerCase()).toContain('local') + + const effectiveDriver = await sut.getCollectionStorageDriver(standaloneCollectionAlias, true) + expect(effectiveDriver.name).toBe('local') + expect(effectiveDriver.label).toBe('Local') + expect(effectiveDriver.type).toBe('file') + }) + + test('should return the allowed collection storage drivers', async () => { + const allowedDrivers = await sut.getAllowedCollectionStorageDrivers(standaloneCollectionAlias) + + expect(allowedDrivers).toMatchObject({ + LocalStack: 'localstack1', + Local: 'local', + Filesystem: 'file1' + }) + }) + }) }) diff --git a/test/testHelpers/collections/collectionHelper.ts b/test/testHelpers/collections/collectionHelper.ts index b19b668f..6d274deb 100644 --- a/test/testHelpers/collections/collectionHelper.ts +++ b/test/testHelpers/collections/collectionHelper.ts @@ -134,7 +134,7 @@ export async function setStorageDriverViaApi( ): Promise { try { return await axios.put( - `${TestConstants.TEST_API_URL}/admin/dataverse/${collectionAlias}/storageDriver`, + `${TestConstants.TEST_API_URL}/dataverses/${collectionAlias}/storageDriver`, driverLabel, { headers: { 'Content-Type': 'text/plain', 'X-Dataverse-Key': process.env.TEST_API_KEY } diff --git a/test/unit/collections/CollectionsRepository.test.ts b/test/unit/collections/CollectionsRepository.test.ts index d099acb1..74796867 100644 --- a/test/unit/collections/CollectionsRepository.test.ts +++ b/test/unit/collections/CollectionsRepository.test.ts @@ -17,6 +17,8 @@ import { import { TestConstants } from '../../testHelpers/TestConstants' import { ReadError, WriteError } from '../../../src' import { ROOT_COLLECTION_ID } from '../../../src/collections/domain/models/Collection' +import { AllowedStorageDrivers } from '../../../src/collections/domain/models/AllowedStorageDrivers' +import { StorageDriver } from '../../../src/core/domain/models/StorageDriver' import { createCollectionUserPermissionsModel, createCollectionUserPermissionsPayload @@ -45,6 +47,46 @@ import { describe('CollectionsRepository', () => { const sut: CollectionsRepository = new CollectionsRepository() + const testStorageDriver: StorageDriver = { + name: 'local', + type: 'filesystem', + label: 'Local Storage', + directUpload: true, + directDownload: true, + uploadOutOfBand: false + } + const testStorageDriverResponse = { + data: { + status: 'OK', + data: testStorageDriver + } + } + const testStorageDriverList: AllowedStorageDrivers = { + Filesystem: 'file1', + LocalStack: 'localstack1' + } + const testStorageDriverListResponse = { + data: { + status: 'OK', + data: testStorageDriverList + } + } + const testStorageDriverMessageResponse = { + data: { + status: 'OK', + data: { + message: 'Storage set to: local/Local Storage' + } + } + } + const testDeleteStorageDriverMessageResponse = { + data: { + status: 'OK', + data: { + message: 'Storage driver cleared. Falling back to default: local' + } + } + } const testCollectionSuccessfulResponse = { data: { status: 'OK', @@ -144,6 +186,130 @@ describe('CollectionsRepository', () => { }) }) + describe('getCollectionStorageDriver', () => { + test('should return collection storage driver when request is successful', async () => { + jest.spyOn(axios, 'get').mockResolvedValue(testStorageDriverResponse) + const expectedApiEndpoint = `${TestConstants.TEST_API_URL}/dataverses/test-collection/storageDriver` + const expectedRequestConfigApiKey = { + params: { + getEffective: false + }, + headers: TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_API_KEY.headers + } + + const actual = await sut.getCollectionStorageDriver('test-collection') + + expect(axios.get).toHaveBeenCalledWith(expectedApiEndpoint, expectedRequestConfigApiKey) + expect(actual).toStrictEqual(testStorageDriver) + }) + + test('should include getEffective query param when requested', async () => { + jest.spyOn(axios, 'get').mockResolvedValue(testStorageDriverResponse) + const expectedApiEndpoint = `${TestConstants.TEST_API_URL}/dataverses/test-collection/storageDriver` + const expectedRequestConfigApiKey = { + params: { + getEffective: true + }, + headers: TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_API_KEY.headers + } + + await sut.getCollectionStorageDriver('test-collection', true) + + expect(axios.get).toHaveBeenCalledWith(expectedApiEndpoint, expectedRequestConfigApiKey) + }) + + test('should return error on repository read error', async () => { + jest.spyOn(axios, 'get').mockRejectedValue(TestConstants.TEST_ERROR_RESPONSE) + let error = undefined as unknown as ReadError + + await sut.getCollectionStorageDriver('test-collection').catch((e) => (error = e)) + + expect(error).toBeInstanceOf(Error) + }) + }) + + describe('setCollectionStorageDriver', () => { + test('should call the API with a plain text request payload', async () => { + jest.spyOn(axios, 'put').mockResolvedValue(testStorageDriverMessageResponse) + const expectedApiEndpoint = `${TestConstants.TEST_API_URL}/dataverses/test-collection/storageDriver` + const expectedRequestConfigApiKey = { + params: {}, + headers: { + 'Content-Type': 'text/plain', + 'X-Dataverse-Key': TestConstants.TEST_DUMMY_API_KEY + } + } + + const actual = await sut.setCollectionStorageDriver('test-collection', 'Local Storage') + + expect(axios.put).toHaveBeenCalledWith( + expectedApiEndpoint, + 'Local Storage', + expectedRequestConfigApiKey + ) + expect(actual).toBe('Storage set to: local/Local Storage') + }) + + test('should return error result on error response', async () => { + jest.spyOn(axios, 'put').mockRejectedValue(TestConstants.TEST_ERROR_RESPONSE) + let error = undefined as unknown as WriteError + + await sut + .setCollectionStorageDriver('test-collection', 'Local Storage') + .catch((e) => (error = e)) + + expect(error).toBeInstanceOf(Error) + }) + }) + + describe('deleteCollectionStorageDriver', () => { + test('should delete collection storage driver when request is successful', async () => { + jest.spyOn(axios, 'delete').mockResolvedValue(testDeleteStorageDriverMessageResponse) + const expectedApiEndpoint = `${TestConstants.TEST_API_URL}/dataverses/test-collection/storageDriver` + + const actual = await sut.deleteCollectionStorageDriver('test-collection') + + expect(axios.delete).toHaveBeenCalledWith( + expectedApiEndpoint, + TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_API_KEY + ) + expect(actual).toBe('Storage driver cleared. Falling back to default: local') + }) + + 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.deleteCollectionStorageDriver('test-collection').catch((e) => (error = e)) + + expect(error).toBeInstanceOf(Error) + }) + }) + + describe('getAllowedCollectionStorageDrivers', () => { + test('should return allowed collection storage drivers when request is successful', async () => { + jest.spyOn(axios, 'get').mockResolvedValue(testStorageDriverListResponse) + const expectedApiEndpoint = `${TestConstants.TEST_API_URL}/dataverses/test-collection/allowedStorageDrivers` + + const actual = await sut.getAllowedCollectionStorageDrivers('test-collection') + + expect(axios.get).toHaveBeenCalledWith( + expectedApiEndpoint, + TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_API_KEY + ) + expect(actual).toStrictEqual(testStorageDriverList) + }) + + test('should return error on repository read error', async () => { + jest.spyOn(axios, 'get').mockRejectedValue(TestConstants.TEST_ERROR_RESPONSE) + let error = undefined as unknown as ReadError + + await sut.getAllowedCollectionStorageDrivers('test-collection').catch((e) => (error = e)) + + expect(error).toBeInstanceOf(Error) + }) + }) + describe('createCollection', () => { const testNewCollection = createCollectionDTO() diff --git a/test/unit/collections/DeleteCollectionStorageDriver.test.ts b/test/unit/collections/DeleteCollectionStorageDriver.test.ts new file mode 100644 index 00000000..f654a17d --- /dev/null +++ b/test/unit/collections/DeleteCollectionStorageDriver.test.ts @@ -0,0 +1,30 @@ +import { WriteError } from '../../../src' +import { ICollectionsRepository } from '../../../src/collections/domain/repositories/ICollectionsRepository' +import { DeleteCollectionStorageDriver } from '../../../src/collections/domain/useCases/DeleteCollectionStorageDriver' + +describe('DeleteCollectionStorageDriver (unit)', () => { + test('should delete collection storage driver on repository success', async () => { + const collectionsRepositoryStub: ICollectionsRepository = {} as ICollectionsRepository + collectionsRepositoryStub.deleteCollectionStorageDriver = jest + .fn() + .mockResolvedValue('Storage driver cleared. Falling back to default: local') + const sut = new DeleteCollectionStorageDriver(collectionsRepositoryStub) + + const actual = await sut.execute('test-collection') + + expect(actual).toBe('Storage driver cleared. Falling back to default: local') + expect(collectionsRepositoryStub.deleteCollectionStorageDriver).toHaveBeenCalledWith( + 'test-collection' + ) + }) + + test('should return error result on repository error', async () => { + const collectionsRepositoryStub: ICollectionsRepository = {} as ICollectionsRepository + collectionsRepositoryStub.deleteCollectionStorageDriver = jest + .fn() + .mockRejectedValue(new WriteError('[404] Collection not found')) + const sut = new DeleteCollectionStorageDriver(collectionsRepositoryStub) + + await expect(sut.execute('test-collection')).rejects.toThrow(WriteError) + }) +}) diff --git a/test/unit/collections/GetAllowedCollectionStorageDrivers.test.ts b/test/unit/collections/GetAllowedCollectionStorageDrivers.test.ts new file mode 100644 index 00000000..eb802980 --- /dev/null +++ b/test/unit/collections/GetAllowedCollectionStorageDrivers.test.ts @@ -0,0 +1,36 @@ +import { ReadError } from '../../../src' +import { AllowedStorageDrivers } from '../../../src/collections/domain/models/AllowedStorageDrivers' +import { ICollectionsRepository } from '../../../src/collections/domain/repositories/ICollectionsRepository' +import { GetAllowedCollectionStorageDrivers } from '../../../src/collections/domain/useCases/GetAllowedCollectionStorageDrivers' + +describe('GetAllowedCollectionStorageDrivers (unit)', () => { + const testStorageDrivers: AllowedStorageDrivers = { + Filesystem: 'file1', + LocalStack: 'localstack1' + } + + test('should return allowed collection storage drivers on repository success', async () => { + const collectionsRepositoryStub: ICollectionsRepository = {} as ICollectionsRepository + collectionsRepositoryStub.getAllowedCollectionStorageDrivers = jest + .fn() + .mockResolvedValue(testStorageDrivers) + const sut = new GetAllowedCollectionStorageDrivers(collectionsRepositoryStub) + + const actual = await sut.execute('test-collection') + + expect(actual).toEqual(testStorageDrivers) + expect(collectionsRepositoryStub.getAllowedCollectionStorageDrivers).toHaveBeenCalledWith( + 'test-collection' + ) + }) + + test('should return error result on repository error', async () => { + const collectionsRepositoryStub: ICollectionsRepository = {} as ICollectionsRepository + collectionsRepositoryStub.getAllowedCollectionStorageDrivers = jest + .fn() + .mockRejectedValue(new ReadError('[404] Collection not found')) + const sut = new GetAllowedCollectionStorageDrivers(collectionsRepositoryStub) + + await expect(sut.execute('test-collection')).rejects.toThrow(ReadError) + }) +}) diff --git a/test/unit/collections/GetCollectionStorageDriver.test.ts b/test/unit/collections/GetCollectionStorageDriver.test.ts new file mode 100644 index 00000000..8472f149 --- /dev/null +++ b/test/unit/collections/GetCollectionStorageDriver.test.ts @@ -0,0 +1,56 @@ +import { ReadError } from '../../../src' +import { ICollectionsRepository } from '../../../src/collections/domain/repositories/ICollectionsRepository' +import { GetCollectionStorageDriver } from '../../../src/collections/domain/useCases/GetCollectionStorageDriver' +import { StorageDriver } from '../../../src/core/domain/models/StorageDriver' + +describe('GetCollectionStorageDriver (unit)', () => { + const testStorageDriver: StorageDriver = { + name: 'local', + type: 'filesystem', + label: 'Local Storage', + directUpload: true, + directDownload: true, + uploadOutOfBand: false + } + + test('should return collection storage driver on repository success', async () => { + const collectionsRepositoryStub: ICollectionsRepository = {} as ICollectionsRepository + collectionsRepositoryStub.getCollectionStorageDriver = jest + .fn() + .mockResolvedValue(testStorageDriver) + const sut = new GetCollectionStorageDriver(collectionsRepositoryStub) + + const actual = await sut.execute('test-collection') + + expect(actual).toEqual(testStorageDriver) + expect(collectionsRepositoryStub.getCollectionStorageDriver).toHaveBeenCalledWith( + 'test-collection', + false + ) + }) + + test('should request effective storage driver when requested', async () => { + const collectionsRepositoryStub: ICollectionsRepository = {} as ICollectionsRepository + collectionsRepositoryStub.getCollectionStorageDriver = jest + .fn() + .mockResolvedValue(testStorageDriver) + const sut = new GetCollectionStorageDriver(collectionsRepositoryStub) + + await sut.execute('test-collection', true) + + expect(collectionsRepositoryStub.getCollectionStorageDriver).toHaveBeenCalledWith( + 'test-collection', + true + ) + }) + + test('should return error result on repository error', async () => { + const collectionsRepositoryStub: ICollectionsRepository = {} as ICollectionsRepository + collectionsRepositoryStub.getCollectionStorageDriver = jest + .fn() + .mockRejectedValue(new ReadError('[404] Collection not found')) + const sut = new GetCollectionStorageDriver(collectionsRepositoryStub) + + await expect(sut.execute('test-collection')).rejects.toThrow(ReadError) + }) +}) diff --git a/test/unit/collections/SetCollectionStorageDriver.test.ts b/test/unit/collections/SetCollectionStorageDriver.test.ts new file mode 100644 index 00000000..ac4fcf13 --- /dev/null +++ b/test/unit/collections/SetCollectionStorageDriver.test.ts @@ -0,0 +1,31 @@ +import { WriteError } from '../../../src' +import { ICollectionsRepository } from '../../../src/collections/domain/repositories/ICollectionsRepository' +import { SetCollectionStorageDriver } from '../../../src/collections/domain/useCases/SetCollectionStorageDriver' + +describe('SetCollectionStorageDriver (unit)', () => { + test('should set collection storage driver on repository success', async () => { + const collectionsRepositoryStub: ICollectionsRepository = {} as ICollectionsRepository + collectionsRepositoryStub.setCollectionStorageDriver = jest + .fn() + .mockResolvedValue('Storage set to: local/Local Storage') + const sut = new SetCollectionStorageDriver(collectionsRepositoryStub) + + const actual = await sut.execute('test-collection', 'Local Storage') + + expect(actual).toBe('Storage set to: local/Local Storage') + expect(collectionsRepositoryStub.setCollectionStorageDriver).toHaveBeenCalledWith( + 'test-collection', + 'Local Storage' + ) + }) + + test('should return error result on repository error', async () => { + const collectionsRepositoryStub: ICollectionsRepository = {} as ICollectionsRepository + collectionsRepositoryStub.setCollectionStorageDriver = jest + .fn() + .mockRejectedValue(new WriteError('[403] Permission denied')) + const sut = new SetCollectionStorageDriver(collectionsRepositoryStub) + + await expect(sut.execute('test-collection', 'Local Storage')).rejects.toThrow(WriteError) + }) +}) diff --git a/test/unit/datasets/GetDatasetStorageDriver.test.ts b/test/unit/datasets/GetDatasetStorageDriver.test.ts index bd55d164..02570237 100644 --- a/test/unit/datasets/GetDatasetStorageDriver.test.ts +++ b/test/unit/datasets/GetDatasetStorageDriver.test.ts @@ -1,6 +1,6 @@ import { GetDatasetStorageDriver } from '../../../src/datasets/domain/useCases/GetDatasetStorageDriver' import { IDatasetsRepository } from '../../../src/datasets/domain/repositories/IDatasetsRepository' -import { StorageDriver } from '../../../src/datasets/domain/models/StorageDriver' +import { StorageDriver } from '../../../src/core/domain/models/StorageDriver' import { ReadError } from '../../../src/core/domain/repositories/ReadError' describe('GetDatasetStorageDriver (unit)', () => {