Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Edit dataset use case #151

Merged
merged 14 commits into from
May 13, 2024
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
8 changes: 8 additions & 0 deletions docs/localDevelopment.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,14 @@ These environment variables can be updated as needed for integration testing. Fo
npm run test:coverage
```

### Display container logs while running tests

If you want to see the container logs while running integration or functional tests, run the following command before running the test commands:

```bash
export DEBUG=testcontainers:containers npm test
```

## Format and lint

### Run formatter
Expand Down
71 changes: 65 additions & 6 deletions docs/useCases.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ The different use cases currently available in the package are classified below,
- [List All Datasets](#list-all-datasets)
- [Datasets write use cases](#datasets-write-use-cases)
- [Create a Dataset](#create-a-dataset)
- [Update a Dataset](#update-a-dataset)
- [Publish a Dataset](#publish-a-dataset)
- [Files](#Files)
- [Files read use cases](#files-read-use-cases)
Expand Down Expand Up @@ -299,7 +300,7 @@ The `DatasetPreviewSubset`returned instance contains a property called `totalDat

#### Create a Dataset

Creates a new Dataset in a collection, given a [NewDatasetDTO](../src/datasets/domain/dtos/NewDatasetDTO.ts) object and an optional collection identifier, which defaults to `root`.
Creates a new Dataset in a collection, given a [DatasetDTO](../src/datasets/domain/dtos/DatasetDTO.ts) object and an optional collection identifier, which defaults to `root`.

This use case validates the submitted fields of each metadata block and can return errors of type [ResourceValidationError](../src/core/domain/useCases/validators/errors/ResourceValidationError.ts), which include sufficient information to determine which field value is invalid and why.

Expand All @@ -310,7 +311,7 @@ import { createDataset } from '@iqss/dataverse-client-javascript'

/* ... */

