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
29 changes: 29 additions & 0 deletions docs/useCases.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ The different use cases currently available in the package are classified below,
- [List Files in a Dataset](#list-files-in-a-dataset)
- [Files write use cases](#files-write-use-cases)
- [File Uploading Use Cases](#file-uploading-use-cases)
- [Delete a File](#delete-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 @@ -1203,6 +1204,34 @@ The following error might arise from the `AddUploadedFileToDataset` use case:

- AddUploadedFileToDatasetError: This error indicates that there was an error while adding the uploaded file to the dataset.

#### Delete a File

Deletes a File.

##### Example call:

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

/* ... */

const fileId = 12345

deleteFile.execute(fileId)

/* ... */
```

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

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

Note that the behavior of deleting files depends on if the dataset has ever been published or not.

- If the dataset has never been published, the file will be deleted forever.
- 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.

## Metadata Blocks

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

protected buildApiEndpoint(
resourceName: string,
operation: string,
resourceId: number | string | undefined = undefined
operation?: string,
resourceId?: number | string
) {
const operationSegment = operation ? `/${operation}` : ''

return typeof resourceId === 'number'
? `/${resourceName}/${resourceId}/${operation}`
? `/${resourceName}/${resourceId}${operationSegment}`
: typeof resourceId === 'string'
? `/${resourceName}/:persistentId/${operation}?persistentId=${resourceId}`
: `/${resourceName}/${operation}`
? `/${resourceName}/:persistentId${operationSegment}?persistentId=${resourceId}`
: `/${resourceName}${operationSegment}`
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
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 @@ -59,4 +59,6 @@ export interface IFilesRepository {
datasetId: number | string,
uploadedFileDTOs: UploadedFileDTO[]
): Promise<undefined>

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

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

/**
* Deletes a file.
* More detailed information about the file deletion behavior can be found in https://guides.dataverse.org/en/latest/api/native-api.html#deleting-files
*
* @param {number | string} [fileId] - The File identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers).
* @returns {Promise<void>} -This method does not return anything upon successful completion.
*/
async execute(fileId: number | string): Promise<void> {
return await this.filesRepository.deleteFile(fileId)
}
}
5 changes: 4 additions & 1 deletion src/files/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { GetFileAndDataset } from './domain/useCases/GetFileAndDataset'
import { UploadFile } from './domain/useCases/UploadFile'
import { DirectUploadClient } from './infra/clients/DirectUploadClient'
import { AddUploadedFilesToDataset } from './domain/useCases/AddUploadedFilesToDataset'
import { DeleteFile } from './domain/useCases/DeleteFile'

const filesRepository = new FilesRepository()
const directUploadClient = new DirectUploadClient(filesRepository)
Expand All @@ -26,6 +27,7 @@ const getFileAndDataset = new GetFileAndDataset(filesRepository)
const getFileCitation = new GetFileCitation(filesRepository)
const uploadFile = new UploadFile(directUploadClient)
const addUploadedFilesToDataset = new AddUploadedFilesToDataset(filesRepository)
const deleteFile = new DeleteFile(filesRepository)

export {
getDatasetFiles,
Expand All @@ -38,7 +40,8 @@ export {
getFileAndDataset,
getFileCitation,
uploadFile,
addUploadedFilesToDataset
addUploadedFilesToDataset,
deleteFile
}

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 @@ -293,4 +293,12 @@ export class FilesRepository extends ApiRepository implements IFilesRepository {
queryParams.searchText = fileSearchCriteria.searchText
}
}

public async deleteFile(fileId: number | string): Promise<undefined> {
return this.doDelete(this.buildApiEndpoint(this.filesResourceName, undefined, fileId))
.then(() => undefined)
.catch((error) => {
throw error
})
}
}
89 changes: 89 additions & 0 deletions test/functional/files/DeleteFile.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import {
ApiConfig,
createDataset,
CreatedDatasetIdentifiers,
deleteFile,
getDatasetFileCounts,
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 = 'deleteFileFunctionalTest'
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 delete a file', async () => {
try {
const datasetFiles = await getDatasetFiles.execute(testDatasetIds.numericId)

await deleteFile.execute(datasetFiles.files[0].id)
} catch (error) {
throw new Error('File should be deleted')
} finally {
const datasetFileCounts = await getDatasetFileCounts.execute(testDatasetIds.numericId)

expect(datasetFileCounts.total).toEqual(0)
}
})

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 deleteFile.execute(nonExistentFileId)
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: [404] File with ID ${nonExistentFileId} not found.`
)
}
})
})
81 changes: 80 additions & 1 deletion test/integration/files/FilesRepository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
} from '../../../src/datasets'
import { FileModel } from '../../../src/files/domain/models/FileModel'
import { FileCounts } from '../../../src/files/domain/models/FileCounts'
import { FileDownloadSizeMode } from '../../../src'
import { FileDownloadSizeMode, WriteError } from '../../../src'
import {
deaccessionDatasetViaApi,
publishDatasetViaApi,
Expand Down Expand Up @@ -646,4 +646,83 @@ describe('FilesRepository', () => {
).rejects.toThrow(errorExpected)
})
})

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

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

