Skip to content

Commit

Permalink
Merge pull request #162 from IQSS/156-create-collection-second-part
Browse files Browse the repository at this point in the history
Extended create collection use case
  • Loading branch information
ekraffmiller authored Jul 26, 2024
2 parents a3e1c0e + eee6302 commit 3c135eb
Show file tree
Hide file tree
Showing 14 changed files with 341 additions and 14 deletions.
28 changes: 28 additions & 0 deletions docs/useCases.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ 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)
- [Get Collection Facets](#get-collection-facets)
- [Collections write use cases](#collections-write-use-cases)
- [Create a Collection](#create-a-collection)
- [Datasets](#Datasets)
Expand Down Expand Up @@ -100,6 +101,33 @@ 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.

#### Get Collection Facets

Returns the names of the configured collection facets, given a collection identifier or alias.

##### Example call:

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

const collectionIdOrAlias = 12345

getCollectionFacets
.execute(collectionId)
.then((facets: string[]) => {
/* ... */
})
.catch((error: Error) => {
/* ... */
})
```

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

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

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
Expand Down
11 changes: 11 additions & 0 deletions src/collections/domain/dtos/CollectionDTO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@ export interface CollectionDTO {
name: string
contacts: string[]
type: CollectionType
affiliation?: string
description?: string
metadataBlockNames?: string[]
facetIds?: string[]
inputLevels?: CollectionInputLevelDTO[]
}

export interface CollectionInputLevelDTO {
datasetFieldName: string
include: boolean
required: boolean
}

export enum CollectionType {
Expand Down
8 changes: 8 additions & 0 deletions src/collections/domain/models/Collection.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { DvObjectOwnerNode } from '../../../core'

export interface Collection {
id: number
alias: string
Expand All @@ -7,6 +8,13 @@ export interface Collection {
affiliation?: string
description?: string
isPartOf: DvObjectOwnerNode
inputLevels?: CollectionInputLevel[]
}

export interface CollectionInputLevel {
datasetFieldName: string
include: boolean
required: boolean
}

export const ROOT_COLLECTION_ALIAS = 'root'
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export interface ICollectionsRepository {
collectionDTO: CollectionDTO,
parentCollectionId: number | string
): Promise<number>
getCollectionFacets(collectionIdOrAlias: number | string): Promise<string[]>
}
22 changes: 22 additions & 0 deletions src/collections/domain/useCases/GetCollectionFacets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { UseCase } from '../../../core/domain/useCases/UseCase'
import { ICollectionsRepository } from '../repositories/ICollectionsRepository'
import { ROOT_COLLECTION_ALIAS } from '../models/Collection'

export class GetCollectionFacets implements UseCase<string[]> {
private collectionsRepository: ICollectionsRepository

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

/**
* Returns the names of the configured collection facets, given a collection identifier or alias.
*
* @param {number | string} [collectionIdOrAlias = 'root'] - A generic collection identifier, which can be either a string (for queries by CollectionAlias), or a number (for queries by CollectionId)
* If this parameter is not set, the default value is: 'root'
* @returns {Promise<string[]>}
*/
async execute(collectionIdOrAlias: number | string = ROOT_COLLECTION_ALIAS): Promise<string[]> {
return await this.collectionsRepository.getCollectionFacets(collectionIdOrAlias)
}
}
8 changes: 5 additions & 3 deletions src/collections/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { CreateCollection } from './domain/useCases/CreateCollection'
import { GetCollection } from './domain/useCases/GetCollection'
import { GetCollectionFacets } from './domain/useCases/GetCollectionFacets'

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

const collectionsRepository = new CollectionsRepository()

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

export { getCollection, createCollection }
export { Collection } from './domain/models/Collection'
export { CollectionDTO } from './domain/dtos/CollectionDTO'
export { getCollection, createCollection, getCollectionFacets }
export { Collection, CollectionInputLevel } from './domain/models/Collection'
export { CollectionDTO, CollectionInputLevelDTO } from './domain/dtos/CollectionDTO'
35 changes: 34 additions & 1 deletion src/collections/infra/repositories/CollectionsRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,25 @@ export interface NewCollectionRequestPayload {
name: string
dataverseContacts: NewCollectionContactRequestPayload[]
dataverseType: string
metadataBlocks: NewCollectionMetadataBlocksRequestPayload
}

export interface NewCollectionContactRequestPayload {
contactEmail: string
}

export interface NewCollectionMetadataBlocksRequestPayload {
metadataBlockNames: string[]
facetIds: string[]
inputLevels: NewCollectionInputLevelRequestPayload[]
}

export interface NewCollectionInputLevelRequestPayload {
datasetFieldTypeName: string
include: boolean
required: boolean
}

export class CollectionsRepository extends ApiRepository implements ICollectionsRepository {
private readonly collectionsResourceName: string = 'dataverses'

Expand All @@ -40,11 +53,23 @@ export class CollectionsRepository extends ApiRepository implements ICollections
})
)

const inputLevelsRequestBody: NewCollectionInputLevelRequestPayload[] =
collectionDTO.inputLevels.map((inputLevel) => ({
datasetFieldTypeName: inputLevel.datasetFieldName,
include: inputLevel.include,
required: inputLevel.required
}))

const requestBody: NewCollectionRequestPayload = {
alias: collectionDTO.alias,
name: collectionDTO.name,
dataverseContacts: dataverseContacts,
dataverseType: collectionDTO.type.toString()
dataverseType: collectionDTO.type,
metadataBlocks: {
metadataBlockNames: collectionDTO.metadataBlockNames,
facetIds: collectionDTO.facetIds,
inputLevels: inputLevelsRequestBody
}
}

return this.doPost(`/${this.collectionsResourceName}/${parentCollectionId}`, requestBody)
Expand All @@ -53,4 +78,12 @@ export class CollectionsRepository extends ApiRepository implements ICollections
throw error
})
}

public async getCollectionFacets(collectionIdOrAlias: string | number): Promise<string[]> {
return this.doGet(`/${this.collectionsResourceName}/${collectionIdOrAlias}/facets`, true)
.then((response) => response.data.data)
.catch((error) => {
throw error
})
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { OwnerNodePayload } from '../../../../core/infra/repositories/transformers/OwnerNodePayload'

export interface CollectionPayload {
id: number
alias: string
Expand All @@ -7,4 +8,11 @@ export interface CollectionPayload {
isReleased: string
description?: string
isPartOf: OwnerNodePayload
inputLevels?: CollectionInputLevelPayload[]
}

export interface CollectionInputLevelPayload {
datasetFieldTypeName: string
required: boolean
include: boolean
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Collection } from '../../../domain/models/Collection'
import { Collection, CollectionInputLevel } from '../../../domain/models/Collection'
import { AxiosResponse } from 'axios'
import { CollectionPayload } from './CollectionPayload'
import { CollectionInputLevelPayload, CollectionPayload } from './CollectionPayload'
import { transformPayloadToOwnerNode } from '../../../../core/infra/repositories/transformers/dvObjectOwnerNodeTransformer'
import { transformHtmlToMarkdown } from '../../../../datasets/infra/repositories/transformers/datasetTransformers'

Expand All @@ -21,7 +21,20 @@ const transformPayloadToCollection = (collectionPayload: CollectionPayload): Col
}),
...(collectionPayload.isPartOf && {
isPartOf: transformPayloadToOwnerNode(collectionPayload.isPartOf)
}),
...(collectionPayload.inputLevels && {
inputLevels: transformInputLevelsPayloadToInputLevels(collectionPayload.inputLevels)
})
}
return collectionModel
}

const transformInputLevelsPayloadToInputLevels = (
inputLevelsPayload: CollectionInputLevelPayload[]
): CollectionInputLevel[] => {
return inputLevelsPayload.map((inputLevel) => ({
datasetFieldName: inputLevel.datasetFieldTypeName,
include: inputLevel.include,
required: inputLevel.required
}))
}
44 changes: 44 additions & 0 deletions test/functional/collections/GetCollectionFacets.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { ApiConfig, ReadError, getCollectionFacets } from '../../../src'
import { TestConstants } from '../../testHelpers/TestConstants'
import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig'
import { ROOT_COLLECTION_ALIAS } from '../../../src/collections/domain/models/Collection'

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

test('should return facets when a valid collection alias is provided', async () => {
let actual: string[] = []
try {
actual = await getCollectionFacets.execute(ROOT_COLLECTION_ALIAS)
} catch (error) {
throw new Error('Facets should be retrieved')
} finally {
expect(actual).toContain('authorName')
expect(actual).toContain('subject')
expect(actual).toContain('keywordValue')
expect(actual).toContain('dateOfDeposit')
}
})

test('should throw an error when collection does not exist', async () => {
expect.assertions(2)
let readError: ReadError
try {
await getCollectionFacets.execute(TestConstants.TEST_DUMMY_COLLECTION_ID)
throw new Error('Use case should throw an error')
} catch (error) {
readError = error
} finally {
expect(readError).toBeInstanceOf(ReadError)
expect(readError.message).toEqual(
`There was an error when reading the resource. Reason was: [404] Can't find dataverse with identifier='${TestConstants.TEST_DUMMY_COLLECTION_ID}'`
)
}
})
})
51 changes: 47 additions & 4 deletions test/integration/collections/CollectionsRepository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ describe('CollectionsRepository', () => {
test('should return the root collection of the Dataverse installation if no parameter is passed AS `root`', async () => {
const actual = await sut.getCollection()
expect(actual.alias).toBe(ROOT_COLLECTION_ALIAS)
expect(actual.id).toBe(1)
expect(actual.name).toBe('Root')
expect(actual.alias).toBe('root')
expect(actual.isReleased).toBe(true)
expect(actual.affiliation).toBe(undefined)
expect(actual.description).toBe('The root dataverse.')
expect(actual.inputLevels).toBe(undefined)
})

test('should return isReleased is true for root collection', async () => {
Expand Down Expand Up @@ -97,16 +104,32 @@ describe('CollectionsRepository', () => {
})

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')
const newCollectionDTO = createCollectionDTO(testCreateCollectionAlias1)
const actualId = await sut.createCollection(newCollectionDTO)
expect(typeof actualId).toBe('number')

const createdCollection = await sut.getCollection(actualId)
expect(createdCollection.id).toBe(actualId)
expect(createdCollection.alias).toBe(newCollectionDTO.alias)
expect(createdCollection.name).toBe(newCollectionDTO.name)
expect(createdCollection.affiliation).toBe(newCollectionDTO.affiliation)
expect(createdCollection.isPartOf.type).toBe('DATAVERSE')
expect(createdCollection.isPartOf.displayName).toBe('Root')
expect(createdCollection.isPartOf.identifier).toBe('root')

expect(createdCollection.inputLevels?.length).toBe(1)
const inputLevel = createdCollection.inputLevels?.[0]
expect(inputLevel?.datasetFieldName).toBe('geographicCoverage')
expect(inputLevel?.include).toBe(true)
expect(inputLevel?.required).toBe(true)
})

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

test('should return error when parent collection does not exist', async () => {
Expand All @@ -122,4 +145,24 @@ describe('CollectionsRepository', () => {
).rejects.toThrow(expectedError)
})
})

describe('getCollectionFacets', () => {
test('should return collection facets given a valid collection alias', async () => {
const actual = await sut.getCollectionFacets(testCollectionAlias)
expect(actual).toContain('authorName')
expect(actual).toContain('subject')
expect(actual).toContain('keywordValue')
expect(actual).toContain('dateOfDeposit')
})

test('should return error when collection does not exist', async () => {
const expectedError = new ReadError(
`[404] Can't find dataverse with identifier='${TestConstants.TEST_DUMMY_COLLECTION_ALIAS}'`
)

await expect(
sut.getCollectionFacets(TestConstants.TEST_DUMMY_COLLECTION_ALIAS)
).rejects.toThrow(expectedError)
})
})
})
Loading

0 comments on commit 3c135eb

Please sign in to comment.