diff --git a/src/common/utils/deep-mapper.util.ts b/src/common/utils/deep-mapper.util.ts index 9c36ae381..6038458ce 100644 --- a/src/common/utils/deep-mapper.util.ts +++ b/src/common/utils/deep-mapper.util.ts @@ -1,6 +1,10 @@ -import { get, trim } from "lodash"; +import { get, merge, set, trim } from "lodash"; -function mapDeep( +type MappingFn = (source: S) => unknown; + +type FieldsMap = Partial>>; + +function getDeep( source: T, key: keyof U & string, fieldsMap: Partial>, @@ -22,6 +26,43 @@ export function createDeepMapper( fieldsMap: Partial>, ) { return (source: T, key: string) => { - return mapDeep(source, key as keyof U & string, fieldsMap); + return getDeep(source, key as keyof U & string, fieldsMap); }; } + +function setDeep( + source: S, + key: keyof S & string, + fieldsMap: FieldsMap, +): Record { + if (!source) return {}; + const instruction = fieldsMap[key]; + if (typeof instruction === "function") return { [key]: instruction(source) }; + const path = (instruction as string) || key; + const value = get(source, key); + if (value === undefined) return {}; + const fragment = {}; + if (!path.includes("[]")) return set(fragment, path, value); + const [arrayRootPath, rawLeafPath] = path.split("[]"); + const leafPath = trim(rawLeafPath, "."); + const arrayResult: Record[] = []; + + if (Array.isArray(value)) + value.forEach((itemValue, index) => { + arrayResult[index] = {}; + set(arrayResult[index], leafPath, itemValue); + }); + else { + arrayResult[0] = {}; + set(arrayResult[0], leafPath, value); + } + return set(fragment, trim(arrayRootPath, "."), arrayResult); +} + +export function createDeepSetter(fieldsMap: FieldsMap) { + return (source: S): T => + Object.keys(source as keyof S).reduce((acc, key) => { + const fragment = setDeep(source, key as keyof S & string, fieldsMap); + return merge(acc, fragment); + }, {}) as T; +} diff --git a/src/published-data/dto/published-data.obsolete.dto.ts b/src/published-data/dto/published-data.obsolete.dto.ts index 3b26342c4..91969752f 100644 --- a/src/published-data/dto/published-data.obsolete.dto.ts +++ b/src/published-data/dto/published-data.obsolete.dto.ts @@ -31,7 +31,7 @@ export const publishedDataV3toV4FieldMap: Partial< downloadLink: "metadata.downloadLink", }; -const mapPublishedDataV3toV4Field = createDeepMapper< +export const mapPublishedDataV3toV4Field = createDeepMapper< PublishedData, PublishedDataObsoleteDto >(publishedDataV3toV4FieldMap); diff --git a/src/published-data/pipes/body-dto.pipe.ts b/src/published-data/pipes/body-dto.pipe.ts new file mode 100644 index 000000000..fd3559d8a --- /dev/null +++ b/src/published-data/pipes/body-dto.pipe.ts @@ -0,0 +1,28 @@ +import { Injectable, PipeTransform } from "@nestjs/common"; +import { createDeepSetter } from "src/common/utils/deep-mapper.util"; +import { CreatePublishedDataDto } from "../dto/create-published-data.dto"; +import { CreatePublishedDataV4Dto } from "../dto/create-published-data.v4.dto"; +import { publishedDataV3toV4FieldMap } from "../dto/published-data.obsolete.dto"; +import { PublishedDataStatus } from "../interfaces/published-data.interface"; + +const dtoV3toV4 = createDeepSetter< + CreatePublishedDataDto, + CreatePublishedDataV4Dto +>({ + ...publishedDataV3toV4FieldMap, + status: (src: CreatePublishedDataDto): PublishedDataStatus => + src.status === "registered" + ? PublishedDataStatus.REGISTERED + : PublishedDataStatus.PRIVATE, +}); + +@Injectable() +export class V3ToV4MigrationPipe implements PipeTransform { + constructor(private readonly mapper: (source: S) => T) {} + + transform(value: S): T { + return this.mapper(value); + } +} + +export const V3_TO_V4_DTO_BODY_PIPE = new V3ToV4MigrationPipe(dtoV3toV4); diff --git a/src/published-data/published-data.controller.spec.ts b/src/published-data/published-data.controller.spec.ts index b17ec39b3..fbe2c4889 100644 --- a/src/published-data/published-data.controller.spec.ts +++ b/src/published-data/published-data.controller.spec.ts @@ -5,7 +5,6 @@ import { AttachmentsService } from "src/attachments/attachments.service"; import { CaslAbilityFactory } from "src/casl/casl-ability.factory"; import { DatasetsService } from "src/datasets/datasets.service"; import { ProposalsService } from "src/proposals/proposals.service"; -import { PublishedDataStatus } from "./interfaces/published-data.interface"; import { PublishedDataController } from "./published-data.controller"; import { PublishedDataService } from "./published-data.service"; @@ -44,20 +43,4 @@ describe("PublishedDataController", () => { it("should be defined", () => { expect(controller).toBeDefined(); }); - - it("should result in PublishedDataStatus.REGISTERED", () => { - expect(controller.convertObsoleteStatusToCurrent("registered")).toEqual( - PublishedDataStatus.REGISTERED, - ); - }); - it("should result in PublishedDataStatus.PRIVATE", () => { - expect( - controller.convertObsoleteStatusToCurrent("pending_registration"), - ).toEqual(PublishedDataStatus.PRIVATE); - }); - it("should result in PublishedDataStatus.PRIVATE", () => { - expect(controller.convertObsoleteStatusToCurrent("invalid_status")).toEqual( - PublishedDataStatus.PRIVATE, - ); - }); }); diff --git a/src/published-data/published-data.controller.ts b/src/published-data/published-data.controller.ts index a06d3481d..413263406 100644 --- a/src/published-data/published-data.controller.ts +++ b/src/published-data/published-data.controller.ts @@ -8,7 +8,6 @@ import { Get, HttpException, HttpStatus, - Logger, NotFoundException, Param, Patch, @@ -48,16 +47,12 @@ import { PartialUpdatePublishedDataDto, UpdatePublishedDataDto, } from "./dto/update-published-data.dto"; -import { - PartialUpdatePublishedDataV4Dto, - UpdatePublishedDataV4Dto, -} from "./dto/update-published-data.v4.dto"; +import { UpdatePublishedDataV4Dto } from "./dto/update-published-data.v4.dto"; import { FormPopulateData, ICount, IPublishedDataFilters, IRegister, - PublishedDataStatus, } from "./interfaces/published-data.interface"; import { IdToDoiPipe, @@ -68,6 +63,7 @@ import { PublishedDataService } from "./published-data.service"; import { PublishedData } from "./schemas/published-data.schema"; import { V3_FILTER_PIPE } from "./pipes/filter.pipe"; import { Filter } from "src/datasets/decorators/filter.decorator"; +import { V3_TO_V4_DTO_BODY_PIPE } from "./pipes/body-dto.pipe"; @ApiBearerAuth() @ApiTags("published data") @@ -83,152 +79,6 @@ export class PublishedDataController { private readonly publishedDataService: PublishedDataService, ) {} - convertObsoleteStatusToCurrent(obsoleteStatus: string): PublishedDataStatus { - switch (obsoleteStatus) { - case "registered": - return PublishedDataStatus.REGISTERED; - case "pending_registration": - return PublishedDataStatus.PRIVATE; - default: - Logger.error( - `Unknown PublishedData.status '${obsoleteStatus}' defaulting to PublishedDataStatus.PRIVATE`, - ); - return PublishedDataStatus.PRIVATE; - } - } - - convertObsoleteToCurrentSchema( - inputObsoletePublishedData: - | CreatePublishedDataDto - | UpdatePublishedDataDto - | PartialUpdatePublishedDataDto, - ): - | CreatePublishedDataV4Dto - | UpdatePublishedDataV4Dto - | PartialUpdatePublishedDataV4Dto { - const propertiesModifier: Record = { - metadata: {}, - title: inputObsoletePublishedData.title, - abstract: inputObsoletePublishedData.abstract, - datasetPids: inputObsoletePublishedData.pidArray, - }; - - if ("affiliation" in inputObsoletePublishedData) { - propertiesModifier.metadata.affiliation = - inputObsoletePublishedData.affiliation; - } - - if ("publisher" in inputObsoletePublishedData) { - propertiesModifier.metadata.publisher = { - name: inputObsoletePublishedData.publisher, - }; - } - - if ("publicationYear" in inputObsoletePublishedData) { - propertiesModifier.metadata.publicationYear = - inputObsoletePublishedData.publicationYear; - } - - if ("creator" in inputObsoletePublishedData) { - propertiesModifier.metadata.creators = - inputObsoletePublishedData.creator?.map((creator) => ({ - name: creator.trim(), - affiliation: [ - { name: inputObsoletePublishedData.affiliation?.trim() || "" }, - ], - })); - } - - if ("dataDescription" in inputObsoletePublishedData) { - propertiesModifier.metadata.dataDescription = - inputObsoletePublishedData.dataDescription; - } - - if ("resourceType" in inputObsoletePublishedData) { - propertiesModifier.metadata.resourceType = - inputObsoletePublishedData.resourceType; - } - - if ("numberOfFiles" in inputObsoletePublishedData) { - propertiesModifier.metadata.numberOfFiles = - inputObsoletePublishedData.numberOfFiles; - } - - if ("sizeOfArchive" in inputObsoletePublishedData) { - propertiesModifier.metadata.sizeOfArchive = - inputObsoletePublishedData.sizeOfArchive; - } - - if ("url" in inputObsoletePublishedData) { - propertiesModifier.metadata.url = inputObsoletePublishedData.url; - } - - if ("thumbnail" in inputObsoletePublishedData) { - propertiesModifier.metadata.thumbnail = - inputObsoletePublishedData.thumbnail; - } - - if ("scicatUser" in inputObsoletePublishedData) { - propertiesModifier.metadata.scicatUser = - inputObsoletePublishedData.scicatUser; - } - - if ("downloadLink" in inputObsoletePublishedData) { - propertiesModifier.metadata.downloadLink = - inputObsoletePublishedData.downloadLink; - } - - if ("authors" in inputObsoletePublishedData) { - propertiesModifier.metadata.contributors = - inputObsoletePublishedData.authors?.map((author) => ({ - name: author.trim(), - })); - } - - if ("relatedPublications" in inputObsoletePublishedData) { - propertiesModifier.metadata.relatedIdentifiers = - inputObsoletePublishedData.relatedPublications?.map((publication) => ({ - relatedIdentifier: publication, - })); - } - - if ("pidArray" in inputObsoletePublishedData) { - propertiesModifier.datasetPids = inputObsoletePublishedData.pidArray; - } - - if ( - "status" in inputObsoletePublishedData && - typeof inputObsoletePublishedData.status === "string" - ) { - propertiesModifier.status = this.convertObsoleteStatusToCurrent( - inputObsoletePublishedData.status, - ); - } - - let outputPublishedData: - | CreatePublishedDataV4Dto - | UpdatePublishedDataV4Dto - | PartialUpdatePublishedDataV4Dto = {}; - - if (inputObsoletePublishedData instanceof CreatePublishedDataDto) { - outputPublishedData = { - ...propertiesModifier, - } as CreatePublishedDataV4Dto; - } else if (inputObsoletePublishedData instanceof UpdatePublishedDataDto) { - outputPublishedData = { - ...propertiesModifier, - } as UpdatePublishedDataV4Dto; - } else if ( - inputObsoletePublishedData instanceof PartialUpdatePublishedDataDto - ) { - outputPublishedData = { - ...propertiesModifier, - } as PartialUpdatePublishedDataV4Dto; - } - - return outputPublishedData; - } - // POST /publisheddata @UseGuards(PoliciesGuard) @CheckPolicies("publisheddata", (ability: AppAbility) => @@ -245,14 +95,12 @@ export class PublishedDataController { }) @Post() async create( - @Body() createPublishedDataDto: CreatePublishedDataDto, + @Body(V3_TO_V4_DTO_BODY_PIPE) + createPublishedDataDto: CreatePublishedDataDto, ): Promise { - const publishedDataDto = this.convertObsoleteToCurrentSchema( - createPublishedDataDto, - ) as CreatePublishedDataV4Dto; - - const createdPublishedData = - await this.publishedDataService.create(publishedDataDto); + const createdPublishedData = await this.publishedDataService.create( + createPublishedDataDto as unknown as CreatePublishedDataV4Dto, + ); return createdPublishedData as unknown as PublishedDataObsoleteDto; } @@ -456,14 +304,12 @@ export class PublishedDataController { @Patch("/:id") async update( @Param("id") id: string, - @Body() updatePublishedDataDto: PartialUpdatePublishedDataDto, + @Body(V3_TO_V4_DTO_BODY_PIPE) + updatePublishedDataDto: PartialUpdatePublishedDataDto, ): Promise { - const updateData = this.convertObsoleteToCurrentSchema( - updatePublishedDataDto, - ); const updatedData = await this.publishedDataService.update( { doi: id }, - updateData, + updatePublishedDataDto as unknown as PublishedData, ); return updatedData as unknown as PublishedDataObsoleteDto; @@ -721,27 +567,22 @@ export class PublishedDataController { @Post("/:id/resync") async resync( @Param("id") id: string, - @Body() data: UpdatePublishedDataDto, + @Body(V3_TO_V4_DTO_BODY_PIPE) + data: UpdatePublishedDataDto, ): Promise { - const { ...obsolettePublishedData } = data; - - const publishedData = this.convertObsoleteToCurrentSchema( - obsolettePublishedData, - ); - const OAIServerUri = this.configService.get("oaiProviderRoute"); let returnValue = null; if (OAIServerUri) { returnValue = await this.publishedDataService.resyncOAIPublication( id, - publishedData as UpdatePublishedDataV4Dto, + data as unknown as UpdatePublishedDataV4Dto, OAIServerUri, ); } try { - await this.publishedDataService.update({ doi: id }, publishedData); + await this.publishedDataService.update({ doi: id }, data); } catch (error: any) { throw new HttpException( `Error occurred: ${error}`,