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

Extended create collection use case #162

Merged
merged 12 commits into from
Jul 26, 2024
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
g-saracca marked this conversation as resolved.
Show resolved Hide resolved

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[]
}
g-saracca marked this conversation as resolved.
Show resolved Hide resolved

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
Loading