Skip to content

Commit

Permalink
Merge pull request #266 from IQSS/feat/259-restrict-unrestrict-file-u…
Browse files Browse the repository at this point in the history
…se-case

Restrict File use case
  • Loading branch information
ofahimIQSS authored Feb 24, 2025
2 parents 37f11fd + 8e979e1 commit e689f25
Show file tree
Hide file tree
Showing 10 changed files with 331 additions and 9 deletions.
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> {
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
})
}
}
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}`
)
}
})
})
129 changes: 129 additions & 0 deletions test/integration/files/FilesRepository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -725,4 +725,133 @@ describe('FilesRepository', () => {
await expect(sut.deleteFile(nonExistentFiledId)).rejects.toThrow(expectedError)
})
})

describe('restrictFile', () => {
let restrictFileDatasetIds: CreatedDatasetIdentifiers
const testTextFile1Name = 'test-file-1.txt'

const setFileToRestricted = async (fileId: number) => {
await sut.restrictFile(fileId, true)
}

const setFileToUnrestricted = async (fileId: number) => {
await sut.restrictFile(fileId, false)
}

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

afterEach(async () => {
await deleteUnpublishedDatasetViaApi(restrictFileDatasetIds.numericId)
})

test('should successfully restrict a file', async () => {
const datasetFiles = await sut.getDatasetFiles(
restrictFileDatasetIds.numericId,
DatasetNotNumberedVersion.LATEST,
false,
FileOrderCriteria.NAME_AZ
)

expect(datasetFiles.files[0].restricted).toEqual(false)

await setFileToRestricted(datasetFiles.files[0].id)

const datasetFilesAfterRestrict = await sut.getDatasetFiles(
restrictFileDatasetIds.numericId,
DatasetNotNumberedVersion.LATEST,
false,
FileOrderCriteria.NAME_AZ
)

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

// Unrestrict the file Just in case to avoid conflicts with other tests
await setFileToUnrestricted(datasetFiles.files[0].id)
})

test('should successfully unrestrict a file', async () => {
const datasetFiles = await sut.getDatasetFiles(
restrictFileDatasetIds.numericId,
DatasetNotNumberedVersion.LATEST,
false,
FileOrderCriteria.NAME_AZ
)

expect(datasetFiles.files[0].restricted).toEqual(false)

await setFileToRestricted(datasetFiles.files[0].id)

const datasetFilesAfterRestrict = await sut.getDatasetFiles(
restrictFileDatasetIds.numericId,
DatasetNotNumberedVersion.LATEST,
false,
FileOrderCriteria.NAME_AZ
)

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

await setFileToUnrestricted(datasetFiles.files[0].id)

const datasetFilesAfterUnrestrict = await sut.getDatasetFiles(
restrictFileDatasetIds.numericId,
DatasetNotNumberedVersion.LATEST,
false,
FileOrderCriteria.NAME_AZ
)

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

test('should return error when file was already restricted', async () => {
const datasetFiles = await sut.getDatasetFiles(
restrictFileDatasetIds.numericId,
DatasetNotNumberedVersion.LATEST,
false,
FileOrderCriteria.NAME_AZ
)

await setFileToRestricted(datasetFiles.files[0].id)

const expectedError = new WriteError(
`[400] Problem trying to update restriction status on ${testTextFile1Name}: File ${testTextFile1Name} is already restricted`
)

await expect(setFileToRestricted(datasetFiles.files[0].id)).rejects.toThrow(expectedError)

// Unrestrict the file Just in case to avoid conflicts with other tests
await setFileToUnrestricted(datasetFiles.files[0].id)
})

test('should return error when files was already unrestricted', async () => {
const datasetFiles = await sut.getDatasetFiles(
restrictFileDatasetIds.numericId,
DatasetNotNumberedVersion.LATEST,
false,
FileOrderCriteria.NAME_AZ
)

const expectedError = new WriteError(
`[400] Problem trying to update restriction status on ${testTextFile1Name}: File ${testTextFile1Name} is already unrestricted`
)

await expect(setFileToUnrestricted(datasetFiles.files[0].id)).rejects.toThrow(expectedError)
})

test('should return error when file does not exist', async () => {
const expectedError = new WriteError(
`[400] Could not find datafile with id ${nonExistentFiledId}`
)

await expect(setFileToRestricted(nonExistentFiledId)).rejects.toThrow(expectedError)
})
})
})
Loading

0 comments on commit e689f25

Please sign in to comment.