Skip to content
Merged
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
23 changes: 23 additions & 0 deletions docs/useCases.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ The different use cases currently available in the package are classified below,
- [Files write use cases](#files-write-use-cases)
- [File Uploading Use Cases](#file-uploading-use-cases)
- [Delete a File](#delete-a-file)
- [Restrict or Unrestrict a File](#restrict-or-unrestrict-a-file)
- [Metadata Blocks](#metadata-blocks)
- [Metadata Blocks read use cases](#metadata-blocks-read-use-cases)
- [Get All Facetable Metadata Fields](#get-all-facetable-metadata-fields)
Expand Down Expand Up @@ -1264,6 +1265,28 @@ Note that the behavior of deleting files depends on if the dataset has ever been
- If the dataset has published, the file is deleted from the draft (and future published versions).
- If the dataset has published, the deleted file can still be downloaded because it was part of a published version.

#### Restrict or Unrestrict a File

Restrict or unrestrict an existing file.

##### Example call:

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

/* ... */

const fileId = 12345

restrictFile.execute(fileId, true)

/* ... */
```

_See [use case](../src/files/domain/useCases/RestrictFile.ts) implementation_.

The `fileId` parameter can be a string, for persistent identifiers, or a number, for numeric identifiers.

## Metadata Blocks

### Metadata Blocks read use cases
Expand Down
16 changes: 11 additions & 5 deletions src/core/infra/repositories/ApiRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export abstract class ApiRepository {

public async doPost(
apiEndpoint: string,
data: string | object,
data: string | object | boolean,
queryParams: object = {},
contentType: string = ApiConstants.CONTENT_TYPE_APPLICATION_JSON
): Promise<AxiosResponse> {
Expand All @@ -29,7 +29,7 @@ export abstract class ApiRepository {

public async doPut(
apiEndpoint: string,
data: string | object,
data: string | object | boolean,
queryParams: object = {},
contentType: string = ApiConstants.CONTENT_TYPE_APPLICATION_JSON
): Promise<AxiosResponse> {
Expand Down Expand Up @@ -70,12 +70,18 @@ export abstract class ApiRepository {
private async doRequest(
method: 'post' | 'put',
apiEndpoint: string,
data: string | object,
data: string | object | boolean,
queryParams: object = {},
contentType: string = ApiConstants.CONTENT_TYPE_APPLICATION_JSON
): Promise<AxiosResponse> {
const requestData =
contentType == ApiConstants.CONTENT_TYPE_APPLICATION_JSON ? JSON.stringify(data) : data
let requestData = data

if (contentType === ApiConstants.CONTENT_TYPE_APPLICATION_JSON) {
if (typeof data === 'object' || typeof data === 'boolean') {
requestData = JSON.stringify(data)
}
}

const requestUrl = buildRequestUrl(apiEndpoint)
const requestConfig = buildRequestConfig(true, queryParams, contentType)

Expand Down
2 changes: 2 additions & 0 deletions src/files/domain/repositories/IFilesRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,6 @@ export interface IFilesRepository {
): Promise<undefined>

deleteFile(fileId: number | string): Promise<undefined>

restrictFile(fileId: number | string, restrict: boolean): Promise<undefined>
}
18 changes: 18 additions & 0 deletions src/files/domain/useCases/RestrictFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { IFilesRepository } from '../repositories/IFilesRepository'
import { UseCase } from '../../../core/domain/useCases/UseCase'

export class RestrictFile implements UseCase<void> {
constructor(private readonly filesRepository: IFilesRepository) {}

/**
* Restrict or unrestrict an existing file.
* More detailed information about the file restriction behavior can be found in https://guides.dataverse.org/en/latest/api/native-api.html#restrict-files
*
* @param {number | string} [fileId] - The File identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers).
* @param {boolean} [restrict] - A boolean value that indicates whether the file should be restricted or unrestricted.
* @returns {Promise<void>} -This method does not return anything upon successful completion.
*/
async execute(fileId: number | string, restrict: boolean): Promise<void> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the UI, the restrict file modal includes an option of changing the default fileAccessRequest value, and adding termsOfAccessForRestrictedFiles. (these values are also in the Terms tab). Do we want to add this as optional params to the restrict method? Or from the frontend, should it be handled by making a separate API call to update Dataset Terms of Use?

Screenshot 2025-02-21 at 1 31 37 PM

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question, the API is not accepting params and I think it's ok.
A second API call to update Dataset Terms of Use seems reasonable.
We can do that separately while working on the frontend integration. Is there a use case for doing that in Js-dataverse?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, we don't have a use case for updating terms of use yet, so yes would be good to work on that separately.

return await this.filesRepository.restrictFile(fileId, restrict)
}
}
5 changes: 4 additions & 1 deletion src/files/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { UploadFile } from './domain/useCases/UploadFile'
import { DirectUploadClient } from './infra/clients/DirectUploadClient'
import { AddUploadedFilesToDataset } from './domain/useCases/AddUploadedFilesToDataset'
import { DeleteFile } from './domain/useCases/DeleteFile'
import { RestrictFile } from './domain/useCases/RestrictFile'

const filesRepository = new FilesRepository()
const directUploadClient = new DirectUploadClient(filesRepository)
Expand All @@ -28,6 +29,7 @@ const getFileCitation = new GetFileCitation(filesRepository)
const uploadFile = new UploadFile(directUploadClient)
const addUploadedFilesToDataset = new AddUploadedFilesToDataset(filesRepository)
const deleteFile = new DeleteFile(filesRepository)
const restrictFile = new RestrictFile(filesRepository)

export {
getDatasetFiles,
Expand All @@ -41,7 +43,8 @@ export {
getFileCitation,
uploadFile,
addUploadedFilesToDataset,
deleteFile
deleteFile,
restrictFile
}

export { FileModel as File, FileEmbargo, FileChecksum } from './domain/models/FileModel'
Expand Down
8 changes: 8 additions & 0 deletions src/files/infra/repositories/FilesRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,4 +301,12 @@ export class FilesRepository extends ApiRepository implements IFilesRepository {
throw error
})
}

public async restrictFile(fileId: number | string, restrict: boolean): Promise<undefined> {
return this.doPut(this.buildApiEndpoint(this.filesResourceName, 'restrict', fileId), restrict)
.then(() => undefined)
.catch((error) => {
throw error
})
}
}
4 changes: 2 additions & 2 deletions test/functional/collections/GetCollectionItems.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ describe('execute', () => {
try {
const actual = await getCollectionItems.execute(testCollectionAlias)

const actualFilePreview = actual.items[0] as FilePreview
const actualDatasetPreview = actual.items[1] as DatasetPreview
const actualFilePreview = actual.items[1] as FilePreview
const actualDatasetPreview = actual.items[0] as DatasetPreview

expect(actualFilePreview.name).toBe('test-file-1.txt')
expect(actualDatasetPreview.title).toBe('Dataset created using the createDataset use case')
Expand Down
108 changes: 108 additions & 0 deletions test/functional/files/RestrictFile.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import {
ApiConfig,
createDataset,
CreatedDatasetIdentifiers,
restrictFile,
getDatasetFiles,
WriteError
} from '../../../src'
import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig'
import {
createCollectionViaApi,
deleteCollectionViaApi
} from '../../testHelpers/collections/collectionHelper'
import { deleteUnpublishedDatasetViaApi } from '../../testHelpers/datasets/datasetHelper'
import { uploadFileViaApi } from '../../testHelpers/files/filesHelper'
import { TestConstants } from '../../testHelpers/TestConstants'

describe('execute', () => {
const testCollectionAlias = 'restrictFileFunctionalTest'
let testDatasetIds: CreatedDatasetIdentifiers
const testTextFile1Name = 'test-file-1.txt'

beforeAll(async () => {
ApiConfig.init(
TestConstants.TEST_API_URL,
DataverseApiAuthMechanism.API_KEY,
process.env.TEST_API_KEY
)
await createCollectionViaApi(testCollectionAlias)

try {
testDatasetIds = await createDataset.execute(
TestConstants.TEST_NEW_DATASET_DTO,
testCollectionAlias
)
} catch (error) {
throw new Error('Tests beforeAll(): Error while creating test dataset')
}
await uploadFileViaApi(testDatasetIds.numericId, testTextFile1Name).catch(() => {
throw new Error(`Tests beforeAll(): Error while uploading file ${testTextFile1Name}`)
})
})

afterAll(async () => {
try {
await deleteUnpublishedDatasetViaApi(testDatasetIds.numericId)
} catch (error) {
throw new Error('Tests afterAll(): Error while deleting test dataset')
}
try {
await deleteCollectionViaApi(testCollectionAlias)
} catch (error) {
throw new Error('Tests afterAll(): Error while deleting test collection')
}
})

test('should successfully restrict a file', async () => {
try {
const datasetFiles = await getDatasetFiles.execute(testDatasetIds.numericId)

await restrictFile.execute(datasetFiles.files[0].id, true)
} catch (error) {
throw new Error('File should be deleted')
} finally {
const datasetFilesAfterRestriction = await getDatasetFiles.execute(testDatasetIds.numericId)

expect(datasetFilesAfterRestriction.files[0].restricted).toEqual(true)

// Unrestrict the file for the next test
await restrictFile.execute(datasetFilesAfterRestriction.files[0].id, false)
}
})

test('should succesfully unrestrict a file', async () => {
try {
const datasetFiles = await getDatasetFiles.execute(testDatasetIds.numericId)

await restrictFile.execute(datasetFiles.files[0].id, true)

await restrictFile.execute(datasetFiles.files[0].id, false)
} catch (error) {
throw new Error('File should be deleted')
} finally {
const datasetFilesAfterRestriction = await getDatasetFiles.execute(testDatasetIds.numericId)

expect(datasetFilesAfterRestriction.files[0].restricted).toEqual(false)
}
})

test('should throw an error when the file id does not exist', async () => {
expect.assertions(2)
let writeError: WriteError | undefined = undefined
const nonExistentFileId = 5

try {
await restrictFile.execute(nonExistentFileId, true)
throw new Error('Use case should throw an error')
} catch (error) {
writeError = error as WriteError
} finally {
expect(writeError).toBeInstanceOf(WriteError)

expect(writeError?.message).toEqual(
`There was an error when writing the resource. Reason was: [400] Could not find datafile with id ${nonExistentFileId}`
)
}
})
})
10 changes: 5 additions & 5 deletions test/integration/collections/CollectionsRepository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,8 +339,8 @@ describe('CollectionsRepository', () => {
await new Promise((resolve) => setTimeout(resolve, 5000))

let actual = await sut.getCollectionItems(testCollectionAlias)
const actualFilePreview = actual.items[0] as FilePreview
const actualDatasetPreview = actual.items[1] as DatasetPreview
const actualFilePreview = actual.items[1] as FilePreview
const actualDatasetPreview = actual.items[0] as DatasetPreview
const actualCollectionPreview = actual.items[2] as CollectionPreview

const expectedFileMd5 = '68b22040025784da775f55cfcb6dee2e'
Expand Down Expand Up @@ -473,7 +473,7 @@ describe('CollectionsRepository', () => {

// Test limit and offset
actual = await sut.getCollectionItems(testCollectionAlias, 1, 1)
expect((actual.items[0] as DatasetPreview).persistentId).toBe(testDatasetIds.persistentId)
expect((actual.items[0] as FilePreview).name).toBe(expectedFileName)
expect(actual.items.length).toBe(1)
expect(actual.totalItemCount).toBe(3)

Expand Down Expand Up @@ -683,8 +683,8 @@ describe('CollectionsRepository', () => {
)
expect(actual.items.length).toBe(3)
expect(actual.totalItemCount).toBe(3)
expect((actual.items[0] as FilePreview).type).toBe(CollectionItemType.FILE)
expect((actual.items[1] as DatasetPreview).type).toBe(CollectionItemType.DATASET)
expect((actual.items[0] as DatasetPreview).type).toBe(CollectionItemType.DATASET)
expect((actual.items[1] as FilePreview).type).toBe(CollectionItemType.FILE)
expect((actual.items[2] as CollectionPreview).type).toBe(CollectionItemType.COLLECTION)
expect(actual.countPerObjectType.dataverses).toBe(1)
expect(actual.countPerObjectType.datasets).toBe(1)
Expand Down
Loading