test('should successfully delete a file', async () => {
const datasetFiles = await sut.getDatasetFiles(
deleFileTestDatasetIds.numericId,
latestDatasetVersionId,
false,
FileOrderCriteria.NAME_AZ
)
await sut.deleteFile(datasetFiles.files[0].id)

const datasetFileCounts = await sut.getDatasetFileCounts(
deleFileTestDatasetIds.numericId,
latestDatasetVersionId,
false
)
expect(datasetFileCounts.total).toEqual(0)

await deleteUnpublishedDatasetViaApi(deleFileTestDatasetIds.numericId)
})

test('should delete a file from the draft dataset but not from the published dataset', async () => {
await publishDatasetViaApi(deleFileTestDatasetIds.numericId).catch(() => {
throw new Error('Error while publishing test Dataset')
})

await waitForNoLocks(deleFileTestDatasetIds.numericId, 10).catch(() => {
throw new Error('Error while waiting for no locks')
})

const datasetFiles = await sut.getDatasetFiles(
deleFileTestDatasetIds.numericId,
latestDatasetVersionId,
false,
FileOrderCriteria.NAME_AZ
)
await sut.deleteFile(datasetFiles.files[0].id)

const datasetFileCounts = await sut.getDatasetFileCounts(
deleFileTestDatasetIds.numericId,
DatasetNotNumberedVersion.DRAFT,
false
)

expect(datasetFileCounts.total).toEqual(0)

const publishedDatasetFileCounts = await sut.getDatasetFileCounts(
deleFileTestDatasetIds.numericId,
DatasetNotNumberedVersion.LATEST_PUBLISHED,
false
)

expect(publishedDatasetFileCounts.total).toBeGreaterThan(0)

await deletePublishedDatasetViaApi(deleFileTestDatasetIds.persistentId).catch(() => {
throw new Error('Error while deleting published test Dataset')
})
})

test('should return error when file does not exist', async () => {
const expectedError = new WriteError(`[404] File with ID ${nonExistentFiledId} not found.`)

await expect(sut.deleteFile(nonExistentFiledId)).rejects.toThrow(expectedError)
})
})
})
25 changes: 25 additions & 0 deletions test/unit/files/DeleteFile.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { IFilesRepository } from '../../../src/files/domain/repositories/IFilesRepository'
import { WriteError } from '../../../src'
import { DeleteFile } from '../../../src/files/domain/useCases/DeleteFile'

describe('execute', () => {
test('should return undefined when repository call is successful', async () => {
const filesRepositoryStub: IFilesRepository = {} as IFilesRepository
filesRepositoryStub.deleteFile = jest.fn().mockResolvedValue(undefined)

const sut = new DeleteFile(filesRepositoryStub)

const actual = await sut.execute(1)

expect(actual).toEqual(undefined)
})

test('should return error result on repository error', async () => {
const filesRepositoryStub: IFilesRepository = {} as IFilesRepository
filesRepositoryStub.deleteFile = jest.fn().mockRejectedValue(new WriteError())

const sut = new DeleteFile(filesRepositoryStub)

await expect(sut.execute(1)).rejects.toThrow(WriteError)
})
})
Loading