Skip to content
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel
### Added

- New Use Case: [Get Collections For Linking Use Case](./docs/useCases.md#get-collections-for-linking).
- New Use Case: [Create a Dataset Template](./docs/useCases.md#create-a-dataset-template) under Collections.

### Changed

Expand Down
36 changes: 36 additions & 0 deletions docs/useCases.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ The different use cases currently available in the package are classified below,
- [Update Collection Featured Items](#update-collection-featured-items)
- [Delete Collection Featured Items](#delete-collection-featured-items)
- [Delete a Collection Featured Item](#delete-a-collection-featured-item)
- [Create a Dataset Template](#create-a-dataset-template)
- [Datasets](#Datasets)
- [Datasets read use cases](#datasets-read-use-cases)
- [Get a Dataset](#get-a-dataset)
Expand Down Expand Up @@ -567,6 +568,41 @@ deleteCollectionFeaturedItem.execute(featuredItemId)

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

#### Create a Dataset Template

Creates a dataset template for a given Dataverse collection id or alias.

##### Example call:

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

const collectionAlias = ':root'
const template: TemplateCreateDTO = {
name: 'Dataverse template',
isDefault: true,
fields: [
{
typeName: 'author',
typeClass: 'compound',
multiple: true,
value: [
{
authorName: { typeName: 'authorName', value: 'Belicheck, Bill' },
authorAffiliation: { typeName: 'authorIdentifierScheme', value: 'ORCID' }
}
]
}
],
instructions: [{ instructionField: 'author', instructionText: 'The author data' }]
}

await createDatasetTemplate.execute(template, collectionAlias)
```

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

## Datasets

### Datasets Read Use Cases
Expand Down
45 changes: 45 additions & 0 deletions src/collections/domain/dtos/CreateDatasetTemplateDTO.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { MetadataFieldTypeClass } from '../../../metadataBlocks/domain/models/MetadataBlock'

export interface CreateDatasetTemplateDTO {
name: string
isDefault?: boolean
fields?: TemplateFieldDTO[]
instructions?: TemplateInstructionDTO[]
}

export interface TemplateFieldDTO {
typeName: string
multiple: boolean
typeClass?: MetadataFieldTypeClass
value?: TemplateFieldValueDTO[]
}

export interface TemplateFieldValueDTO {
[key: string]:
| TemplateFieldValuePrimitiveDTO
| TemplateFieldValueCompoundDTO
| TemplateFieldValueControlledVocabularyDTO
}

export interface TemplateFieldValuePrimitiveDTO {
typeName: string
typeClass: MetadataFieldTypeClass.Primitive
value: string | string[]
}

export interface TemplateFieldValueCompoundDTO {
typeName: string
typeClass: MetadataFieldTypeClass.Compound
value: TemplateFieldValueDTO[]
}

export interface TemplateFieldValueControlledVocabularyDTO {
typeName: string
typeClass: MetadataFieldTypeClass.ControlledVocabulary
value: string
}

export interface TemplateInstructionDTO {
instructionField: string
instructionText: string
}
5 changes: 5 additions & 0 deletions src/collections/domain/repositories/ICollectionsRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { CollectionItemType } from '../../../collections/domain/models/Collectio
import { CollectionLinks } from '../models/CollectionLinks'
import { CollectionSummary } from '../models/CollectionSummary'
import { LinkingObjectType } from '../useCases/GetCollectionsForLinking'
import { CreateDatasetTemplateDTO } from '../dtos/CreateDatasetTemplateDTO'

export interface ICollectionsRepository {
getCollection(collectionIdOrAlias: number | string): Promise<Collection>
Expand Down Expand Up @@ -68,4 +69,8 @@ export interface ICollectionsRepository {
searchTerm: string,
alreadyLinked: boolean
): Promise<CollectionSummary[]>
createDatasetTemplate(
collectionIdOrAlias: number | string,
template: CreateDatasetTemplateDTO
): Promise<void>
}
27 changes: 27 additions & 0 deletions src/collections/domain/useCases/CreateDatasetTemplate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { ROOT_COLLECTION_ID } from '../models/Collection'
import { UseCase } from '../../../core/domain/useCases/UseCase'
import { ICollectionsRepository } from '../repositories/ICollectionsRepository'
import { CreateDatasetTemplateDTO } from '../dtos/CreateDatasetTemplateDTO'

export class CreateDatasetTemplate implements UseCase<void> {
private collectionsRepository: ICollectionsRepository

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

/**
* Creates a Dataset Template in the specified collection.
*
* @param {CreateDatasetTemplateDTO} template - Template definition payload.
* @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<void>}
*/
async execute(
template: CreateDatasetTemplateDTO,
collectionIdOrAlias: number | string = ROOT_COLLECTION_ID
): Promise<void> {
return await this.collectionsRepository.createDatasetTemplate(collectionIdOrAlias, template)
}
}
5 changes: 4 additions & 1 deletion src/collections/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { LinkCollection } from './domain/useCases/LinkCollection'
import { UnlinkCollection } from './domain/useCases/UnlinkCollection'
import { GetCollectionLinks } from './domain/useCases/GetCollectionLinks'
import { GetCollectionsForLinking } from './domain/useCases/GetCollectionsForLinking'
import { CreateDatasetTemplate } from './domain/useCases/CreateDatasetTemplate'

const collectionsRepository = new CollectionsRepository()

Expand All @@ -36,6 +37,7 @@ const linkCollection = new LinkCollection(collectionsRepository)
const unlinkCollection = new UnlinkCollection(collectionsRepository)
const getCollectionLinks = new GetCollectionLinks(collectionsRepository)
const getCollectionsForLinking = new GetCollectionsForLinking(collectionsRepository)
const createDatasetTemplate = new CreateDatasetTemplate(collectionsRepository)

export {
getCollection,
Expand All @@ -54,7 +56,8 @@ export {
linkCollection,
unlinkCollection,
getCollectionLinks,
getCollectionsForLinking
getCollectionsForLinking,
createDatasetTemplate
}
export { Collection, CollectionInputLevel } from './domain/models/Collection'
export { CollectionFacet } from './domain/models/CollectionFacet'
Expand Down
15 changes: 15 additions & 0 deletions src/collections/infra/repositories/CollectionsRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { ReadError } from '../../../core/domain/repositories/ReadError'
import { CollectionLinks } from '../../domain/models/CollectionLinks'
import { CollectionSummary } from '../../domain/models/CollectionSummary'
import { LinkingObjectType } from '../../domain/useCases/GetCollectionsForLinking'
import { CreateDatasetTemplateDTO } from '../../domain/dtos/CreateDatasetTemplateDTO'

export interface NewCollectionRequestPayload {
alias: string
Expand Down Expand Up @@ -528,4 +529,18 @@ export class CollectionsRepository extends ApiRepository implements ICollections
throw error
})
}

public async createDatasetTemplate(
collectionIdOrAlias: number | string,
template: CreateDatasetTemplateDTO
): Promise<void> {
return this.doPost(
`/${this.collectionsResourceName}/${collectionIdOrAlias}/templates`,
template
)
.then(() => undefined)
.catch((error) => {
throw error
})
}
}
62 changes: 62 additions & 0 deletions test/functional/collections/createDatasetTemplate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { ApiConfig } from '../../../src'
import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig'
import { TestConstants } from '../../testHelpers/TestConstants'
import { getDatasetTemplates } from '../../../src/datasets'
import { CreateDatasetTemplateDTO } from '../../../src/collections/domain/dtos/CreateDatasetTemplateDTO'
import { createDatasetTemplate } from '../../../src/collections'
import { MetadataFieldTypeClass } from '../../../src/metadataBlocks/domain/models/MetadataBlock'
import { deleteDatasetTemplateViaApi } from '../../testHelpers/datasets/datasetTemplatesHelper'

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

test('should create a template in :root with provided JSON', async () => {
const templateDto: CreateDatasetTemplateDTO = {
name: 'TestDataverse template',
isDefault: true,
fields: [
{
typeName: 'author',
typeClass: MetadataFieldTypeClass.Compound,
multiple: true,
value: [
{
authorName: {
typeName: 'authorName',
typeClass: MetadataFieldTypeClass.Primitive,
value: 'Belicheck, Bill'
},
authorAffiliation: {
typeName: 'authorIdentifierScheme',
typeClass: MetadataFieldTypeClass.Primitive,
value: 'ORCID'
}
}
]
}
],
instructions: [
{
instructionField: 'author',
instructionText: 'The author data'
}
]
}
await createDatasetTemplate.execute(templateDto)
const templates = await getDatasetTemplates.execute(':root')

expect(templates[templates.length - 1].name).toBe(templateDto.name)
expect(templates[templates.length - 1].isDefault).toBe(templateDto.isDefault)
expect(templates[templates.length - 1].instructions.length).toBe(
templateDto.instructions?.length ?? 0
)

deleteDatasetTemplateViaApi(templates[templates.length - 1].id)
})
})
62 changes: 61 additions & 1 deletion test/integration/collections/CollectionsRepository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import {
getDatasetFiles,
restrictFile,
deleteFile,
linkDataset
linkDataset,
createDatasetTemplate,
MetadataFieldTypeClass
} from '../../../src'
import { ApiConfig } from '../../../src'
import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig'
Expand Down Expand Up @@ -58,6 +60,9 @@ import {
DvObjectFeaturedItemDTO,
FeaturedItemsDTO
} from '../../../src/collections/domain/dtos/FeaturedItemsDTO'
import { CreateDatasetTemplateDTO } from '../../../src/collections/domain/dtos/CreateDatasetTemplateDTO'
import { getDatasetTemplates } from '../../../src/datasets'
import { deleteDatasetTemplateViaApi } from '../../testHelpers/datasets/datasetTemplatesHelper'

describe('CollectionsRepository', () => {
const testCollectionAlias = 'collectionsRepositoryTestCollection'
Expand Down Expand Up @@ -2143,4 +2148,59 @@ describe('CollectionsRepository', () => {
await expect(sut.getCollectionLinks(invalidCollectionId)).rejects.toThrow(expectedError)
})
})

describe('createDatasetTemplate', () => {
const templateDto: CreateDatasetTemplateDTO = {
name: 'CollectionsRepository template',
isDefault: true,
fields: [
{
typeName: 'author',
typeClass: MetadataFieldTypeClass.Compound,
multiple: true,
value: [
{
authorName: {
typeName: 'authorName',
typeClass: MetadataFieldTypeClass.Primitive,
value: 'Belicheck, Bill'
},
authorAffiliation: {
typeName: 'authorIdentifierScheme',
typeClass: MetadataFieldTypeClass.Primitive,
value: 'ORCID'
}
}
]
}
],
instructions: [
{
instructionField: 'author',
instructionText: 'The author data'
}
]
}
test('should create a template in :root with provided JSON', async () => {
await createDatasetTemplate.execute(templateDto)
const templates = await getDatasetTemplates.execute(':root')

expect(templates[templates.length - 1].name).toBe(templateDto.name)
expect(templates[templates.length - 1].isDefault).toBe(templateDto.isDefault)
expect(templates[templates.length - 1].instructions.length).toBe(
templateDto.instructions?.length ?? 0
)

deleteDatasetTemplateViaApi(templates[templates.length - 1].id)
})

test('should return error when creating a template with invalidCollectionAlias', async () => {
const expectedError = new WriteError(
`[404] Can't find dataverse with identifier='invalidCollectionAlias'`
)
await expect(
createDatasetTemplate.execute(templateDto, 'invalidCollectionAlias')
).rejects.toThrow(expectedError)
})
})
})
46 changes: 46 additions & 0 deletions test/unit/collections/createDatasetTemplate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { CreateDatasetTemplate } from '../../../src/collections/domain/useCases/CreateDatasetTemplate'
import { ICollectionsRepository } from '../../../src/collections/domain/repositories/ICollectionsRepository'
import { CreateDatasetTemplateDTO } from '../../../src/collections/domain/dtos/CreateDatasetTemplateDTO'
import { WriteError } from '../../../src'

describe('execute', () => {
const testTemplateDTO = { name: 't' } as CreateDatasetTemplateDTO
const testCollectionId = 1

test('should return undefined when repository call is successful', async () => {
const collectionRepositoryStub: ICollectionsRepository = {} as ICollectionsRepository
collectionRepositoryStub.createDatasetTemplate = jest.fn().mockResolvedValue(testCollectionId)
const sut = new CreateDatasetTemplate(collectionRepositoryStub)

const actual = await sut.execute(testTemplateDTO)

expect(collectionRepositoryStub.createDatasetTemplate).toHaveBeenCalledWith(
':root',
testTemplateDTO
)
expect(actual).toEqual(testCollectionId)
})

test('should call repository with provided collection id/alias', async () => {
const collectionRepositoryStub: ICollectionsRepository = {} as ICollectionsRepository
collectionRepositoryStub.createDatasetTemplate = jest.fn().mockResolvedValue(testCollectionId)

const sut = new CreateDatasetTemplate(collectionRepositoryStub)
const actual = await sut.execute(testTemplateDTO, 'alias123')

expect(collectionRepositoryStub.createDatasetTemplate).toHaveBeenCalledWith(
'alias123',
testTemplateDTO
)

expect(actual).toEqual(testCollectionId)
})

test('should return error result on repository error', async () => {
const collectionRepositoryStub: ICollectionsRepository = {} as ICollectionsRepository
collectionRepositoryStub.createDatasetTemplate = jest.fn().mockRejectedValue(new WriteError())
const testCreateTemplate = new CreateDatasetTemplate(collectionRepositoryStub)

await expect(testCreateTemplate.execute(testTemplateDTO)).rejects.toThrow(WriteError)
})
})