const newDatasetDTO: NewDatasetDTO = {
const datasetDTO: DatasetDTO = {
metadataBlockValues: [
{
name: 'citation',
Expand Down Expand Up @@ -345,7 +346,7 @@ const newDatasetDTO: NewDatasetDTO = {
]
}

createDataset.execute(newDatasetDTO).then((newDatasetIds: CreatedDatasetIdentifiers) => {
createDataset.execute(datasetDTO).then((newDatasetIds: CreatedDatasetIdentifiers) => {
/* ... */
})

Expand All @@ -358,6 +359,66 @@ The above example creates the new dataset in the `root` collection since no coll

The use case returns a [CreatedDatasetIdentifiers](../src/datasets/domain/models/CreatedDatasetIdentifiers.ts) object, which includes the persistent and numeric identifiers of the created dataset.

#### Update a Dataset

Updates an existing Dataset, given a [DatasetDTO](../src/datasets/domain/dtos/DatasetDTO.ts) with the updated information.

If a draft of the dataset already exists, the metadata of that draft is overwritten; otherwise, a new draft is created with the updated metadata.

This use case validates the submitted fields of each metadata block and can return errors of type [ResourceValidationError](../src/core/domain/useCases/validators/errors/ResourceValidationError.ts), which include sufficient information to determine which field value is invalid and why.

##### Example call:

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

/* ... */

const datasetId = 1
const datasetDTO: DatasetDTO = {
metadataBlockValues: [
{
name: 'citation',
fields: {
title: 'Updated Dataset',
author: [
{
authorName: 'John Doe',
authorAffiliation: 'Dataverse'
},
{
authorName: 'John Lee',
authorAffiliation: 'Dataverse'
}
],
datasetContact: [
{
datasetContactEmail: 'johndoe@dataverse.com',
datasetContactName: 'John'
}
],
dsDescription: [
{
dsDescriptionValue: 'This is the description of our new dataset'
}
],
subject: 'Earth and Environmental Sciences'

/* Rest of field values... */
}
}
]
}

updateDataset.execute(datasetId, datasetDTO)

/* ... */
```

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

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

#### Publish a Dataset

Publishes a Dataset, given its identifier and the type of version update to perform.
Expand All @@ -372,9 +433,7 @@ import { publishDataset } from '@iqss/dataverse-client-javascript'
const datasetId = 'doi:10.77777/FK2/AAAAAA'
const versionUpdateType = VersionUpdateType.MINOR

publishDataset.execute(datasetId, versionUpdateType).then((publishedDataset: Dataset) => {
/* ... */
})
publishDataset.execute(datasetId, versionUpdateType)

/* ... */
```
Expand Down
3 changes: 1 addition & 2 deletions src/core/domain/useCases/UseCase.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export interface UseCase<T> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
execute(...args: any[]): Promise<T>
execute(...args: unknown[]): Promise<T>
}
4 changes: 0 additions & 4 deletions src/core/domain/useCases/validators/NewResourceValidator.ts

This file was deleted.

3 changes: 3 additions & 0 deletions src/core/domain/useCases/validators/ResourceValidator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface ResourceValidator {
validate(...args: unknown[]): void
}
37 changes: 27 additions & 10 deletions src/core/infra/repositories/ApiRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,15 @@ export abstract class ApiRepository {
data: string | object,
queryParams: object = {}
): Promise<AxiosResponse> {
return await axios
.post(
this.buildRequestUrl(apiEndpoint),
JSON.stringify(data),
this.buildRequestConfig(true, queryParams)
)
.then((response) => response)
.catch((error) => {
throw new WriteError(this.buildErrorMessage(error))
})
return await this.doRequest('post', apiEndpoint, data, queryParams)
}

public async doPut(
apiEndpoint: string,
data: string | object,
queryParams: object = {}
): Promise<AxiosResponse> {
return await this.doRequest('put', apiEndpoint, data, queryParams)
}

protected buildApiEndpoint(
Expand Down Expand Up @@ -83,4 +82,22 @@ export abstract class ApiRepository {
const message = error.response && error.response.data ? ` ${error.response.data.message}` : ''
return `[${status}]${message}`
}

private async doRequest(
method: 'post' | 'put',
apiEndpoint: string,
data: string | object,
queryParams: object = {}
): Promise<AxiosResponse> {
const requestData = JSON.stringify(data)
const requestUrl = this.buildRequestUrl(apiEndpoint)
const requestConfig = this.buildRequestConfig(true, queryParams)

try {
const response = await axios[method](requestUrl, requestData, requestConfig)
return response
} catch (error) {
throw new WriteError(this.buildErrorMessage(error))
}
}
}
21 changes: 21 additions & 0 deletions src/datasets/domain/dtos/DatasetDTO.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { DatasetLicense } from '../models/Dataset'

export interface DatasetDTO {
license?: DatasetLicense
metadataBlockValues: DatasetMetadataBlockValuesDTO[]
}

export interface DatasetMetadataBlockValuesDTO {
name: string
fields: DatasetMetadataFieldsDTO
}

export type DatasetMetadataFieldsDTO = Record<string, DatasetMetadataFieldValueDTO>

export type DatasetMetadataFieldValueDTO =
| string
| string[]
| DatasetMetadataChildFieldValueDTO
| DatasetMetadataChildFieldValueDTO[]

export type DatasetMetadataChildFieldValueDTO = Record<string, string>
21 changes: 0 additions & 21 deletions src/datasets/domain/dtos/NewDatasetDTO.ts

This file was deleted.

9 changes: 7 additions & 2 deletions src/datasets/domain/repositories/IDatasetsRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { DatasetLock } from '../models/DatasetLock'
import { DatasetPreviewSubset } from '../models/DatasetPreviewSubset'
import { DatasetUserPermissions } from '../models/DatasetUserPermissions'
import { CreatedDatasetIdentifiers } from '../models/CreatedDatasetIdentifiers'
import { NewDatasetDTO } from '../dtos/NewDatasetDTO'
import { DatasetDTO } from '../dtos/DatasetDTO'
import { MetadataBlock } from '../../../metadataBlocks'

export interface IDatasetsRepository {
Expand All @@ -28,9 +28,14 @@ export interface IDatasetsRepository {
getPrivateUrlDatasetCitation(token: string): Promise<string>
getDatasetUserPermissions(datasetId: number | string): Promise<DatasetUserPermissions>
createDataset(
newDataset: NewDatasetDTO,
newDataset: DatasetDTO,
datasetMetadataBlocks: MetadataBlock[],
collectionId: string
): Promise<CreatedDatasetIdentifiers>
publishDataset(datasetId: number | string, versionUpdateType: VersionUpdateType): Promise<void>
updateDataset(
datasetId: number | string,
dataset: DatasetDTO,
datasetMetadataBlocks: MetadataBlock[]
): Promise<void>
}
45 changes: 11 additions & 34 deletions src/datasets/domain/useCases/CreateDataset.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,36 @@
import { UseCase } from '../../../core/domain/useCases/UseCase'
import { IDatasetsRepository } from '../repositories/IDatasetsRepository'
import { NewDatasetDTO, NewDatasetMetadataBlockValuesDTO } from '../dtos/NewDatasetDTO'
import { NewResourceValidator } from '../../../core/domain/useCases/validators/NewResourceValidator'
import { DatasetDTO } from '../dtos/DatasetDTO'
import { ResourceValidator } from '../../../core/domain/useCases/validators/ResourceValidator'
import { IMetadataBlocksRepository } from '../../../metadataBlocks/domain/repositories/IMetadataBlocksRepository'
import { MetadataBlock } from '../../../metadataBlocks'
import { CreatedDatasetIdentifiers } from '../models/CreatedDatasetIdentifiers'
import { ROOT_COLLECTION_ALIAS } from '../../../collections/domain/models/Collection'
import { DatasetWriteUseCase } from './DatasetWriteUseCase'

export class CreateDataset implements UseCase<CreatedDatasetIdentifiers> {
private datasetsRepository: IDatasetsRepository
private metadataBlocksRepository: IMetadataBlocksRepository
private newDatasetValidator: NewResourceValidator

export class CreateDataset extends DatasetWriteUseCase<CreatedDatasetIdentifiers> {
constructor(
datasetsRepository: IDatasetsRepository,
metadataBlocksRepository: IMetadataBlocksRepository,
newDatasetValidator: NewResourceValidator
newDatasetValidator: ResourceValidator
) {
this.datasetsRepository = datasetsRepository
this.metadataBlocksRepository = metadataBlocksRepository
this.newDatasetValidator = newDatasetValidator
super(datasetsRepository, metadataBlocksRepository, newDatasetValidator)
}

/**
* Creates a new Dataset in a collection, given a NewDatasetDTO object and an optional collection identifier, which defaults to root.
* Creates a new Dataset in a collection, given a DatasetDTO object and an optional collection identifier, which defaults to root.
*
* @param {NewDatasetDTO} [newDataset] - NewDatasetDTO object including the new dataset metadata field values for each metadata block.
* @param {DatasetDTO} [newDataset] - DatasetDTO object including the new dataset metadata field values for each metadata block.
* @param {string} [collectionId] - Specifies the collection identifier where the new dataset should be created (optional, defaults to root).
* @returns {Promise<CreatedDatasetIdentifiers>}
* @throws {ResourceValidationError} - If there are validation errors related to the provided information.
* @throws {ReadError} - If there are errors while reading data.
* @throws {WriteError} - If there are errors while writing data.
*/
async execute(
newDataset: NewDatasetDTO,
newDataset: DatasetDTO,
collectionId = ROOT_COLLECTION_ALIAS
): Promise<CreatedDatasetIdentifiers> {
const metadataBlocks = await this.getNewDatasetMetadataBlocks(newDataset)

this.newDatasetValidator.validate(newDataset, metadataBlocks)

return this.datasetsRepository.createDataset(newDataset, metadataBlocks, collectionId)
}

async getNewDatasetMetadataBlocks(newDataset: NewDatasetDTO): Promise<MetadataBlock[]> {
const metadataBlocks: MetadataBlock[] = []
await Promise.all(
newDataset.metadataBlockValues.map(
async (metadataBlockValue: NewDatasetMetadataBlockValuesDTO) => {
metadataBlocks.push(
await this.metadataBlocksRepository.getMetadataBlockByName(metadataBlockValue.name)
)
}
)
)
return metadataBlocks
this.getNewDatasetValidator().validate(newDataset, metadataBlocks)
return this.getDatasetsRepository().createDataset(newDataset, metadataBlocks, collectionId)
}
}
48 changes: 48 additions & 0 deletions src/datasets/domain/useCases/DatasetWriteUseCase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { UseCase } from '../../../core/domain/useCases/UseCase'
import { IDatasetsRepository } from '../repositories/IDatasetsRepository'
import { DatasetDTO, DatasetMetadataBlockValuesDTO } from '../dtos/DatasetDTO'
import { ResourceValidator } from '../../../core/domain/useCases/validators/ResourceValidator'
import { IMetadataBlocksRepository } from '../../../metadataBlocks/domain/repositories/IMetadataBlocksRepository'
import { MetadataBlock } from '../../../metadataBlocks'

export abstract class DatasetWriteUseCase<T> implements UseCase<T> {
private datasetsRepository: IDatasetsRepository
private metadataBlocksRepository: IMetadataBlocksRepository
private newDatasetValidator: ResourceValidator

constructor(
datasetsRepository: IDatasetsRepository,
metadataBlocksRepository: IMetadataBlocksRepository,
newDatasetValidator: ResourceValidator
) {
this.datasetsRepository = datasetsRepository
this.metadataBlocksRepository = metadataBlocksRepository
this.newDatasetValidator = newDatasetValidator
}

abstract execute(...args: unknown[]): Promise<T>

getDatasetsRepository(): IDatasetsRepository {
return this.datasetsRepository
}

getMetadataBlocksRepository(): IMetadataBlocksRepository {
return this.metadataBlocksRepository
}

getNewDatasetValidator(): ResourceValidator {
return this.newDatasetValidator
}

async getNewDatasetMetadataBlocks(dataset: DatasetDTO): Promise<MetadataBlock[]> {
const metadataBlocks: MetadataBlock[] = []
await Promise.all(
dataset.metadataBlockValues.map(async (metadataBlockValue: DatasetMetadataBlockValuesDTO) => {
metadataBlocks.push(
await this.metadataBlocksRepository.getMetadataBlockByName(metadataBlockValue.name)
)
})
)
return metadataBlocks
}
}
Loading
Loading