Skip to content

Commit

Permalink
Merge branch 'develop' into 213-implement-use-case-for-edit-files-met…
Browse files Browse the repository at this point in the history
…adata-on-file-page
  • Loading branch information
ChengShi-1 authored Feb 26, 2025
2 parents af00b1a + 25520a1 commit 5c76eb4
Show file tree
Hide file tree
Showing 25 changed files with 915 additions and 12 deletions.
26 changes: 26 additions & 0 deletions .github/workflows/copy_labels.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Copy labels from issue to pull request

on:
pull_request:
types: [opened]

jobs:
copy-labels:
runs-on: ubuntu-latest
name: Copy labels from linked issues
steps:
- name: copy-labels
uses: michalvankodev/[email protected]
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
labels-to-exclude: |
Size: 3
Size: 10
Size: 20
Size: 33
Size: 80
Original size: 3
Original size: 10
Original size: 20
Original size: 33
Original size: 80
51 changes: 51 additions & 0 deletions docs/useCases.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ The different use cases currently available in the package are classified below,
- [Create a Collection](#create-a-collection)
- [Update a Collection](#update-a-collection)
- [Publish a Collection](#publish-a-collection)
- [Delete a Collection](#delete-a-collection)
- [Update Collection Featured Items](#update-collection-featured-items)
- [Delete Collection Featured Items](#delete-collection-featured-items)
- [Datasets](#Datasets)
Expand Down Expand Up @@ -51,6 +52,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)
- [Replace a File](#replace-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)
Expand Down Expand Up @@ -320,6 +322,24 @@ The `collectionIdOrAlias` is a generic collection identifier, which can be eithe

_See [use case](../src/collections/domain/useCases/PublishCollection.ts)_ definition.

### Delete a Collection

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

/* ... */

const collectionIdOrAlias = 12345

deleteCollection.execute(collectionIdOrAlias)

/* ... */
```

The `collectionIdOrAlias` is a generic collection identifier, which can be either a string (for queries by CollectionAlias), or a number (for queries by CollectionId).

_See [use case](../src/collections/domain/useCases/DeleteCollection.ts)_ definition.

#### Update Collection Featured Items

Updates all featured items, given a collection identifier and a CollectionFeaturedItemsDTO.
Expand Down Expand Up @@ -1300,6 +1320,37 @@ 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.

#### Replace a File

Replaces a File. Currently working for already uploaded S3 bucket files.

##### Example call:

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

/* ... */

const fileId = 12345
const uploadedFileDTO: UploadedFileDTO = {
fileName: 'the-file-name',
storageId: 'localstack1://mybucket:19121faf7e7-2c40322ff54e',
checksumType: 'md5',
checksumValue: 'ede3d3b685b4e137ba4cb2521329a75e',
mimeType: 'text/plain'
}

replaceFile.execute(fileId, uploadedFileDTO)

/* ... */
```

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

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

The `uploadedFileDTO` parameter is a [UploadedFileDTO](../src/files/domain/dtos/UploadedFileDTO.ts) and includes properties related to the uploaded files. Some of these properties should be calculated from the uploaded File Blob objects and the resulting storage identifiers from the Upload File use case.

#### Restrict or Unrestrict a File

Restrict or unrestrict an existing file.
Expand Down
1 change: 1 addition & 0 deletions src/collections/domain/models/Collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface Collection {
contacts?: CollectionContact[]
isMetadataBlockRoot: boolean
isFacetRoot: boolean
childCount: number
}

export interface CollectionInputLevel {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface ICollectionsRepository {
parentCollectionId: number | string
): Promise<number>
publishCollection(collectionIdOrAlias: number | string): Promise<void>
deleteCollection(collectionIdOrAlias: number | string): Promise<void>
getCollectionFacets(collectionIdOrAlias: number | string): Promise<CollectionFacet[]>
getCollectionUserPermissions(
collectionIdOrAlias: number | string
Expand Down
20 changes: 20 additions & 0 deletions src/collections/domain/useCases/DeleteCollection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { UseCase } from '../../../core/domain/useCases/UseCase'
import { ICollectionsRepository } from '../repositories/ICollectionsRepository'

export class DeleteCollection implements UseCase<void> {
private collectionsRepository: ICollectionsRepository

constructor(collectionsRepository: ICollectionsRepository) {
this.collectionsRepository = collectionsRepository
}

/**
* Deletes the Dataverse collection whose database ID or alias is given:
*
* @param {number | string} [collectionIdOrAlias] - A generic collection identifier, which can be either a string (for queries by CollectionAlias), or a number (for queries by CollectionId)
* @returns {Promise<void>} -This method does not return anything upon successful completion.
*/
async execute(collectionIdOrAlias: number | string): Promise<void> {
return await this.collectionsRepository.deleteCollection(collectionIdOrAlias)
}
}
5 changes: 4 additions & 1 deletion src/collections/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { GetCollectionFeaturedItems } from './domain/useCases/GetCollectionFeatu
import { CollectionsRepository } from './infra/repositories/CollectionsRepository'
import { UpdateCollectionFeaturedItems } from './domain/useCases/UpdateCollectionFeaturedItems'
import { DeleteCollectionFeaturedItems } from './domain/useCases/DeleteCollectionFeaturedItems'
import { DeleteCollection } from './domain/useCases/DeleteCollection'

const collectionsRepository = new CollectionsRepository()

Expand All @@ -22,6 +23,7 @@ const updateCollection = new UpdateCollection(collectionsRepository)
const getCollectionFeaturedItems = new GetCollectionFeaturedItems(collectionsRepository)
const updateCollectionFeaturedItems = new UpdateCollectionFeaturedItems(collectionsRepository)
const deleteCollectionFeaturedItems = new DeleteCollectionFeaturedItems(collectionsRepository)
const deleteCollection = new DeleteCollection(collectionsRepository)

export {
getCollection,
Expand All @@ -33,7 +35,8 @@ export {
updateCollection,
getCollectionFeaturedItems,
updateCollectionFeaturedItems,
deleteCollectionFeaturedItems
deleteCollectionFeaturedItems,
deleteCollection
}
export { Collection, CollectionInputLevel } from './domain/models/Collection'
export { CollectionFacet } from './domain/models/CollectionFacet'
Expand Down
12 changes: 11 additions & 1 deletion src/collections/infra/repositories/CollectionsRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ export class CollectionsRepository extends ApiRepository implements ICollections
collectionIdOrAlias: number | string = ROOT_COLLECTION_ID
): Promise<Collection> {
return this.doGet(`/${this.collectionsResourceName}/${collectionIdOrAlias}`, true, {
returnOwners: true
returnOwners: true,
returnChildCount: true
})
.then((response) => transformCollectionResponseToCollection(response))
.catch((error) => {
Expand Down Expand Up @@ -102,6 +103,7 @@ export class CollectionsRepository extends ApiRepository implements ICollections
throw error
})
}

public async publishCollection(collectionIdOrAlias: string | number): Promise<void> {
return this.doPost(
`/${this.collectionsResourceName}/${collectionIdOrAlias}/actions/:publish`,
Expand All @@ -113,6 +115,14 @@ export class CollectionsRepository extends ApiRepository implements ICollections
})
}

public async deleteCollection(collectionIdOrAlias: number | string): Promise<void> {
return this.doDelete(`/${this.collectionsResourceName}/${collectionIdOrAlias}`)
.then(() => undefined)
.catch((error) => {
throw error
})
}

public async getCollectionUserPermissions(
collectionIdOrAlias: number | string
): Promise<CollectionUserPermissions> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface CollectionPayload {
dataverseType: string
isMetadataBlockRoot: boolean
isFacetRoot: boolean
childCount: number
}

export interface CollectionInputLevelPayload {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const transformPayloadToCollection = (collectionPayload: CollectionPayload): Col
isMetadataBlockRoot: collectionPayload.isMetadataBlockRoot,
isFacetRoot: collectionPayload.isFacetRoot,
description: collectionPayload.description,
childCount: collectionPayload.childCount,
...(collectionPayload.isPartOf && {
isPartOf: transformPayloadToOwnerNode(collectionPayload.isPartOf)
}),
Expand Down
1 change: 1 addition & 0 deletions src/datasets/infra/repositories/DatasetsRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi
throw error
})
}

public async createDataset(
newDataset: DatasetDTO,
datasetMetadataBlocks: MetadataBlock[],
Expand Down
1 change: 1 addition & 0 deletions src/files/domain/dtos/UploadedFileDTO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export interface UploadedFileDTO {
checksumValue: string
checksumType: string
mimeType: string
forceReplace?: boolean // Only used in the ReplaceFile use case, whether to allow the mimetype to change
}
2 changes: 2 additions & 0 deletions src/files/domain/repositories/IFilesRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ export interface IFilesRepository {

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

replaceFile(fileId: number | string, uploadedFileDTO: UploadedFileDTO): Promise<undefined>

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

export class ReplaceFile implements UseCase<void> {
private filesRepository: IFilesRepository

constructor(filesRepository: IFilesRepository) {
this.filesRepository = filesRepository
}

/**
* Replaces an existing file.
*
* This method completes the flow initiated by the UploadFile use case, which involves replacing an existing file with a new one just uploaded.
* (https://guides.dataverse.org/en/latest/developers/s3-direct-upload-api.html#replacing-an-existing-file-in-the-dataset)
*
* Note: This use case can be used independently of the UploadFile use case, e.g., supporting scenarios in which the files already exist in S3 or have been uploaded via some out-of-band method.
*
* @param {number | string} [fileId] - The File identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers).
* @param {UploadedFileDTO} [uploadedFileDTO] - File DTO associated with the uploaded file.
* @returns {Promise<void>} A promise that resolves when the file has been successfully replaced.
* @throws {WriteError} - If there are errors while writing data.
*/
async execute(fileId: number | string, uploadedFileDTO: UploadedFileDTO): Promise<void> {
await this.filesRepository.replaceFile(fileId, uploadedFileDTO)
}
}
4 changes: 3 additions & 1 deletion src/files/domain/useCases/UploadFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ export class UploadFile implements UseCase<string> {
* Uploads a file to remote storage and returns the storage identifier.
*
* This use case is based on the Direct Upload API, particularly the first part of the flow, "Requesting Direct Upload of a DataFile".
* To fulfill the flow, you will need to call the AddUploadedFileToDataset Use Case to add the uploaded file to the dataset.
* To fulfill the flow, you could:
* - Call the AddUploadedFilesToDataset Use Case to add the uploaded file to the dataset.
* - Call the ReplaceFile Use Case to replace an existing file with the uploaded one.
* (https://guides.dataverse.org/en/latest/developers/s3-direct-upload-api.html#requesting-direct-upload-of-a-datafile)
*
* @param {number | string} [datasetId] - The dataset identifier, which can be a string (for persistent identifiers) or a number (for numeric identifiers).
Expand Down
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 { ReplaceFile } from './domain/useCases/ReplaceFile'
import { RestrictFile } from './domain/useCases/RestrictFile'
import { UpdateFileMetadata } from './domain/useCases/UpdateFileMetadata'

Expand All @@ -30,6 +31,7 @@ const getFileCitation = new GetFileCitation(filesRepository)
const uploadFile = new UploadFile(directUploadClient)
const addUploadedFilesToDataset = new AddUploadedFilesToDataset(filesRepository)
const deleteFile = new DeleteFile(filesRepository)
const replaceFile = new ReplaceFile(filesRepository)
const restrictFile = new RestrictFile(filesRepository)
const updateFileMetadata = new UpdateFileMetadata(filesRepository)

Expand All @@ -47,7 +49,8 @@ export {
addUploadedFilesToDataset,
deleteFile,
restrictFile,
updateFileMetadata
updateFileMetadata,
replaceFile
}

export { FileModel as File, FileEmbargo, FileChecksum } from './domain/models/FileModel'
Expand Down
35 changes: 35 additions & 0 deletions src/files/infra/repositories/FilesRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export interface UploadedFileRequestBody {
directoryLabel?: string
categories?: string[]
restrict?: boolean
forceReplace?: boolean
}

export interface ChecksumRequestBody {
Expand Down Expand Up @@ -303,6 +304,40 @@ export class FilesRepository extends ApiRepository implements IFilesRepository {
})
}

public async replaceFile(
fileId: number | string,
uploadedFileDTO: UploadedFileDTO
): Promise<undefined> {
const requestBody: UploadedFileRequestBody = {
fileName: uploadedFileDTO.fileName,
checksum: {
'@value': uploadedFileDTO.checksumValue,
'@type': uploadedFileDTO.checksumType.toUpperCase()
},
mimeType: uploadedFileDTO.mimeType,
storageIdentifier: uploadedFileDTO.storageId,
forceReplace: uploadedFileDTO.forceReplace,
...(uploadedFileDTO.description && { description: uploadedFileDTO.description }),
...(uploadedFileDTO.categories && { categories: uploadedFileDTO.categories }),
...(uploadedFileDTO.restrict && { restrict: uploadedFileDTO.restrict }),
...(uploadedFileDTO.directoryLabel && { directoryLabel: uploadedFileDTO.directoryLabel })
}

const formData = new FormData()
formData.append('jsonData', JSON.stringify(requestBody))

return this.doPost(
this.buildApiEndpoint(this.filesResourceName, 'replace', fileId),
formData,
{},
ApiConstants.CONTENT_TYPE_MULTIPART_FORM_DATA
)
.then(() => undefined)
.catch((error) => {
throw error
})
}

public async restrictFile(fileId: number | string, restrict: boolean): Promise<undefined> {
return this.doPut(this.buildApiEndpoint(this.filesResourceName, 'restrict', fileId), restrict)
.then(() => undefined)
Expand Down
Loading

0 comments on commit 5c76eb4

Please sign in to comment.