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

Adds create collection use case #155

Merged
merged 11 commits into from
Jul 8, 2024
35 changes: 35 additions & 0 deletions docs/useCases.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ The different use cases currently available in the package are classified below,
- [Collections](#Collections)
- [Collections read use cases](#collections-read-use-cases)
- [Get a Collection](#get-a-collection)
- [Collections write use cases](#collections-write-use-cases)
- [Create a Collection](#create-a-collection)
- [Datasets](#Datasets)
- [Datasets read use cases](#datasets-read-use-cases)
- [Get a Dataset](#get-a-dataset)
Expand Down Expand Up @@ -98,6 +100,39 @@ The `collectionIdOrAlias` is a generic collection identifier, which can be eithe

If no collection identifier is specified, the default collection identifier; `root` will be used. If you want to search for a different collection, you must add the collection identifier as a parameter in the use case call.

### Collections Write Use Cases

#### Create a Collection

Creates a new Collection, given a [CollectionDTO](../src/collections/domain/dtos/CollectionDTO.ts) object and an optional parent collection identifier, which defaults to `root`.

##### Example call:

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

/* ... */

const collectionDTO: CollectionDTO = {
alias: alias,
name: 'Test Collection',
contacts: ['dataverse@test.com'],
type: CollectionType.DEPARTMENT
}

createCollection.execute(collectionDTO).then((createdCollectionId: number) => {
/* ... */
})

/* ... */
```

_See [use case](../src/collections/domain/useCases/CreateCollection.ts) implementation_.

The above example creates the new collection in the `root` collection since no collection identifier is specified. If you want to create the collection in a different collection, you must add the collection identifier as a second parameter in the use case call.

The use case returns a number, which is the identifier of the created collection.

## Datasets

### Datasets Read Use Cases
Expand Down
18 changes: 18 additions & 0 deletions src/collections/domain/dtos/CollectionDTO.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export interface CollectionDTO {
alias: string
name: string
contacts: string[]
type: CollectionType
}

export enum CollectionType {
RESEARCHERS = 'RESEARCHERS',
RESEARCH_PROJECTS = 'RESEARCH_PROJECTS',
JOURNALS = 'JOURNALS',
ORGANIZATIONS_INSTITUTIONS = 'ORGANIZATIONS_INSTITUTIONS',
TEACHING_COURSES = 'TEACHING_COURSES',
UNCATEGORIZED = 'UNCATEGORIZED',
LABORATORY = 'LABORATORY',
RESEARCH_GROUP = 'RESEARCH_GROUP',
DEPARTMENT = 'DEPARTMENT'
}
5 changes: 5 additions & 0 deletions src/collections/domain/repositories/ICollectionsRepository.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { CollectionDTO } from '../dtos/CollectionDTO'
import { Collection } from '../models/Collection'

export interface ICollectionsRepository {
getCollection(collectionIdOrAlias: number | string): Promise<Collection>
createCollection(
collectionDTO: CollectionDTO,
parentCollectionId: number | string
): Promise<number>
}
27 changes: 27 additions & 0 deletions src/collections/domain/useCases/CreateCollection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { UseCase } from '../../../core/domain/useCases/UseCase'
import { CollectionDTO } from '../dtos/CollectionDTO'
import { ROOT_COLLECTION_ALIAS } from '../models/Collection'
import { ICollectionsRepository } from '../repositories/ICollectionsRepository'

export class CreateCollection implements UseCase<number> {
private collectionsRepository: ICollectionsRepository

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

/**
* Creates a new collection, given a CollectionDTO object and an optional collection identifier, which defaults to root.
*
* @param {CollectionDTO} [newCollection] - CollectionDTO object including the new collection data.
* @param {string} [parentCollectionId] - Specifies the parent collection identifier (optional, defaults to root).
* @returns {Promise<number>} - The created collection identifier.
* @throws {WriteError} - If there are errors while writing data.
*/
async execute(
newCollection: CollectionDTO,
parentCollectionId: number | string = ROOT_COLLECTION_ALIAS
): Promise<number> {
return await this.collectionsRepository.createCollection(newCollection, parentCollectionId)
}
}
5 changes: 4 additions & 1 deletion src/collections/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { CreateCollection } from './domain/useCases/CreateCollection'
import { GetCollection } from './domain/useCases/GetCollection'

import { CollectionsRepository } from './infra/repositories/CollectionsRepository'

const collectionsRepository = new CollectionsRepository()

const getCollection = new GetCollection(collectionsRepository)
const createCollection = new CreateCollection(collectionsRepository)

export { getCollection }
export { getCollection, createCollection }
export { Collection } from './domain/models/Collection'
export { CollectionDTO } from './domain/dtos/CollectionDTO'
36 changes: 36 additions & 0 deletions src/collections/infra/repositories/CollectionsRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@ import { ApiRepository } from '../../../core/infra/repositories/ApiRepository'
import { ICollectionsRepository } from '../../domain/repositories/ICollectionsRepository'
import { transformCollectionResponseToCollection } from './transformers/collectionTransformers'
import { Collection, ROOT_COLLECTION_ALIAS } from '../../domain/models/Collection'
import { CollectionDTO } from '../../domain/dtos/CollectionDTO'

export interface NewCollectionRequestPayload {
alias: string
name: string
dataverseContacts: NewCollectionContactRequestPayload[]
dataverseType: string
}

export interface NewCollectionContactRequestPayload {
contactEmail: string
}

export class CollectionsRepository extends ApiRepository implements ICollectionsRepository {
private readonly collectionsResourceName: string = 'dataverses'
Expand All @@ -17,4 +29,28 @@ export class CollectionsRepository extends ApiRepository implements ICollections
throw error
})
}

public async createCollection(
collectionDTO: CollectionDTO,
parentCollectionId: number | string = ROOT_COLLECTION_ALIAS
): Promise<number> {
const dataverseContacts: NewCollectionContactRequestPayload[] = collectionDTO.contacts.map(
(contact) => ({
contactEmail: contact
})
)

const requestBody: NewCollectionRequestPayload = {
alias: collectionDTO.alias,
name: collectionDTO.name,
dataverseContacts: dataverseContacts,
dataverseType: collectionDTO.type.toString()
}

return this.doPost(`/${this.collectionsResourceName}/${parentCollectionId}`, requestBody)
.then((response) => response.data.data.id)
.catch((error) => {
throw error
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ const transformPayloadToCollection = (collectionPayload: CollectionPayload): Col
name: collectionPayload.name,
isReleased: collectionPayload.isReleased,
affiliation: collectionPayload.affiliation,
description: transformHtmlToMarkdown(collectionPayload.description),
...(collectionPayload.description && {
description: transformHtmlToMarkdown(collectionPayload.description)
}),
...(collectionPayload.isPartOf && {
isPartOf: transformPayloadToOwnerNode(collectionPayload.isPartOf)
})
Expand Down
2 changes: 1 addition & 1 deletion src/files/domain/useCases/AddUploadedFileToDataset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export class AddUploadedFileToDataset implements UseCase<void> {
* @param {File} [file] - The file object that has been uploaded.
* @param {string} [storageId] - The storage identifier associated with the uploaded file.
* @returns {Promise<void>} A promise that resolves when the file has been successfully added to the dataset.
*
* @throws {DirectUploadClientError} - If there are errors while performing the operation.
*/
async execute(datasetId: number | string, file: File, storageId: string): Promise<void> {
await this.directUploadClient.addUploadedFileToDataset(datasetId, file, storageId)
Expand Down
2 changes: 1 addition & 1 deletion src/files/domain/useCases/UploadFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export class UploadFile implements UseCase<string> {
* @param {function(number): void} [progress] - A callback function to monitor the upload progress, receiving the current progress as a number.
* @param {AbortController} [abortController] - An AbortController to manage and abort the upload process if necessary.
* @returns {Promise<string>} A promise that resolves to the storage identifier of the uploaded file.
*
* @throws {DirectUploadClientError} - If there are errors while performing the operation.
*/
async execute(
datasetId: number | string,
Expand Down
45 changes: 45 additions & 0 deletions test/functional/collections/CreateCollection.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { ApiConfig, WriteError, createCollection, getCollection } from '../../../src'
import { TestConstants } from '../../testHelpers/TestConstants'
import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig'
import { createCollectionDTO } from '../../testHelpers/collections/collectionHelper'

describe('execute', () => {
beforeEach(async () => {
ApiConfig.init(
TestConstants.TEST_API_URL,
DataverseApiAuthMechanism.API_KEY,
process.env.TEST_API_KEY
)
})

test('should successfully create a new collection', async () => {
const testNewCollection = createCollectionDTO()
expect.assertions(1)
let createdCollectionId = 0
try {
createdCollectionId = await createCollection.execute(testNewCollection)
} catch (error) {
throw new Error('Collection should be created')
} finally {
const createdCollection = await getCollection.execute(createdCollectionId)
expect(createdCollection.alias).toBe(testNewCollection.alias)
}
})

test('should throw an error when the parent collection does not exist', async () => {
const testNewCollection = createCollectionDTO()
expect.assertions(2)
let writeError: WriteError
try {
await createCollection.execute(testNewCollection, TestConstants.TEST_DUMMY_COLLECTION_ID)
throw new Error('Use case should throw an error')
} catch (error) {
writeError = error
} finally {
expect(writeError).toBeInstanceOf(WriteError)
expect(writeError.message).toEqual(
`There was an error when writing the resource. Reason was: [404] Can't find dataverse with identifier='${TestConstants.TEST_DUMMY_COLLECTION_ID}'`
)
}
})
})
39 changes: 38 additions & 1 deletion test/integration/collections/CollectionsRepository.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { CollectionsRepository } from '../../../src/collections/infra/repositories/CollectionsRepository'
import { TestConstants } from '../../testHelpers/TestConstants'
import { ReadError } from '../../../src'
import { ReadError, WriteError } from '../../../src'
import { ApiConfig } from '../../../src'
import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig'
import {
createCollectionDTO,
createCollectionViaApi,
deleteCollectionViaApi
} from '../../testHelpers/collections/collectionHelper'
Expand Down Expand Up @@ -85,4 +86,40 @@ describe('CollectionsRepository', () => {
})
})
})

