Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel

- Datasets: Added `updateDatasetLicense` use case and repository method to support Dataverse endpoint `PUT /datasets/{id}/license`, for updating dataset license or custom terms.
- Datasets: Added `getDatasetStorageDriver` use case and repository method to support Dataverse endpoint `GET /datasets/{identifier}/storageDriver`, for retrieving dataset storage driver configuration with properties: name, type, label, directUpload, directDownload, and uploadOutOfBand.
- Datasets: Added `updateDatasetLicense` use case and repository method to support Dataverse endpoint `PUT /datasets/{id}/license`, for updating dataset license or custom terms
- Datasets: Added `getDatasetUploadLimits` use case and repository method to support Dataverse endpoint `GET /datasets/{id}/uploadlimits`, for retrieving remaining storage upload quotas, if present.
- New Use Case: [Get Collections For Linking Use Case](./docs/useCases.md#get-collections-for-linking).
- New Use Case: [Create a Template](./docs/useCases.md#create-a-template) under Templates.
- New Use Case: [Get a Template](./docs/useCases.md#get-a-template) under Templates.
Expand Down
25 changes: 25 additions & 0 deletions docs/useCases.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ The different use cases currently available in the package are classified below,
- [Get Dataset Storage Driver](#get-dataset-storage-driver)
- [Get Dataset Available Dataset Types](#get-dataset-available-dataset-types)
- [Get Dataset Available Dataset Type](#get-dataset-available-dataset-type)
- [Get Dataset Upload Limits](#get-dataset-upload-limits)
- [Datasets write use cases](#datasets-write-use-cases)
- [Create a Dataset](#create-a-dataset)
- [Update a Dataset](#update-a-dataset)
Expand Down Expand Up @@ -1516,6 +1517,30 @@ deleteDatasetType.execute(datasetTypeId).then(() => {

_See [use case](../src/datasets/domain/useCases/DeleteDatasetType.ts) implementation_.

#### Get Dataset Upload Limits

Returns a [DatasetUploadLimits](../src/datasets/domain/models/DatasetUploadLimits.ts) instance with the remaining dataset storage and/or file upload quotas, if present.

##### Example call:

```typescript
import { getDatasetUploadLimits } from '@iqss/dataverse-client-javascript'

/* ... */

const datasetId = 'doi:10.77777/FK2/AAAAAA'

getDatasetUploadLimits.execute(datasetId).then((uploadLimits: DatasetUploadLimits) => {
/* ... */
})

/* ... */
```

_See [use case](../src/datasets/domain/useCases/GetDatasetUploadLimits.ts) implementation_.

If the backend does not define any quota limits for the dataset, the returned object can be empty (`{}`).

## Files

### Files read use cases
Expand Down
4 changes: 4 additions & 0 deletions src/datasets/domain/models/DatasetUploadLimits.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface DatasetUploadLimits {
numberOfFilesRemaining?: number
storageQuotaRemaining?: number
}
2 changes: 2 additions & 0 deletions src/datasets/domain/repositories/IDatasetsRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { TermsOfAccess } from '../models/Dataset'
import { DatasetLicenseUpdateRequest } from '../dtos/DatasetLicenseUpdateRequest'
import { DatasetTypeDTO } from '../dtos/DatasetTypeDTO'
import { StorageDriver } from '../models/StorageDriver'
import { DatasetUploadLimits } from '../models/DatasetUploadLimits'

export interface IDatasetsRepository {
getDataset(
Expand Down Expand Up @@ -102,4 +103,5 @@ export interface IDatasetsRepository {
payload: DatasetLicenseUpdateRequest
): Promise<void>
getDatasetStorageDriver(datasetId: number | string): Promise<StorageDriver>
getDatasetUploadLimits(datasetId: number | string): Promise<DatasetUploadLimits>
}
21 changes: 21 additions & 0 deletions src/datasets/domain/useCases/GetDatasetUploadLimits.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { UseCase } from '../../../core/domain/useCases/UseCase'
import { IDatasetsRepository } from '../repositories/IDatasetsRepository'
import { DatasetUploadLimits } from '../models/DatasetUploadLimits'

export class GetDatasetUploadLimits implements UseCase<DatasetUploadLimits> {
private datasetsRepository: IDatasetsRepository

constructor(datasetsRepository: IDatasetsRepository) {
this.datasetsRepository = datasetsRepository
}

/**
* Returns the remaining dataset storage and/or file upload quotas (if present).
*
* @param {number | string} datasetId - The dataset identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers).
* @returns {Promise<DatasetUploadLimits>}
*/
async execute(datasetId: number | string): Promise<DatasetUploadLimits> {
return this.datasetsRepository.getDatasetUploadLimits(datasetId)
}
}
6 changes: 5 additions & 1 deletion src/datasets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { GetDatasetCitationInOtherFormats } from './domain/useCases/GetDatasetCi
import { UpdateTermsOfAccess } from './domain/useCases/UpdateTermsOfAccess'
import { UpdateDatasetLicense } from './domain/useCases/UpdateDatasetLicense'
import { GetDatasetStorageDriver } from './domain/useCases/GetDatasetStorageDriver'
import { GetDatasetUploadLimits } from './domain/useCases/GetDatasetUploadLimits'

const datasetsRepository = new DatasetsRepository()

Expand Down Expand Up @@ -84,6 +85,7 @@ const getDatasetCitationInOtherFormats = new GetDatasetCitationInOtherFormats(da
const updateTermsOfAccess = new UpdateTermsOfAccess(datasetsRepository)
const updateDatasetLicense = new UpdateDatasetLicense(datasetsRepository)
const getDatasetStorageDriver = new GetDatasetStorageDriver(datasetsRepository)
const getDatasetUploadLimits = new GetDatasetUploadLimits(datasetsRepository)

export {
getDataset,
Expand Down Expand Up @@ -115,7 +117,8 @@ export {
setAvailableLicensesForDatasetType,
deleteDatasetType,
updateDatasetLicense,
getDatasetStorageDriver
getDatasetStorageDriver,
getDatasetUploadLimits
}
export { DatasetNotNumberedVersion } from './domain/models/DatasetNotNumberedVersion'
export { DatasetUserPermissions } from './domain/models/DatasetUserPermissions'
Expand Down Expand Up @@ -155,3 +158,4 @@ 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 { DatasetUploadLimits } from './domain/models/DatasetUploadLimits'
12 changes: 12 additions & 0 deletions src/datasets/infra/repositories/DatasetsRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { transformTermsOfAccessToUpdatePayload } from './transformers/termsOfAcc
import { DatasetLicenseUpdateRequest } from '../../domain/dtos/DatasetLicenseUpdateRequest'
import { DatasetTypeDTO } from '../../domain/dtos/DatasetTypeDTO'
import { StorageDriver } from '../../domain/models/StorageDriver'
import { DatasetUploadLimits } from '../../domain/models/DatasetUploadLimits'

export interface GetAllDatasetPreviewsQueryParams {
per_page?: number
Expand Down Expand Up @@ -511,4 +512,15 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi
throw error
})
}

public async getDatasetUploadLimits(datasetId: number | string): Promise<DatasetUploadLimits> {
return this.doGet(
this.buildApiEndpoint(this.datasetsResourceName, 'uploadlimits', datasetId),
true
)
.then((response) => (response.data?.data?.uploadLimits ?? {}) as DatasetUploadLimits)
.catch((error) => {
throw error
})
}
}
49 changes: 48 additions & 1 deletion test/integration/datasets/DatasetsRepository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import {
waitForDatasetsIndexedInSolr,
deletePublishedDatasetViaApi,
deaccessionDatasetViaApi,
createDatasetLicenseModel
createDatasetLicenseModel,
setDatasetStorageSizeViaApi,
setUseStorageQuotasViaApi
} from '../../testHelpers/datasets/datasetHelper'
import { ReadError } from '../../../src/core/domain/repositories/ReadError'
import {
Expand Down Expand Up @@ -2229,4 +2231,49 @@ describe('DatasetsRepository', () => {
expect(typeof storageDriver.uploadOutOfBand).toBe('boolean')
})
})

describe('getDatasetUploadLimits', () => {
const testCollectionAlias = 'UploadLimitsQuotaDataset'
let testDatasetIds: CreatedDatasetIdentifiers
const testCollectionStorageQuotaInBytes = 1000

beforeAll(async () => {
await createCollectionViaApi(testCollectionAlias)
await publishCollectionViaApi(testCollectionAlias)
testDatasetIds = await createDataset.execute(
TestConstants.TEST_NEW_DATASET_DTO,
testCollectionAlias
)
await setUseStorageQuotasViaApi(true)
await publishDatasetViaApi(testDatasetIds.numericId)
await waitForNoLocks(testDatasetIds.numericId, 10)
})

afterAll(async () => {
await deletePublishedDatasetViaApi(testDatasetIds.persistentId).catch(() => undefined)
await deleteCollectionViaApi(testCollectionAlias).catch(() => undefined)
})

test('should return empty for dataset (if DatasetStorageSize is not set)', async () => {
const uploadLimits = await sut.getDatasetUploadLimits(testDatasetIds.numericId)

expect(uploadLimits).toEqual({})
})

test('should return upload limits for dataset (if DatasetStorageSize is set)', async () => {
await setDatasetStorageSizeViaApi(testDatasetIds.numericId, testCollectionStorageQuotaInBytes)
const uploadLimits = await sut.getDatasetUploadLimits(testDatasetIds.numericId)

expect(uploadLimits).toBeDefined()
expect(uploadLimits.storageQuotaRemaining).toBeLessThanOrEqual(
testCollectionStorageQuotaInBytes
)
})

test('should return error when dataset does not exist', async () => {
await expect(sut.getDatasetUploadLimits(nonExistentTestDatasetId)).rejects.toBeInstanceOf(
ReadError
)
})
})
})
39 changes: 39 additions & 0 deletions test/testHelpers/datasets/datasetHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,45 @@ export const publishDatasetViaApi = async (datasetId: number): Promise<AxiosResp
}
}

export const setUseStorageQuotasViaApi = async (
useStorageQuotas: boolean
): Promise<AxiosResponse> => {
try {
return await axios.put(
`${TestConstants.TEST_API_URL}/admin/settings/:UseStorageQuotas`,
`${useStorageQuotas}`,
{
headers: {
'Content-Type': 'text/plain',
'X-Dataverse-Key': process.env.TEST_API_KEY
}
}
)
} catch (error) {
throw new Error(`Error while setting UseStorageQuotas to ${useStorageQuotas}`)
}
}

export const setDatasetStorageSizeViaApi = async (
datasetId: number | string,
sizeInBytes: number
): Promise<AxiosResponse> => {
try {
return await axios.put(
`${TestConstants.TEST_API_URL}/datasets/${datasetId}/storage/quota`,
`${sizeInBytes}`,
{
headers: {
'Content-Type': 'text/plain',
'X-Dataverse-Key': process.env.TEST_API_KEY
}
}
)
} catch (error) {
throw new Error(`Error while setting storage quota for dataset ${datasetId}`)
}
}

export const deaccessionDatasetViaApi = async (
datasetId: number,
versionId: string
Expand Down
43 changes: 43 additions & 0 deletions test/unit/datasets/GetDatasetUploadLimits.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { GetDatasetUploadLimits } from '../../../src/datasets/domain/useCases/GetDatasetUploadLimits'
import { IDatasetsRepository } from '../../../src/datasets/domain/repositories/IDatasetsRepository'
import { DatasetUploadLimits } from '../../../src/datasets/domain/models/DatasetUploadLimits'
import { ReadError } from '../../../src/core/domain/repositories/ReadError'

describe('GetDatasetUploadLimits (unit)', () => {
const testUploadLimits: DatasetUploadLimits = {
numberOfFilesRemaining: 25,
storageQuotaRemaining: 21474836480
}

test('should return upload limits on repository success', async () => {
const datasetsRepositoryStub: IDatasetsRepository = {} as IDatasetsRepository
datasetsRepositoryStub.getDatasetUploadLimits = jest.fn().mockResolvedValue(testUploadLimits)
const sut = new GetDatasetUploadLimits(datasetsRepositoryStub)

const actual = await sut.execute(1)

expect(actual).toEqual(testUploadLimits)
expect(actual.numberOfFilesRemaining).toBe(25)
expect(actual.storageQuotaRemaining).toBe(21474836480)
})

test('should return upload limits when using persistent id', async () => {
const datasetsRepositoryStub: IDatasetsRepository = {} as IDatasetsRepository
datasetsRepositoryStub.getDatasetUploadLimits = jest.fn().mockResolvedValue(testUploadLimits)
const sut = new GetDatasetUploadLimits(datasetsRepositoryStub)

const actual = await sut.execute('doi:10.77777/FK2/AAAAAA')

expect(actual).toEqual(testUploadLimits)
})

test('should return error result on repository error', async () => {
const datasetsRepositoryStub: IDatasetsRepository = {} as IDatasetsRepository
datasetsRepositoryStub.getDatasetUploadLimits = jest
.fn()
.mockRejectedValue(new ReadError('[404] Dataset not found'))
const sut = new GetDatasetUploadLimits(datasetsRepositoryStub)

await expect(sut.execute(1)).rejects.toThrow(ReadError)
})
})