diff --git a/src/index.ts b/src/index.ts index ada6375d..549bb7d2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,3 +3,4 @@ export * from './info'; export * from './users'; export * from './auth'; export * from './datasets'; +export * from './metadataBlocks'; diff --git a/src/metadataBlocks/domain/models/MetadataBlock.ts b/src/metadataBlocks/domain/models/MetadataBlock.ts new file mode 100644 index 00000000..b95bf799 --- /dev/null +++ b/src/metadataBlocks/domain/models/MetadataBlock.ts @@ -0,0 +1,19 @@ +export interface MetadataBlock { + id: number; + name: string; + displayName: string; + metadataFields: Record; +} + +export interface MetadataFieldInfo { + name: string; + displayName: string; + title: string; + type: string; + watermark: string; + description: string; + multiple: boolean; + isControlledVocabulary: boolean; + displayFormat: string; + childMetadataFields?: Record; +} diff --git a/src/metadataBlocks/domain/repositories/IMetadataBlocksRepository.ts b/src/metadataBlocks/domain/repositories/IMetadataBlocksRepository.ts new file mode 100644 index 00000000..e50cf64d --- /dev/null +++ b/src/metadataBlocks/domain/repositories/IMetadataBlocksRepository.ts @@ -0,0 +1,5 @@ +import { MetadataBlock } from '../models/MetadataBlock'; + +export interface IMetadataBlocksRepository { + getMetadataBlockByName(metadataBlockName: string): Promise; +} diff --git a/src/metadataBlocks/domain/useCases/GetMetadataBlockByName.ts b/src/metadataBlocks/domain/useCases/GetMetadataBlockByName.ts new file mode 100644 index 00000000..dc09c1c4 --- /dev/null +++ b/src/metadataBlocks/domain/useCases/GetMetadataBlockByName.ts @@ -0,0 +1,15 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase'; +import { IMetadataBlocksRepository } from '../repositories/IMetadataBlocksRepository'; +import { MetadataBlock } from '../models/MetadataBlock'; + +export class GetMetadataBlockByName implements UseCase { + private metadataBlocksRepository: IMetadataBlocksRepository; + + constructor(metadataBlocksRepository: IMetadataBlocksRepository) { + this.metadataBlocksRepository = metadataBlocksRepository; + } + + async execute(metadataBlockName: string): Promise { + return await this.metadataBlocksRepository.getMetadataBlockByName(metadataBlockName); + } +} diff --git a/src/metadataBlocks/index.ts b/src/metadataBlocks/index.ts new file mode 100644 index 00000000..db6d7a2f --- /dev/null +++ b/src/metadataBlocks/index.ts @@ -0,0 +1,9 @@ +import { GetMetadataBlockByName } from './domain/useCases/GetMetadataBlockByName'; +import { MetadataBlocksRepository } from './infra/repositories/MetadataBlocksRepository'; + +const metadataBlocksRepository = new MetadataBlocksRepository(); + +const getMetadataBlockByName = new GetMetadataBlockByName(metadataBlocksRepository); + +export { getMetadataBlockByName }; +export { MetadataBlock, MetadataFieldInfo } from './domain/models/MetadataBlock'; diff --git a/src/metadataBlocks/infra/repositories/MetadataBlocksRepository.ts b/src/metadataBlocks/infra/repositories/MetadataBlocksRepository.ts new file mode 100644 index 00000000..740a13fd --- /dev/null +++ b/src/metadataBlocks/infra/repositories/MetadataBlocksRepository.ts @@ -0,0 +1,14 @@ +import { ApiRepository } from '../../../core/infra/repositories/ApiRepository'; +import { IMetadataBlocksRepository } from '../../domain/repositories/IMetadataBlocksRepository'; +import { MetadataBlock } from '../../domain/models/MetadataBlock'; +import { transformMetadataBlockResponseToMetadataBlock } from './transformers/metadataBlockTransformers'; + +export class MetadataBlocksRepository extends ApiRepository implements IMetadataBlocksRepository { + public async getMetadataBlockByName(metadataBlockName: string): Promise { + return this.doGet(`/metadatablocks/${metadataBlockName}`) + .then((response) => transformMetadataBlockResponseToMetadataBlock(response)) + .catch((error) => { + throw error; + }); + } +} diff --git a/src/metadataBlocks/infra/repositories/transformers/metadataBlockTransformers.ts b/src/metadataBlocks/infra/repositories/transformers/metadataBlockTransformers.ts new file mode 100644 index 00000000..9ed9c1ba --- /dev/null +++ b/src/metadataBlocks/infra/repositories/transformers/metadataBlockTransformers.ts @@ -0,0 +1,47 @@ +import { AxiosResponse } from 'axios'; +import { MetadataBlock, MetadataFieldInfo } from '../../../domain/models/MetadataBlock'; + +export const transformMetadataBlockResponseToMetadataBlock = (response: AxiosResponse): MetadataBlock => { + const metadataBlockPayload = response.data.data; + let metadataFields: Record = {}; + const metadataBlockFieldsPayload = metadataBlockPayload.fields; + Object.keys(metadataBlockFieldsPayload).map((metadataFieldKey) => { + const metadataFieldInfoPayload = metadataBlockFieldsPayload[metadataFieldKey]; + metadataFields[metadataFieldKey] = transformPayloadMetadataFieldInfo(metadataFieldInfoPayload); + }); + return { + id: metadataBlockPayload.id, + name: metadataBlockPayload.name, + displayName: metadataBlockPayload.displayName, + metadataFields: metadataFields, + }; +}; + +const transformPayloadMetadataFieldInfo = ( + metadataFieldInfoPayload: any, + isChild: boolean = false, +): MetadataFieldInfo => { + let metadataFieldInfo: MetadataFieldInfo = { + name: metadataFieldInfoPayload.name, + displayName: metadataFieldInfoPayload.displayName, + title: metadataFieldInfoPayload.title, + type: metadataFieldInfoPayload.type, + watermark: metadataFieldInfoPayload.watermark, + description: metadataFieldInfoPayload.description, + multiple: metadataFieldInfoPayload.multiple, + isControlledVocabulary: metadataFieldInfoPayload.isControlledVocabulary, + displayFormat: metadataFieldInfoPayload.displayFormat, + }; + if (!isChild && metadataFieldInfoPayload.hasOwnProperty('childFields')) { + const childMetadataFieldsPayload = metadataFieldInfoPayload.childFields; + let childMetadataFields: Record = {}; + Object.keys(childMetadataFieldsPayload).map((metadataFieldKey) => { + childMetadataFields[metadataFieldKey] = transformPayloadMetadataFieldInfo( + childMetadataFieldsPayload[metadataFieldKey], + true, + ); + }); + metadataFieldInfo.childMetadataFields = childMetadataFields; + } + return metadataFieldInfo; +}; diff --git a/test/testHelpers/metadataBlocks/metadataBlockHelper.ts b/test/testHelpers/metadataBlocks/metadataBlockHelper.ts new file mode 100644 index 00000000..54b12cfe --- /dev/null +++ b/test/testHelpers/metadataBlocks/metadataBlockHelper.ts @@ -0,0 +1,113 @@ +import { MetadataBlock } from '../../../src/metadataBlocks/domain/models/MetadataBlock'; + +export const createMetadataBlockModel = (): MetadataBlock => { + return { + id: 1, + name: 'testName', + displayName: 'testDisplayName', + metadataFields: { + testField1: { + name: 'testName1', + displayName: 'testDisplayName1', + title: 'testTitle1', + type: 'testType1', + watermark: 'testWatermark1', + description: 'testDescription1', + multiple: false, + isControlledVocabulary: false, + displayFormat: '#VALUE', + }, + testField2: { + name: 'testName2', + displayName: 'testDisplayName2', + title: 'testTitle2', + type: 'testType2', + watermark: 'testWatermark2', + description: 'testDescription2', + multiple: true, + isControlledVocabulary: false, + displayFormat: '', + childMetadataFields: { + testField3: { + name: 'testName3', + displayName: 'testDisplayName3', + title: 'testTitle3', + type: 'testType3', + watermark: 'testWatermark3', + description: 'testDescription3', + multiple: false, + isControlledVocabulary: false, + displayFormat: '#VALUE', + }, + testField4: { + name: 'testName4', + displayName: 'testDisplayName4', + title: 'testTitle4', + type: 'testType4', + watermark: 'testWatermark4', + description: 'testDescription4', + multiple: false, + isControlledVocabulary: false, + displayFormat: '#VALUE', + }, + }, + }, + }, + }; +}; + +export const createMetadataBlockPayload = (): any => { + return { + id: 1, + name: 'testName', + displayName: 'testDisplayName', + fields: { + testField1: { + name: 'testName1', + displayName: 'testDisplayName1', + title: 'testTitle1', + type: 'testType1', + watermark: 'testWatermark1', + description: 'testDescription1', + multiple: false, + isControlledVocabulary: false, + displayFormat: '#VALUE', + }, + testField2: { + name: 'testName2', + displayName: 'testDisplayName2', + title: 'testTitle2', + type: 'testType2', + watermark: 'testWatermark2', + description: 'testDescription2', + multiple: true, + isControlledVocabulary: false, + displayFormat: '', + childFields: { + testField3: { + name: 'testName3', + displayName: 'testDisplayName3', + title: 'testTitle3', + type: 'testType3', + watermark: 'testWatermark3', + description: 'testDescription3', + multiple: false, + isControlledVocabulary: false, + displayFormat: '#VALUE', + }, + testField4: { + name: 'testName4', + displayName: 'testDisplayName4', + title: 'testTitle4', + type: 'testType4', + watermark: 'testWatermark4', + description: 'testDescription4', + multiple: false, + isControlledVocabulary: false, + displayFormat: '#VALUE', + }, + }, + }, + }, + }; +}; diff --git a/test/unit/metadataBlocks/GetMetadataBlockByName.test.ts b/test/unit/metadataBlocks/GetMetadataBlockByName.test.ts new file mode 100644 index 00000000..df922ac6 --- /dev/null +++ b/test/unit/metadataBlocks/GetMetadataBlockByName.test.ts @@ -0,0 +1,39 @@ +import { GetMetadataBlockByName } from '../../../src/metadataBlocks/domain/useCases/GetMetadataBlockByName'; +import { IMetadataBlocksRepository } from '../../../src/metadataBlocks/domain/repositories/IMetadataBlocksRepository'; +import { ReadError } from '../../../src/core/domain/repositories/ReadError'; +import { assert, createSandbox, SinonSandbox } from 'sinon'; +import { createMetadataBlockModel } from '../../testHelpers/metadataBlocks/metadataBlockHelper'; + +describe('execute', () => { + const sandbox: SinonSandbox = createSandbox(); + const testMetadataBlockName = 'test'; + + afterEach(() => { + sandbox.restore(); + }); + + test('should return metadata block on repository success', async () => { + const testMetadataBlock = createMetadataBlockModel(); + const metadataBlocksRepositoryStub = {}; + const getMetadataBlockByNameStub = sandbox.stub().returns(testMetadataBlock); + metadataBlocksRepositoryStub.getMetadataBlockByName = getMetadataBlockByNameStub; + const sut = new GetMetadataBlockByName(metadataBlocksRepositoryStub); + + const actual = await sut.execute(testMetadataBlockName); + + assert.match(actual, testMetadataBlock); + assert.calledWithExactly(getMetadataBlockByNameStub, testMetadataBlockName); + }); + + test('should return error result on repository error', async () => { + const metadataBlocksRepositoryStub = {}; + const testReadError = new ReadError(); + metadataBlocksRepositoryStub.getMetadataBlockByName = sandbox.stub().throwsException(testReadError); + const sut = new GetMetadataBlockByName(metadataBlocksRepositoryStub); + + let actualError: ReadError = undefined; + await sut.execute(testMetadataBlockName).catch((e) => (actualError = e)); + + assert.match(actualError, testReadError); + }); +}); diff --git a/test/unit/metadataBlocks/MetadataBlocksRepository.test.ts b/test/unit/metadataBlocks/MetadataBlocksRepository.test.ts new file mode 100644 index 00000000..9516c454 --- /dev/null +++ b/test/unit/metadataBlocks/MetadataBlocksRepository.test.ts @@ -0,0 +1,58 @@ +import { MetadataBlocksRepository } from '../../../src/metadataBlocks/infra/repositories/MetadataBlocksRepository'; +import { assert, createSandbox, SinonSandbox } from 'sinon'; +import axios from 'axios'; +import { expect } from 'chai'; +import { ReadError } from '../../../src/core/domain/repositories/ReadError'; +import { ApiConfig } from '../../../src/core/infra/repositories/ApiConfig'; +import { + createMetadataBlockModel, + createMetadataBlockPayload, +} from '../../testHelpers/metadataBlocks/metadataBlockHelper'; + +describe('getMetadataBlockByName', () => { + const sandbox: SinonSandbox = createSandbox(); + const sut: MetadataBlocksRepository = new MetadataBlocksRepository(); + const testApiUrl = 'https://test.dataverse.org/api/v1'; + const testMetadataBlockName = 'test'; + + ApiConfig.init(testApiUrl); + + afterEach(() => { + sandbox.restore(); + }); + + test('should return metadata block on successful response', async () => { + const testSuccessfulResponse = { + data: { + status: 'OK', + data: createMetadataBlockPayload(), + }, + }; + const axiosGetStub = sandbox.stub(axios, 'get').resolves(testSuccessfulResponse); + + const actual = await sut.getMetadataBlockByName(testMetadataBlockName); + + assert.calledWithExactly(axiosGetStub, `${testApiUrl}/metadatablocks/${testMetadataBlockName}`, { + withCredentials: false, + }); + assert.match(actual, createMetadataBlockModel()); + }); + + test('should return error result on error response', async () => { + const testErrorResponse = { + response: { + status: 'ERROR', + message: 'test', + }, + }; + const axiosGetStub = sandbox.stub(axios, 'get').rejects(testErrorResponse); + + let error: ReadError = undefined; + await sut.getMetadataBlockByName(testMetadataBlockName).catch((e) => (error = e)); + + assert.calledWithExactly(axiosGetStub, `${testApiUrl}/metadatablocks/${testMetadataBlockName}`, { + withCredentials: false, + }); + expect(error).to.be.instanceOf(Error); + }); +});