describe('createCollection', () => {
const testCreateCollectionAlias1 = 'createCollection-test-1'
const testCreateCollectionAlias2 = 'createCollection-test-2'

afterAll(async () => {
await deleteCollectionViaApi(testCreateCollectionAlias1)
await deleteCollectionViaApi(testCreateCollectionAlias2)
})

test('should create collection in root when no parent collection is set', async () => {
const actual = await sut.createCollection(createCollectionDTO(testCreateCollectionAlias1))
expect(typeof actual).toBe('number')
})

test('should create collection in parent collection when parent collection is set', async () => {
const actual = await sut.createCollection(
createCollectionDTO(testCreateCollectionAlias2),
testCollectionId
)
expect(typeof actual).toBe('number')
})

test('should return error when parent collection does not exist', async () => {
const expectedError = new WriteError(
`[404] Can't find dataverse with identifier='${TestConstants.TEST_DUMMY_COLLECTION_ID}'`
)
const testCreateCollectionAlias3 = 'createCollection-test-3'
await expect(
sut.createCollection(
createCollectionDTO(testCreateCollectionAlias3),
TestConstants.TEST_DUMMY_COLLECTION_ID
)
).rejects.toThrow(expectedError)
})
})
})
24 changes: 24 additions & 0 deletions test/testHelpers/collections/collectionHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { DvObjectType } from '../../../src'
import { CollectionPayload } from '../../../src/collections/infra/repositories/transformers/CollectionPayload'
import { TestConstants } from '../TestConstants'
import axios from 'axios'
import { CollectionDTO, CollectionType } from '../../../src/collections/domain/dtos/CollectionDTO'
import { NewCollectionRequestPayload } from '../../../src/collections/infra/repositories/CollectionsRepository'

const COLLECTION_ID = 11111
const COLLECTION_IS_RELEASED = 'true'
Expand Down Expand Up @@ -100,3 +102,25 @@ export async function setStorageDriverViaApi(
throw new Error(`Error while setting storage driver for collection ${collectionAlias}`)
}
}

export const createCollectionDTO = (alias = 'test-collection'): CollectionDTO => {
return {
alias: alias,
name: 'Test Collection',
contacts: ['dataverse@test.com'],
type: CollectionType.DEPARTMENT
}
}

export const createNewCollectionRequestPayload = (): NewCollectionRequestPayload => {
return {
alias: 'test-collection',
name: 'Test Collection',
dataverseContacts: [
{
contactEmail: 'dataverse@test.com'
}
],
dataverseType: 'DEPARTMENT'
}
}
Loading
Loading