diff --git a/CHANGELOG.md b/CHANGELOG.md index 57ea977c..906a6918 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/useCases.md b/docs/useCases.md index 6d5565fe..5ec382c6 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -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) @@ -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 diff --git a/src/collections/domain/dtos/CreateDatasetTemplateDTO.ts b/src/collections/domain/dtos/CreateDatasetTemplateDTO.ts new file mode 100644 index 00000000..52ba24cc --- /dev/null +++ b/src/collections/domain/dtos/CreateDatasetTemplateDTO.ts @@ -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 +} diff --git a/src/collections/domain/repositories/ICollectionsRepository.ts b/src/collections/domain/repositories/ICollectionsRepository.ts index bc8960c8..cae28415 100644 --- a/src/collections/domain/repositories/ICollectionsRepository.ts +++ b/src/collections/domain/repositories/ICollectionsRepository.ts @@ -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 @@ -68,4 +69,8 @@ export interface ICollectionsRepository { searchTerm: string, alreadyLinked: boolean ): Promise + createDatasetTemplate( + collectionIdOrAlias: number | string, + template: CreateDatasetTemplateDTO + ): Promise } diff --git a/src/collections/domain/useCases/CreateDatasetTemplate.ts b/src/collections/domain/useCases/CreateDatasetTemplate.ts new file mode 100644 index 00000000..ffb443f6 --- /dev/null +++ b/src/collections/domain/useCases/CreateDatasetTemplate.ts @@ -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 { + 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} + */ + async execute( + template: CreateDatasetTemplateDTO, + collectionIdOrAlias: number | string = ROOT_COLLECTION_ID + ): Promise { + return await this.collectionsRepository.createDatasetTemplate(collectionIdOrAlias, template) + } +} diff --git a/src/collections/index.ts b/src/collections/index.ts index 59e2e50b..df7b6af5 100644 --- a/src/collections/index.ts +++ b/src/collections/index.ts @@ -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() @@ -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, @@ -54,7 +56,8 @@ export { linkCollection, unlinkCollection, getCollectionLinks, - getCollectionsForLinking + getCollectionsForLinking, + createDatasetTemplate } export { Collection, CollectionInputLevel } from './domain/models/Collection' export { CollectionFacet } from './domain/models/CollectionFacet' diff --git a/src/collections/infra/repositories/CollectionsRepository.ts b/src/collections/infra/repositories/CollectionsRepository.ts index e0e459b0..53ebfff3 100644 --- a/src/collections/infra/repositories/CollectionsRepository.ts +++ b/src/collections/infra/repositories/CollectionsRepository.ts @@ -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 @@ -528,4 +529,18 @@ export class CollectionsRepository extends ApiRepository implements ICollections throw error }) } + + public async createDatasetTemplate( + collectionIdOrAlias: number | string, + template: CreateDatasetTemplateDTO + ): Promise { + return this.doPost( + `/${this.collectionsResourceName}/${collectionIdOrAlias}/templates`, + template + ) + .then(() => undefined) + .catch((error) => { + throw error + }) + } } diff --git a/test/functional/collections/createDatasetTemplate.test.ts b/test/functional/collections/createDatasetTemplate.test.ts new file mode 100644 index 00000000..84309e52 --- /dev/null +++ b/test/functional/collections/createDatasetTemplate.test.ts @@ -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) + }) +}) diff --git a/test/integration/collections/CollectionsRepository.test.ts b/test/integration/collections/CollectionsRepository.test.ts index 1d6041b6..c5828eb2 100644 --- a/test/integration/collections/CollectionsRepository.test.ts +++ b/test/integration/collections/CollectionsRepository.test.ts @@ -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' @@ -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' @@ -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) + }) + }) }) diff --git a/test/unit/collections/createDatasetTemplate.ts b/test/unit/collections/createDatasetTemplate.ts new file mode 100644 index 00000000..0004b7a7 --- /dev/null +++ b/test/unit/collections/createDatasetTemplate.ts @@ -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) + }) +})