From 300a336bf0bbfe2c8ba3cca4a5693bee2e601034 Mon Sep 17 00:00:00 2001 From: Chukwudumebi Onwuli <37223065+deeonwuli@users.noreply.github.com> Date: Fri, 13 Mar 2026 21:38:31 +0100 Subject: [PATCH 01/10] fix: ensure each compulsory data element operand in a DataSet has a coc (default if missing) --- src/data/metadata/MetadataD2ApiRepository.ts | 17 +++++--- .../builders/MetadataPayloadBuilder.ts | 42 +++++++++++++++++++ .../repositories/MetadataRepository.ts | 8 ++-- 3 files changed, 57 insertions(+), 10 deletions(-) diff --git a/src/data/metadata/MetadataD2ApiRepository.ts b/src/data/metadata/MetadataD2ApiRepository.ts index f4ed246eb..4d715662a 100644 --- a/src/data/metadata/MetadataD2ApiRepository.ts +++ b/src/data/metadata/MetadataD2ApiRepository.ts @@ -208,9 +208,9 @@ export class MetadataD2ApiRepository implements MetadataRepository { } @cache() - public async getCategoryOptionCombos(): Promise< - Pick[] - > { + public async getCategoryOptionCombos( + filter?: FilterBase + ): Promise[]> { const { objects } = await this.api.models.categoryOptionCombos .get({ paging: false, @@ -220,7 +220,7 @@ export class MetadataD2ApiRepository implements MetadataRepository { categoryCombo: true, categoryOptions: true, }, - ...this.getAdditionalCategoryOptionCombosOptionsByVariant(), + ...this.getAdditionalCategoryOptionCombosOptionsByVariant(filter), }) .getData(); @@ -634,7 +634,9 @@ export class MetadataD2ApiRepository implements MetadataRepository { return this.api.models[type]; } - private getAdditionalCategoryOptionCombosOptionsByVariant(): CategoryOptionCombosAdditionalOptions { + private getAdditionalCategoryOptionCombosOptionsByVariant( + filter: FilterBase = {} + ): CategoryOptionCombosAdditionalOptions { const appVariant = config.appPresentationVariant; switch (appVariant) { @@ -646,13 +648,16 @@ export class MetadataD2ApiRepository implements MetadataRepository { // we filter them out. return { filter: { + ...filter, "categoryCombo.code": [{ null: true }, { "!like": "RVC_" }], }, rootJunction: "OR", }; } default: - return {}; + return { + filter: filter, + }; } } } diff --git a/src/domain/metadata/builders/MetadataPayloadBuilder.ts b/src/domain/metadata/builders/MetadataPayloadBuilder.ts index 7ee4fc5d0..c9a03eb9b 100644 --- a/src/domain/metadata/builders/MetadataPayloadBuilder.ts +++ b/src/domain/metadata/builders/MetadataPayloadBuilder.ts @@ -3,6 +3,7 @@ import { Instance } from "../../instance/entities/Instance"; import { SynchronizationBuilder } from "../../synchronization/entities/SynchronizationBuilder"; import { Dashboard, + DataSet, EventVisualization, MetadataEntities, MetadataEntity, @@ -117,6 +118,7 @@ export class MetadataPayloadBuilder { ); const { + dataSets, organisationUnits, users, userGroups, @@ -129,6 +131,10 @@ export class MetadataPayloadBuilder { ...rest } = metadataWithoutDuplicates; + const fixedDataSets = dataSets + ? await this.fixDataSetsCompulsoryDEOperandCatCombo(originInstance, dataSets as DataSet[]) + : []; + const visualizationsWithRows = visualizations ? await this.addRowsToVisualizations(originInstance, visualizations as Visualization[]) : []; @@ -151,6 +157,7 @@ export class MetadataPayloadBuilder { users: includeUsersObjectsAndReferences ? users : undefined, userGroups: includeSharingSettingsObjectsAndReferences ? userGroups : undefined, userRoles: includeSharingSettingsObjectsAndReferences ? userRoles : undefined, + dataSets: fixedDataSets, ...rest, }; @@ -379,6 +386,41 @@ export class MetadataPayloadBuilder { }; } + private async fixDataSetsCompulsoryDEOperandCatCombo( + originInstance: Instance, + dataSets: DataSet[] + ): Promise { + const metadataRepository = this.repositoryFactory.metadataRepository(originInstance); + const categoryOptionCombos = await metadataRepository.getCategoryOptionCombos({ + code: { eq: "default" }, + }); + + const defaultCategoryOptionCombo = categoryOptionCombos[0]; + if (!defaultCategoryOptionCombo) { + console.error( + "Default category option combo not found, unable to fix data sets compulsory data element operands category option combo" + ); + return dataSets; + } + + return dataSets.map(dataSet => { + if (!dataSet.compulsoryDataElementOperands) return dataSet; + + const fixedCompulsoryDEOperands = dataSet.compulsoryDataElementOperands.map(operand => { + if (operand.categoryOptionCombo?.id) return operand; + + return defaultCategoryOptionCombo + ? { + ...operand, + categoryOptionCombo: { ...operand.categoryOptionCombo, id: defaultCategoryOptionCombo.id }, + } + : operand; + }); + + return { ...dataSet, compulsoryDataElementOperands: fixedCompulsoryDEOperands }; + }); + } + private async addRowsToVisualizations( originInstance: Instance, visualizations: Visualization[] diff --git a/src/domain/metadata/repositories/MetadataRepository.ts b/src/domain/metadata/repositories/MetadataRepository.ts index f8da4cdb6..dd106e473 100644 --- a/src/domain/metadata/repositories/MetadataRepository.ts +++ b/src/domain/metadata/repositories/MetadataRepository.ts @@ -1,4 +1,4 @@ -import { FilterValueOperator } from "@eyeseetea/d2-api/api/common"; +import { FilterBase, FilterValueOperator } from "@eyeseetea/d2-api/api/common"; import { IdentifiableRef, Ref } from "../../common/entities/Ref"; import { Id } from "../../common/entities/Schemas"; import { SynchronizationResult } from "../../reports/entities/SynchronizationResult"; @@ -10,9 +10,9 @@ export interface MetadataRepository { getMetadataByIds(ids: Id[], fields?: object | string, includeDefaults?: boolean): Promise>; getByFilterRules(filterRules: FilterRule[]): Promise; getDefaultIds(filter?: string): Promise; - getCategoryOptionCombos(): Promise< - Pick[] - >; + getCategoryOptionCombos( + filter?: FilterBase + ): Promise[]>; listMetadata(params: ListMetadataParams): Promise; listAllMetadata(params: ListMetadataParams): Promise; From 267baa5b101e86bccddd0f04ad7945575d8647e8 Mon Sep 17 00:00:00 2001 From: Chukwudumebi Onwuli <37223065+deeonwuli@users.noreply.github.com> Date: Fri, 13 Mar 2026 22:00:17 +0100 Subject: [PATCH 02/10] fix: adjust handling of fixed data sets and add missing category option combo mocks in tests --- i18n/en.pot | 4 ++-- src/domain/metadata/builders/MetadataPayloadBuilder.ts | 4 ++-- .../builders/__tests__/MetadataPayloadBuilder.spec.tsx | 3 +++ 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index a3742f9d1..ade08cd82 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2025-11-11T12:44:20.444Z\n" -"PO-Revision-Date: 2025-11-11T12:44:20.444Z\n" +"POT-Creation-Date: 2026-03-13T20:43:09.878Z\n" +"PO-Revision-Date: 2026-03-13T20:43:09.878Z\n" msgid "" "THIS NEW RELEASE INCLUDES SHARING SETTINGS PER INSTANCES. FOR THIS VERSION " diff --git a/src/domain/metadata/builders/MetadataPayloadBuilder.ts b/src/domain/metadata/builders/MetadataPayloadBuilder.ts index c9a03eb9b..ebd515b0e 100644 --- a/src/domain/metadata/builders/MetadataPayloadBuilder.ts +++ b/src/domain/metadata/builders/MetadataPayloadBuilder.ts @@ -133,7 +133,7 @@ export class MetadataPayloadBuilder { const fixedDataSets = dataSets ? await this.fixDataSetsCompulsoryDEOperandCatCombo(originInstance, dataSets as DataSet[]) - : []; + : undefined; const visualizationsWithRows = visualizations ? await this.addRowsToVisualizations(originInstance, visualizations as Visualization[]) @@ -157,7 +157,7 @@ export class MetadataPayloadBuilder { users: includeUsersObjectsAndReferences ? users : undefined, userGroups: includeSharingSettingsObjectsAndReferences ? userGroups : undefined, userRoles: includeSharingSettingsObjectsAndReferences ? userRoles : undefined, - dataSets: fixedDataSets, + ...(fixedDataSets && { dataSets: fixedDataSets }), ...rest, }; diff --git a/src/domain/metadata/builders/__tests__/MetadataPayloadBuilder.spec.tsx b/src/domain/metadata/builders/__tests__/MetadataPayloadBuilder.spec.tsx index b0e90d8c9..e53a4a518 100644 --- a/src/domain/metadata/builders/__tests__/MetadataPayloadBuilder.spec.tsx +++ b/src/domain/metadata/builders/__tests__/MetadataPayloadBuilder.spec.tsx @@ -139,6 +139,7 @@ describe("MetadataPayloadBuilder", () => { } when(mockedMetadataRepository.listAllMetadata(anything())).thenResolve([]); + when(mockedMetadataRepository.getCategoryOptionCombos(anything())).thenResolve([]); const mockedRepositoryFactory = mock(); when(mockedRepositoryFactory.instanceRepository(anything())).thenReturn(instance(mockedInstanceRepository)); @@ -258,6 +259,7 @@ describe("MetadataPayloadBuilder", () => { } when(mockedMetadataRepository.listAllMetadata(anything())).thenResolve([]); + when(mockedMetadataRepository.getCategoryOptionCombos(anything())).thenResolve([]); const mockedRepositoryFactory = mock(); when(mockedRepositoryFactory.instanceRepository(anything())).thenReturn(instance(mockedInstanceRepository)); @@ -374,6 +376,7 @@ describe("MetadataPayloadBuilder", () => { } when(mockedMetadataRepository.listAllMetadata(anything())).thenResolve([]); + when(mockedMetadataRepository.getCategoryOptionCombos(anything())).thenResolve([]); const mockedRepositoryFactory = mock(); when(mockedRepositoryFactory.instanceRepository(anything())).thenReturn(instance(mockedInstanceRepository)); From 8509950767f949a628b506c6186ced341591b0b5 Mon Sep 17 00:00:00 2001 From: Chukwudumebi Onwuli <37223065+deeonwuli@users.noreply.github.com> Date: Wed, 15 Apr 2026 20:53:28 +0100 Subject: [PATCH 03/10] fix: remove ternary operator since defaultCategoryOptionCombo is returned early if undefined --- src/domain/metadata/builders/MetadataPayloadBuilder.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/domain/metadata/builders/MetadataPayloadBuilder.ts b/src/domain/metadata/builders/MetadataPayloadBuilder.ts index ebd515b0e..008544012 100644 --- a/src/domain/metadata/builders/MetadataPayloadBuilder.ts +++ b/src/domain/metadata/builders/MetadataPayloadBuilder.ts @@ -409,12 +409,10 @@ export class MetadataPayloadBuilder { const fixedCompulsoryDEOperands = dataSet.compulsoryDataElementOperands.map(operand => { if (operand.categoryOptionCombo?.id) return operand; - return defaultCategoryOptionCombo - ? { - ...operand, - categoryOptionCombo: { ...operand.categoryOptionCombo, id: defaultCategoryOptionCombo.id }, - } - : operand; + return { + ...operand, + categoryOptionCombo: { ...operand.categoryOptionCombo, id: defaultCategoryOptionCombo.id }, + }; }); return { ...dataSet, compulsoryDataElementOperands: fixedCompulsoryDEOperands }; From 30a345bd3aaa7abd3b43529ad1c2ab2a266e3c69 Mon Sep 17 00:00:00 2001 From: Chukwudumebi Onwuli <37223065+deeonwuli@users.noreply.github.com> Date: Wed, 15 Apr 2026 21:22:34 +0100 Subject: [PATCH 04/10] fix: update condition for compulsory data element operands to handle missing category option combo --- src/domain/metadata/builders/MetadataPayloadBuilder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/metadata/builders/MetadataPayloadBuilder.ts b/src/domain/metadata/builders/MetadataPayloadBuilder.ts index 008544012..d998cd99d 100644 --- a/src/domain/metadata/builders/MetadataPayloadBuilder.ts +++ b/src/domain/metadata/builders/MetadataPayloadBuilder.ts @@ -407,7 +407,7 @@ export class MetadataPayloadBuilder { if (!dataSet.compulsoryDataElementOperands) return dataSet; const fixedCompulsoryDEOperands = dataSet.compulsoryDataElementOperands.map(operand => { - if (operand.categoryOptionCombo?.id) return operand; + if (!operand.categoryOptionCombo || operand.categoryOptionCombo?.id) return operand; return { ...operand, From e97b132dd7de7588803bd0f7e42a43f328585e55 Mon Sep 17 00:00:00 2001 From: Chukwudumebi Onwuli <37223065+deeonwuli@users.noreply.github.com> Date: Wed, 15 Apr 2026 22:05:32 +0100 Subject: [PATCH 05/10] fix: add test to replace missing categoryOptionCombo.id on compulsory data element operands with default coc id --- .../__tests__/MetadataPayloadBuilder.spec.tsx | 57 ++++++++++++++++--- 1 file changed, 50 insertions(+), 7 deletions(-) diff --git a/src/domain/metadata/builders/__tests__/MetadataPayloadBuilder.spec.tsx b/src/domain/metadata/builders/__tests__/MetadataPayloadBuilder.spec.tsx index e53a4a518..928c5ebe2 100644 --- a/src/domain/metadata/builders/__tests__/MetadataPayloadBuilder.spec.tsx +++ b/src/domain/metadata/builders/__tests__/MetadataPayloadBuilder.spec.tsx @@ -29,6 +29,7 @@ import { getDataSetTypeExpectedPayload, } from "./data/data-set-metadata-type"; import { MetadataPayloadBuilder } from "../MetadataPayloadBuilder"; +import { DataSet } from "../../entities/MetadataEntities"; import { givenABuilderWithUserGroupsAndDashboards, givenUserGroupsAndDashboardMetadataResponses, @@ -329,11 +330,51 @@ describe("MetadataPayloadBuilder", () => { expect(payload).toEqual(expectedPayload); }); - function givenMetadataPayloadBuilderOfDataSet(options: { - includeObjectsAndReferences: boolean; - includeOnlyReferences: boolean; - }): MetadataPayloadBuilder { + it("should replace missing categoryOptionCombo.id on compulsory data element operands with the default coc id", async () => { + const defaultCocId = "DEFAULT_COC_ID"; + const dataSetWithOperand = { + ...getDataSetMetadata(), + compulsoryDataElementOperands: [{ dataElement: { id: "BDuY694ZAFa" }, categoryOptionCombo: {} }], + } as unknown as DataSet; + + const builder = givenABuilderWithProgramType({ + includeObjectsAndReferences: false, + includeOnlyReferences: false, + }); + + const metadataPayloadBuilder = givenMetadataPayloadBuilderOfDataSet( + { includeObjectsAndReferences: false, includeOnlyReferences: false }, + { + dataSet: dataSetWithOperand, + categoryOptionCombos: [ + { + id: defaultCocId, + name: "default", + categoryCombo: { id: "bjDvmb4bfuf" }, + categoryOptions: [], + }, + ], + } + ); + + const payload: SynchronizationPayload = await metadataPayloadBuilder.build(builder); + + const operands = (payload.dataSets?.[0] as DataSet | undefined)?.compulsoryDataElementOperands; + expect(operands?.[0]?.categoryOptionCombo?.id).toBe(defaultCocId); + }); + + function givenMetadataPayloadBuilderOfDataSet( + options: { + includeObjectsAndReferences: boolean; + includeOnlyReferences: boolean; + }, + overrides?: { + dataSet?: DataSet; + categoryOptionCombos?: Awaited>; + } + ): MetadataPayloadBuilder { const { includeObjectsAndReferences } = options; + const dataSet = overrides?.dataSet ?? getDataSetMetadata(); const mockedInstanceRepository = mock(); when(mockedInstanceRepository.getById(anything())).thenResolve(dummyInstance); @@ -365,9 +406,9 @@ describe("MetadataPayloadBuilder", () => { .thenResolve(metadataByIdsResponses.eighth); } else { when(mockedMetadataRepository.getMetadataByIds(anything())) - .thenResolve({ dataSets: [getDataSetMetadata()] }) + .thenResolve({ dataSets: [dataSet] }) .thenResolve({ - dataSets: [getDataSetMetadata()], + dataSets: [dataSet], dataElements: [getDataElementDataSetMetadata()], }) .thenResolve({ @@ -376,7 +417,9 @@ describe("MetadataPayloadBuilder", () => { } when(mockedMetadataRepository.listAllMetadata(anything())).thenResolve([]); - when(mockedMetadataRepository.getCategoryOptionCombos(anything())).thenResolve([]); + when(mockedMetadataRepository.getCategoryOptionCombos(anything())).thenResolve( + overrides?.categoryOptionCombos ?? [] + ); const mockedRepositoryFactory = mock(); when(mockedRepositoryFactory.instanceRepository(anything())).thenReturn(instance(mockedInstanceRepository)); From b13583e535b7ab8c890fc9863e9cf2eda9d53bfa Mon Sep 17 00:00:00 2001 From: Chukwudumebi Onwuli <37223065+deeonwuli@users.noreply.github.com> Date: Thu, 23 Apr 2026 15:19:25 +0100 Subject: [PATCH 06/10] feat: reverse changes that add default coc id to exported json --- i18n/en.pot | 4 +- src/data/metadata/MetadataD2ApiRepository.ts | 17 ++--- .../builders/MetadataPayloadBuilder.ts | 40 ------------ .../__tests__/MetadataPayloadBuilder.spec.tsx | 64 ++++--------------- .../repositories/MetadataRepository.ts | 8 +-- 5 files changed, 23 insertions(+), 110 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 0808ef9d6..ae226b2f2 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-04-09T10:14:37.106Z\n" -"PO-Revision-Date: 2026-04-09T10:14:37.106Z\n" +"POT-Creation-Date: 2026-04-23T11:28:44.991Z\n" +"PO-Revision-Date: 2026-04-23T11:28:44.992Z\n" msgid "" "THIS NEW RELEASE INCLUDES SHARING SETTINGS PER INSTANCES. FOR THIS VERSION " diff --git a/src/data/metadata/MetadataD2ApiRepository.ts b/src/data/metadata/MetadataD2ApiRepository.ts index b52800b56..c8f58c85e 100644 --- a/src/data/metadata/MetadataD2ApiRepository.ts +++ b/src/data/metadata/MetadataD2ApiRepository.ts @@ -213,9 +213,9 @@ export class MetadataD2ApiRepository implements MetadataRepository { } @cache() - public async getCategoryOptionCombos( - filter?: FilterBase - ): Promise[]> { + public async getCategoryOptionCombos(): Promise< + Pick[] + > { const { objects } = await this.api.models.categoryOptionCombos .get({ paging: false, @@ -225,7 +225,7 @@ export class MetadataD2ApiRepository implements MetadataRepository { categoryCombo: true, categoryOptions: true, }, - ...this.getAdditionalCategoryOptionCombosOptionsByVariant(filter), + ...this.getAdditionalCategoryOptionCombosOptionsByVariant(), }) .getData(); @@ -651,9 +651,7 @@ export class MetadataD2ApiRepository implements MetadataRepository { return this.api.models[type]; } - private getAdditionalCategoryOptionCombosOptionsByVariant( - filter: FilterBase = {} - ): CategoryOptionCombosAdditionalOptions { + private getAdditionalCategoryOptionCombosOptionsByVariant(): CategoryOptionCombosAdditionalOptions { const appVariant = config.appPresentationVariant; switch (appVariant) { @@ -665,16 +663,13 @@ export class MetadataD2ApiRepository implements MetadataRepository { // we filter them out. return { filter: { - ...filter, "categoryCombo.code": [{ null: true }, { "!like": "RVC_" }], }, rootJunction: "OR", }; } default: - return { - filter: filter, - }; + return {}; } } } diff --git a/src/domain/metadata/builders/MetadataPayloadBuilder.ts b/src/domain/metadata/builders/MetadataPayloadBuilder.ts index 9d381a00a..983907219 100644 --- a/src/domain/metadata/builders/MetadataPayloadBuilder.ts +++ b/src/domain/metadata/builders/MetadataPayloadBuilder.ts @@ -3,7 +3,6 @@ import { Instance } from "../../instance/entities/Instance"; import { SynchronizationBuilder } from "../../synchronization/entities/SynchronizationBuilder"; import { Dashboard, - DataSet, EventVisualization, MetadataEntities, MetadataEntity, @@ -118,7 +117,6 @@ export class MetadataPayloadBuilder { ); const { - dataSets, organisationUnits, users, userGroups, @@ -131,10 +129,6 @@ export class MetadataPayloadBuilder { ...rest } = metadataWithoutDuplicates; - const fixedDataSets = dataSets - ? await this.fixDataSetsCompulsoryDEOperandCatCombo(originInstance, dataSets as DataSet[]) - : undefined; - const visualizationsWithRows = visualizations ? await this.addRowsToVisualizations(originInstance, visualizations as Visualization[]) : []; @@ -157,7 +151,6 @@ export class MetadataPayloadBuilder { users: includeUsersObjectsAndReferences ? users : undefined, userGroups: includeSharingSettingsObjectsAndReferences ? userGroups : undefined, userRoles: includeSharingSettingsObjectsAndReferences ? userRoles : undefined, - ...(fixedDataSets && { dataSets: fixedDataSets }), ...rest, }; @@ -374,39 +367,6 @@ export class MetadataPayloadBuilder { }; } - private async fixDataSetsCompulsoryDEOperandCatCombo( - originInstance: Instance, - dataSets: DataSet[] - ): Promise { - const metadataRepository = this.repositoryFactory.metadataRepository(originInstance); - const categoryOptionCombos = await metadataRepository.getCategoryOptionCombos({ - code: { eq: "default" }, - }); - - const defaultCategoryOptionCombo = categoryOptionCombos[0]; - if (!defaultCategoryOptionCombo) { - console.error( - "Default category option combo not found, unable to fix data sets compulsory data element operands category option combo" - ); - return dataSets; - } - - return dataSets.map(dataSet => { - if (!dataSet.compulsoryDataElementOperands) return dataSet; - - const fixedCompulsoryDEOperands = dataSet.compulsoryDataElementOperands.map(operand => { - if (!operand.categoryOptionCombo || operand.categoryOptionCombo?.id) return operand; - - return { - ...operand, - categoryOptionCombo: { ...operand.categoryOptionCombo, id: defaultCategoryOptionCombo.id }, - }; - }); - - return { ...dataSet, compulsoryDataElementOperands: fixedCompulsoryDEOperands }; - }); - } - private async addRowsToVisualizations( originInstance: Instance, visualizations: Visualization[] diff --git a/src/domain/metadata/builders/__tests__/MetadataPayloadBuilder.spec.tsx b/src/domain/metadata/builders/__tests__/MetadataPayloadBuilder.spec.tsx index 096c08bae..69b8f565f 100644 --- a/src/domain/metadata/builders/__tests__/MetadataPayloadBuilder.spec.tsx +++ b/src/domain/metadata/builders/__tests__/MetadataPayloadBuilder.spec.tsx @@ -29,7 +29,6 @@ import { getDataSetTypeExpectedPayload, } from "./data/data-set-metadata-type"; import { MetadataPayloadBuilder } from "../MetadataPayloadBuilder"; -import { DataSet } from "../../entities/MetadataEntities"; import { givenABuilderWithUserGroupsAndDashboards, givenUserGroupsAndDashboardMetadataResponses, @@ -139,7 +138,6 @@ describe("MetadataPayloadBuilder", () => { } when(mockedMetadataRepository.listAllMetadata(anything())).thenResolve([]); - when(mockedMetadataRepository.getCategoryOptionCombos(anything())).thenResolve([]); const mockedRepositoryFactory = mock(); when(mockedRepositoryFactory.instanceRepository(anything())).thenReturn(instance(mockedInstanceRepository)); @@ -258,7 +256,6 @@ describe("MetadataPayloadBuilder", () => { } when(mockedMetadataRepository.listAllMetadata(anything())).thenResolve([]); - when(mockedMetadataRepository.getCategoryOptionCombos(anything())).thenResolve([]); const mockedRepositoryFactory = mock(); when(mockedRepositoryFactory.instanceRepository(anything())).thenReturn(instance(mockedInstanceRepository)); @@ -328,51 +325,11 @@ describe("MetadataPayloadBuilder", () => { expect(payload).toEqual(expectedPayload); }); - it("should replace missing categoryOptionCombo.id on compulsory data element operands with the default coc id", async () => { - const defaultCocId = "DEFAULT_COC_ID"; - const dataSetWithOperand = { - ...getDataSetMetadata(), - compulsoryDataElementOperands: [{ dataElement: { id: "BDuY694ZAFa" }, categoryOptionCombo: {} }], - } as unknown as DataSet; - - const builder = givenABuilderWithProgramType({ - includeObjectsAndReferences: false, - includeOnlyReferences: false, - }); - - const metadataPayloadBuilder = givenMetadataPayloadBuilderOfDataSet( - { includeObjectsAndReferences: false, includeOnlyReferences: false }, - { - dataSet: dataSetWithOperand, - categoryOptionCombos: [ - { - id: defaultCocId, - name: "default", - categoryCombo: { id: "bjDvmb4bfuf" }, - categoryOptions: [], - }, - ], - } - ); - - const payload: SynchronizationPayload = await metadataPayloadBuilder.build(builder); - - const operands = (payload.dataSets?.[0] as DataSet | undefined)?.compulsoryDataElementOperands; - expect(operands?.[0]?.categoryOptionCombo?.id).toBe(defaultCocId); - }); - - function givenMetadataPayloadBuilderOfDataSet( - options: { - includeObjectsAndReferences: boolean; - includeOnlyReferences: boolean; - }, - overrides?: { - dataSet?: DataSet; - categoryOptionCombos?: Awaited>; - } - ): MetadataPayloadBuilder { + function givenMetadataPayloadBuilderOfDataSet(options: { + includeObjectsAndReferences: boolean; + includeOnlyReferences: boolean; + }): MetadataPayloadBuilder { const { includeObjectsAndReferences } = options; - const dataSet = overrides?.dataSet ?? getDataSetMetadata(); const mockedInstanceRepository = mock(); when(mockedInstanceRepository.getById(anything())).thenResolve(dummyInstance); @@ -392,8 +349,10 @@ describe("MetadataPayloadBuilder", () => { if (includeObjectsAndReferences) { const metadataByIdsResponses = getDataSetMetadataByIdsResponsesWithIncludeAll(); + when( + mockedMetadataRepository.getMetadataByIds(anything(), anything(), anything()) + ).thenResolve(metadataByIdsResponses.first); when(mockedMetadataRepository.getMetadataByIds(anything())) - .thenResolve(metadataByIdsResponses.first) .thenResolve(metadataByIdsResponses.second) .thenResolve(metadataByIdsResponses.third) .thenResolve(metadataByIdsResponses.fourth) @@ -402,10 +361,12 @@ describe("MetadataPayloadBuilder", () => { .thenResolve(metadataByIdsResponses.seventh) .thenResolve(metadataByIdsResponses.eighth); } else { + when( + mockedMetadataRepository.getMetadataByIds(anything(), anything(), anything()) + ).thenResolve({ dataSets: [getDataSetMetadata()] }); when(mockedMetadataRepository.getMetadataByIds(anything())) - .thenResolve({ dataSets: [dataSet] }) .thenResolve({ - dataSets: [dataSet], + dataSets: [getDataSetMetadata()], dataElements: [getDataElementDataSetMetadata()], }) .thenResolve({ @@ -414,9 +375,6 @@ describe("MetadataPayloadBuilder", () => { } when(mockedMetadataRepository.listAllMetadata(anything())).thenResolve([]); - when(mockedMetadataRepository.getCategoryOptionCombos(anything())).thenResolve( - overrides?.categoryOptionCombos ?? [] - ); const mockedRepositoryFactory = mock(); when(mockedRepositoryFactory.instanceRepository(anything())).thenReturn(instance(mockedInstanceRepository)); diff --git a/src/domain/metadata/repositories/MetadataRepository.ts b/src/domain/metadata/repositories/MetadataRepository.ts index f44d2cc91..393ffd0d5 100644 --- a/src/domain/metadata/repositories/MetadataRepository.ts +++ b/src/domain/metadata/repositories/MetadataRepository.ts @@ -1,4 +1,4 @@ -import { FilterBase, FilterValueOperator } from "@eyeseetea/d2-api/api/common"; +import { FilterValueOperator } from "@eyeseetea/d2-api/api/common"; import { IdentifiableRef, Ref } from "../../common/entities/Ref"; import { Id } from "../../common/entities/Schemas"; import { SynchronizationResult } from "../../reports/entities/SynchronizationResult"; @@ -16,9 +16,9 @@ export interface MetadataRepository { getMetadataByIds(ids: Id[], fields?: object | string, includeDefaults?: boolean): Promise>; getByFilterRules(filterRules: FilterRule[]): Promise; getDefaultIds(filter?: string): Promise; - getCategoryOptionCombos( - filter?: FilterBase - ): Promise[]>; + getCategoryOptionCombos(): Promise< + Pick[] + >; getOrgUnitRoots(): Promise[]>; listMetadata(params: ListMetadataParams): Promise; listAllMetadata(params: ListMetadataParams): Promise; From 66dccb61c8310504247ece258b265c747d920253 Mon Sep 17 00:00:00 2001 From: Chukwudumebi Onwuli <37223065+deeonwuli@users.noreply.github.com> Date: Thu, 23 Apr 2026 15:19:31 +0100 Subject: [PATCH 07/10] fix: adjust metadata fetching for dataSets to include default categoryOptionCombo.id --- src/domain/metadata/builders/MetadataPayloadBuilder.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/domain/metadata/builders/MetadataPayloadBuilder.ts b/src/domain/metadata/builders/MetadataPayloadBuilder.ts index 983907219..bb6fba024 100644 --- a/src/domain/metadata/builders/MetadataPayloadBuilder.ts +++ b/src/domain/metadata/builders/MetadataPayloadBuilder.ts @@ -208,7 +208,13 @@ export class MetadataPayloadBuilder { // Get all the required metadata const originInstance = await this.getOriginInstance(originInstanceId); const metadataRepository = this.repositoryFactory.metadataRepository(originInstance); - const syncMetadata = await metadataRepository.getMetadataByIds(newIds); + // DataSets are fetched with includeDefaults=true because the server-side + // defaults=EXCLUDE strips the default categoryOptionCombo.id from + // compulsoryDataElementOperands, which then fails on import. + const syncMetadata = + type === "dataSets" + ? await metadataRepository.getMetadataByIds(newIds, undefined, true) + : await metadataRepository.getMetadataByIds(newIds); const elements = syncMetadata[collectionName] || []; this.registry.addList(builder, newIds); From 6ac58bfd7fcbcd9b4581e12fbfea126091031f7d Mon Sep 17 00:00:00 2001 From: Chukwudumebi Onwuli <37223065+deeonwuli@users.noreply.github.com> Date: Thu, 23 Apr 2026 15:20:54 +0100 Subject: [PATCH 08/10] Update MetadataPayloadBuilder.spec.tsx --- .../__tests__/MetadataPayloadBuilder.spec.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/domain/metadata/builders/__tests__/MetadataPayloadBuilder.spec.tsx b/src/domain/metadata/builders/__tests__/MetadataPayloadBuilder.spec.tsx index 69b8f565f..87ced7ee4 100644 --- a/src/domain/metadata/builders/__tests__/MetadataPayloadBuilder.spec.tsx +++ b/src/domain/metadata/builders/__tests__/MetadataPayloadBuilder.spec.tsx @@ -349,9 +349,9 @@ describe("MetadataPayloadBuilder", () => { if (includeObjectsAndReferences) { const metadataByIdsResponses = getDataSetMetadataByIdsResponsesWithIncludeAll(); - when( - mockedMetadataRepository.getMetadataByIds(anything(), anything(), anything()) - ).thenResolve(metadataByIdsResponses.first); + when(mockedMetadataRepository.getMetadataByIds(anything(), anything(), anything())).thenResolve( + metadataByIdsResponses.first + ); when(mockedMetadataRepository.getMetadataByIds(anything())) .thenResolve(metadataByIdsResponses.second) .thenResolve(metadataByIdsResponses.third) @@ -361,9 +361,9 @@ describe("MetadataPayloadBuilder", () => { .thenResolve(metadataByIdsResponses.seventh) .thenResolve(metadataByIdsResponses.eighth); } else { - when( - mockedMetadataRepository.getMetadataByIds(anything(), anything(), anything()) - ).thenResolve({ dataSets: [getDataSetMetadata()] }); + when(mockedMetadataRepository.getMetadataByIds(anything(), anything(), anything())).thenResolve({ + dataSets: [getDataSetMetadata()], + }); when(mockedMetadataRepository.getMetadataByIds(anything())) .thenResolve({ dataSets: [getDataSetMetadata()], From eac6f7a168207fa25823c342c1434afcc447db0a Mon Sep 17 00:00:00 2001 From: Chukwudumebi Onwuli <37223065+deeonwuli@users.noreply.github.com> Date: Thu, 23 Apr 2026 16:09:28 +0100 Subject: [PATCH 09/10] fix: update getMetadataByIds to include preserveNestedDefaultRefs parameter and adjust related logic --- i18n/en.pot | 4 ++-- src/data/metadata/MetadataD2ApiRepository.ts | 19 ++++++++++++++----- .../builders/MetadataPayloadBuilder.ts | 8 ++++---- .../__tests__/MetadataPayloadBuilder.spec.tsx | 10 ++++++---- .../repositories/MetadataRepository.ts | 8 +++++++- 5 files changed, 33 insertions(+), 16 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index ae226b2f2..c0a581a54 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-04-23T11:28:44.991Z\n" -"PO-Revision-Date: 2026-04-23T11:28:44.992Z\n" +"POT-Creation-Date: 2026-04-23T14:24:01.114Z\n" +"PO-Revision-Date: 2026-04-23T14:24:01.114Z\n" msgid "" "THIS NEW RELEASE INCLUDES SHARING SETTINGS PER INSTANCES. FOR THIS VERSION " diff --git a/src/data/metadata/MetadataD2ApiRepository.ts b/src/data/metadata/MetadataD2ApiRepository.ts index c8f58c85e..58f7b88dd 100644 --- a/src/data/metadata/MetadataD2ApiRepository.ts +++ b/src/data/metadata/MetadataD2ApiRepository.ts @@ -68,14 +68,20 @@ export class MetadataD2ApiRepository implements MetadataRepository { public async getMetadataByIds( ids: string[], fields?: object | string, - includeDefaults = false + includeDefaults = false, + preserveNestedDefaultRefs = false ): Promise> { const { apiVersion } = this.targetInstance; const d2ApiDataStore = new D2ApiDataStore(this.targetInstance); const dataStoreIds = DataStoreMetadata.getDataStoreIds(ids); const requestFields = typeof fields === "object" ? getFieldsAsString(fields) : fields; - const d2Metadata = await this.getMetadata(ids, requestFields, includeDefaults); + const d2Metadata = await this.getMetadata( + ids, + requestFields, + includeDefaults, + preserveNestedDefaultRefs + ); if (apiVersion >= 32 && d2Metadata["dashboards"] && fields === undefined) { //Fix dashboard bug from 2.32 @@ -84,7 +90,8 @@ export class MetadataD2ApiRepository implements MetadataRepository { const fixedD2Metadata = await this.getMetadata( ids, ":all,dashboardItems[:all,visualization[id,type]]", - includeDefaults + includeDefaults, + preserveNestedDefaultRefs ); const metadataPackage = this.transformationRepository.mapPackageFrom( @@ -589,11 +596,13 @@ export class MetadataD2ApiRepository implements MetadataRepository { private async getMetadata( elements: string[], fields = ":all", - includeDefaults: boolean + includeDefaults: boolean, + preserveNestedDefaultRefs: boolean ): Promise> { try { const promises = []; const chunkSize = 50; + const keepDefaults = includeDefaults || preserveNestedDefaultRefs === true; for (let i = 0; i < elements.length; i += chunkSize) { const requestElements = elements.slice(i, i + chunkSize).toString(); @@ -602,7 +611,7 @@ export class MetadataD2ApiRepository implements MetadataRepository { .get("/metadata", { fields, filter: "id:in:[" + requestElements + "]", - defaults: includeDefaults ? undefined : "EXCLUDE", + defaults: keepDefaults ? undefined : "EXCLUDE", }) .getData() ); diff --git a/src/domain/metadata/builders/MetadataPayloadBuilder.ts b/src/domain/metadata/builders/MetadataPayloadBuilder.ts index bb6fba024..19048d81d 100644 --- a/src/domain/metadata/builders/MetadataPayloadBuilder.ts +++ b/src/domain/metadata/builders/MetadataPayloadBuilder.ts @@ -208,12 +208,12 @@ export class MetadataPayloadBuilder { // Get all the required metadata const originInstance = await this.getOriginInstance(originInstanceId); const metadataRepository = this.repositoryFactory.metadataRepository(originInstance); - // DataSets are fetched with includeDefaults=true because the server-side - // defaults=EXCLUDE strips the default categoryOptionCombo.id from - // compulsoryDataElementOperands, which then fails on import. + // DataSets need preserveNestedDefaultRefs because the server-side defaults=EXCLUDE + // strips the default categoryOptionCombo.id from compulsoryDataElementOperands, + // which then fails on import. Client-side default stripping still runs. const syncMetadata = type === "dataSets" - ? await metadataRepository.getMetadataByIds(newIds, undefined, true) + ? await metadataRepository.getMetadataByIds(newIds, undefined, false, true) : await metadataRepository.getMetadataByIds(newIds); const elements = syncMetadata[collectionName] || []; this.registry.addList(builder, newIds); diff --git a/src/domain/metadata/builders/__tests__/MetadataPayloadBuilder.spec.tsx b/src/domain/metadata/builders/__tests__/MetadataPayloadBuilder.spec.tsx index 87ced7ee4..c88acb2f4 100644 --- a/src/domain/metadata/builders/__tests__/MetadataPayloadBuilder.spec.tsx +++ b/src/domain/metadata/builders/__tests__/MetadataPayloadBuilder.spec.tsx @@ -349,9 +349,9 @@ describe("MetadataPayloadBuilder", () => { if (includeObjectsAndReferences) { const metadataByIdsResponses = getDataSetMetadataByIdsResponsesWithIncludeAll(); - when(mockedMetadataRepository.getMetadataByIds(anything(), anything(), anything())).thenResolve( - metadataByIdsResponses.first - ); + when( + mockedMetadataRepository.getMetadataByIds(anything(), anything(), anything(), anything()) + ).thenResolve(metadataByIdsResponses.first); when(mockedMetadataRepository.getMetadataByIds(anything())) .thenResolve(metadataByIdsResponses.second) .thenResolve(metadataByIdsResponses.third) @@ -361,7 +361,9 @@ describe("MetadataPayloadBuilder", () => { .thenResolve(metadataByIdsResponses.seventh) .thenResolve(metadataByIdsResponses.eighth); } else { - when(mockedMetadataRepository.getMetadataByIds(anything(), anything(), anything())).thenResolve({ + when( + mockedMetadataRepository.getMetadataByIds(anything(), anything(), anything(), anything()) + ).thenResolve({ dataSets: [getDataSetMetadata()], }); when(mockedMetadataRepository.getMetadataByIds(anything())) diff --git a/src/domain/metadata/repositories/MetadataRepository.ts b/src/domain/metadata/repositories/MetadataRepository.ts index 393ffd0d5..9aa6de2f7 100644 --- a/src/domain/metadata/repositories/MetadataRepository.ts +++ b/src/domain/metadata/repositories/MetadataRepository.ts @@ -13,13 +13,19 @@ import { import { MetadataImportParams } from "../entities/MetadataSynchronizationParams"; export interface MetadataRepository { - getMetadataByIds(ids: Id[], fields?: object | string, includeDefaults?: boolean): Promise>; + getMetadataByIds( + ids: Id[], + fields?: object | string, + includeDefaults?: boolean, + preserveNestedDefaultRefs?: boolean + ): Promise>; getByFilterRules(filterRules: FilterRule[]): Promise; getDefaultIds(filter?: string): Promise; getCategoryOptionCombos(): Promise< Pick[] >; getOrgUnitRoots(): Promise[]>; + listMetadata(params: ListMetadataParams): Promise; listAllMetadata(params: ListMetadataParams): Promise; lookupSimilar(query: IdentifiableRef): Promise>; From 8ebec2452673f997a4111b0ba1e36c7b51f1b5b6 Mon Sep 17 00:00:00 2001 From: Chukwudumebi Onwuli <37223065+deeonwuli@users.noreply.github.com> Date: Mon, 27 Apr 2026 09:43:30 +0100 Subject: [PATCH 10/10] fix: simplify condition for keepDefaults in getMetadataByIds method --- i18n/en.pot | 4 ++-- src/data/metadata/MetadataD2ApiRepository.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index c0a581a54..aaefa41e4 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-04-23T14:24:01.114Z\n" -"PO-Revision-Date: 2026-04-23T14:24:01.114Z\n" +"POT-Creation-Date: 2026-04-27T07:43:21.475Z\n" +"PO-Revision-Date: 2026-04-27T07:43:21.476Z\n" msgid "" "THIS NEW RELEASE INCLUDES SHARING SETTINGS PER INSTANCES. FOR THIS VERSION " diff --git a/src/data/metadata/MetadataD2ApiRepository.ts b/src/data/metadata/MetadataD2ApiRepository.ts index 58f7b88dd..02a073745 100644 --- a/src/data/metadata/MetadataD2ApiRepository.ts +++ b/src/data/metadata/MetadataD2ApiRepository.ts @@ -602,7 +602,7 @@ export class MetadataD2ApiRepository implements MetadataRepository { try { const promises = []; const chunkSize = 50; - const keepDefaults = includeDefaults || preserveNestedDefaultRefs === true; + const keepDefaults = includeDefaults || preserveNestedDefaultRefs; for (let i = 0; i < elements.length; i += chunkSize) { const requestElements = elements.slice(i, i + chunkSize).toString();