Skip to content

Commit a3e1c0e

Browse files
authored
Merge pull request #155 from IQSS/75-create-collection-use-case
Adds create collection use case
2 parents 7fa8961 + be01a3f commit a3e1c0e

File tree

14 files changed

+330
-7
lines changed

14 files changed

+330
-7
lines changed

docs/useCases.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ The different use cases currently available in the package are classified below,
1111
- [Collections](#Collections)
1212
- [Collections read use cases](#collections-read-use-cases)
1313
- [Get a Collection](#get-a-collection)
14+
- [Collections write use cases](#collections-write-use-cases)
15+
- [Create a Collection](#create-a-collection)
1416
- [Datasets](#Datasets)
1517
- [Datasets read use cases](#datasets-read-use-cases)
1618
- [Get a Dataset](#get-a-dataset)
@@ -98,6 +100,39 @@ The `collectionIdOrAlias` is a generic collection identifier, which can be eithe
98100

99101
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.
100102

103+
### Collections Write Use Cases
104+
105+
#### Create a Collection
106+
107+
Creates a new Collection, given a [CollectionDTO](../src/collections/domain/dtos/CollectionDTO.ts) object and an optional parent collection identifier, which defaults to `root`.
108+
109+
##### Example call:
110+
111+
```typescript
112+
import { createCollection } from '@iqss/dataverse-client-javascript'
113+
114+
/* ... */
115+
116+
const collectionDTO: CollectionDTO = {
117+
alias: alias,
118+
name: 'Test Collection',
119+
contacts: ['[email protected]'],
120+
type: CollectionType.DEPARTMENT
121+
}
122+
123+
createCollection.execute(collectionDTO).then((createdCollectionId: number) => {
124+
/* ... */
125+
})
126+
127+
/* ... */
128+
```
129+
130+
_See [use case](../src/collections/domain/useCases/CreateCollection.ts) implementation_.
131+
132+
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.
133+
134+
The use case returns a number, which is the identifier of the created collection.
135+
101136
## Datasets
102137

103138
### Datasets Read Use Cases
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export interface CollectionDTO {
2+
alias: string
3+
name: string
4+
contacts: string[]
5+
type: CollectionType
6+
}
7+
8+
export enum CollectionType {
9+
RESEARCHERS = 'RESEARCHERS',
10+
RESEARCH_PROJECTS = 'RESEARCH_PROJECTS',
11+
JOURNALS = 'JOURNALS',
12+
ORGANIZATIONS_INSTITUTIONS = 'ORGANIZATIONS_INSTITUTIONS',
13+
TEACHING_COURSES = 'TEACHING_COURSES',
14+
UNCATEGORIZED = 'UNCATEGORIZED',
15+
LABORATORY = 'LABORATORY',
16+
RESEARCH_GROUP = 'RESEARCH_GROUP',
17+
DEPARTMENT = 'DEPARTMENT'
18+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
import { CollectionDTO } from '../dtos/CollectionDTO'
12
import { Collection } from '../models/Collection'
23

34
export interface ICollectionsRepository {
45
getCollection(collectionIdOrAlias: number | string): Promise<Collection>
6+
createCollection(
7+
collectionDTO: CollectionDTO,
8+
parentCollectionId: number | string
9+
): Promise<number>
510
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { UseCase } from '../../../core/domain/useCases/UseCase'
2+
import { CollectionDTO } from '../dtos/CollectionDTO'
3+
import { ROOT_COLLECTION_ALIAS } from '../models/Collection'
4+
import { ICollectionsRepository } from '../repositories/ICollectionsRepository'
5+
6+
export class CreateCollection implements UseCase<number> {
7+
private collectionsRepository: ICollectionsRepository
8+
9+
constructor(collectionsRepository: ICollectionsRepository) {
10+
this.collectionsRepository = collectionsRepository
11+
}
12+
13+
/**
14+
* Creates a new collection, given a CollectionDTO object and an optional collection identifier, which defaults to root.
15+
*
16+
* @param {CollectionDTO} [newCollection] - CollectionDTO object including the new collection data.
17+
* @param {string} [parentCollectionId] - Specifies the parent collection identifier (optional, defaults to root).
18+
* @returns {Promise<number>} - The created collection identifier.
19+
* @throws {WriteError} - If there are errors while writing data.
20+
*/
21+
async execute(
22+
newCollection: CollectionDTO,
23+
parentCollectionId: number | string = ROOT_COLLECTION_ALIAS
24+
): Promise<number> {
25+
return await this.collectionsRepository.createCollection(newCollection, parentCollectionId)
26+
}
27+
}

src/collections/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import { CreateCollection } from './domain/useCases/CreateCollection'
12
import { GetCollection } from './domain/useCases/GetCollection'
23

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

56
const collectionsRepository = new CollectionsRepository()
67

78
const getCollection = new GetCollection(collectionsRepository)
9+
const createCollection = new CreateCollection(collectionsRepository)
810

9-
export { getCollection }
11+
export { getCollection, createCollection }
1012
export { Collection } from './domain/models/Collection'
13+
export { CollectionDTO } from './domain/dtos/CollectionDTO'

src/collections/infra/repositories/CollectionsRepository.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@ import { ApiRepository } from '../../../core/infra/repositories/ApiRepository'
22
import { ICollectionsRepository } from '../../domain/repositories/ICollectionsRepository'
33
import { transformCollectionResponseToCollection } from './transformers/collectionTransformers'
44
import { Collection, ROOT_COLLECTION_ALIAS } from '../../domain/models/Collection'
5+
import { CollectionDTO } from '../../domain/dtos/CollectionDTO'
6+
7+
export interface NewCollectionRequestPayload {
8+
alias: string
9+
name: string
10+
dataverseContacts: NewCollectionContactRequestPayload[]
11+
dataverseType: string
12+
}
13+
14+
export interface NewCollectionContactRequestPayload {
15+
contactEmail: string
16+
}
517

618
export class CollectionsRepository extends ApiRepository implements ICollectionsRepository {
719
private readonly collectionsResourceName: string = 'dataverses'
@@ -17,4 +29,28 @@ export class CollectionsRepository extends ApiRepository implements ICollections
1729
throw error
1830
})
1931
}
32+
33+
public async createCollection(
34+
collectionDTO: CollectionDTO,
35+
parentCollectionId: number | string = ROOT_COLLECTION_ALIAS
36+
): Promise<number> {
37+
const dataverseContacts: NewCollectionContactRequestPayload[] = collectionDTO.contacts.map(
38+
(contact) => ({
39+
contactEmail: contact
40+
})
41+
)
42+
43+
const requestBody: NewCollectionRequestPayload = {
44+
alias: collectionDTO.alias,
45+
name: collectionDTO.name,
46+
dataverseContacts: dataverseContacts,
47+
dataverseType: collectionDTO.type.toString()
48+
}
49+
50+
return this.doPost(`/${this.collectionsResourceName}/${parentCollectionId}`, requestBody)
51+
.then((response) => response.data.data.id)
52+
.catch((error) => {
53+
throw error
54+
})
55+
}
2056
}

src/collections/infra/repositories/transformers/collectionTransformers.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ const transformPayloadToCollection = (collectionPayload: CollectionPayload): Col
1616
name: collectionPayload.name,
1717
isReleased: collectionPayload.isReleased,
1818
affiliation: collectionPayload.affiliation,
19-
description: transformHtmlToMarkdown(collectionPayload.description),
19+
...(collectionPayload.description && {
20+
description: transformHtmlToMarkdown(collectionPayload.description)
21+
}),
2022
...(collectionPayload.isPartOf && {
2123
isPartOf: transformPayloadToOwnerNode(collectionPayload.isPartOf)
2224
})

src/files/domain/useCases/AddUploadedFileToDataset.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export class AddUploadedFileToDataset implements UseCase<void> {
2020
* @param {File} [file] - The file object that has been uploaded.
2121
* @param {string} [storageId] - The storage identifier associated with the uploaded file.
2222
* @returns {Promise<void>} A promise that resolves when the file has been successfully added to the dataset.
23-
*
23+
* @throws {DirectUploadClientError} - If there are errors while performing the operation.
2424
*/
2525
async execute(datasetId: number | string, file: File, storageId: string): Promise<void> {
2626
await this.directUploadClient.addUploadedFileToDataset(datasetId, file, storageId)

src/files/domain/useCases/UploadFile.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export class UploadFile implements UseCase<string> {
2020
* @param {function(number): void} [progress] - A callback function to monitor the upload progress, receiving the current progress as a number.
2121
* @param {AbortController} [abortController] - An AbortController to manage and abort the upload process if necessary.
2222
* @returns {Promise<string>} A promise that resolves to the storage identifier of the uploaded file.
23-
*
23+
* @throws {DirectUploadClientError} - If there are errors while performing the operation.
2424
*/
2525
async execute(
2626
datasetId: number | string,
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { ApiConfig, WriteError, createCollection, getCollection } from '../../../src'
2+
import { TestConstants } from '../../testHelpers/TestConstants'
3+
import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig'
4+
import { createCollectionDTO } from '../../testHelpers/collections/collectionHelper'
5+
6+
describe('execute', () => {
7+
beforeEach(async () => {
8+
ApiConfig.init(
9+
TestConstants.TEST_API_URL,
10+
DataverseApiAuthMechanism.API_KEY,
11+
process.env.TEST_API_KEY
12+
)
13+
})
14+
15+
test('should successfully create a new collection', async () => {
16+
const testNewCollection = createCollectionDTO()
17+
expect.assertions(1)
18+
let createdCollectionId = 0
19+
try {
20+
createdCollectionId = await createCollection.execute(testNewCollection)
21+
} catch (error) {
22+
throw new Error('Collection should be created')
23+
} finally {
24+
const createdCollection = await getCollection.execute(createdCollectionId)
25+
expect(createdCollection.alias).toBe(testNewCollection.alias)
26+
}
27+
})
28+
29+
test('should throw an error when the parent collection does not exist', async () => {
30+
const testNewCollection = createCollectionDTO()
31+
expect.assertions(2)
32+
let writeError: WriteError
33+
try {
34+
await createCollection.execute(testNewCollection, TestConstants.TEST_DUMMY_COLLECTION_ID)
35+
throw new Error('Use case should throw an error')
36+
} catch (error) {
37+
writeError = error
38+
} finally {
39+
expect(writeError).toBeInstanceOf(WriteError)
40+
expect(writeError.message).toEqual(
41+
`There was an error when writing the resource. Reason was: [404] Can't find dataverse with identifier='${TestConstants.TEST_DUMMY_COLLECTION_ID}'`
42+
)
43+
}
44+
})
45+
})

0 commit comments

Comments
 (0)