Skip to content

Commit f240dbe

Browse files
committed
generate metadata and response errors to disk
1 parent ccd5871 commit f240dbe

4 files changed

Lines changed: 116 additions & 52 deletions

File tree

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,9 @@ REACT_APP_DHIS2_AUTH='admin:district'
6262
Run the script:
6363

6464
```shell
65-
yarn run generate-dataset-approval --dataSet MY_DS_CODE
65+
yarn run generate-dataset-approval --dataSet MY_DS_CODE \
66+
--dataElement-submission "MY_DS_CODE-Submission date module1-APVD" \
67+
--dataElement-approval "MY_DS_CODE-Approval date module1-APVD"
6668
```
6769

6870
Parameters:
@@ -73,5 +75,5 @@ Parameters:
7375
Notes:
7476

7577
- Writes a metadata JSON file named `<dataSetCode>_<timestamp>.json` in the current directory.
76-
- Skips dataElements without code and saves them to `skipped_<dataSetCode>_<timestamp>.json`.
78+
- Show dataElements without code and saves them to `warning_<dataSetCode>_<timestamp>.json`.
7779
- On validation/import errors, saves details to `errors_<dataSetCode>_<timestamp>.json`.

src/domain/reports/mal-data-approval/repositories/UserGroupRepository.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { Ref } from "../../../common/entities/Base";
21
import { NamedRef } from "../../../common/entities/Ref";
32
import { FutureData } from "../../../generic/Future";
43

src/scripts/approve-mal-datavalues.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export async function approveMalDataValues(options: ApprovalOptions): Promise<vo
5353
const dataSetConfigs = await getConfigUseCase.execute().toPromise();
5454

5555
const { dataSet, orgUnit } = await getMalWMRMetadata(api, dataSetCode, ouOption);
56-
console.log(`dataSet original: ${dataSet.name}`);
56+
console.debug(`dataSet original: ${dataSet.name}`);
5757
const malDataApprovalItems = await buildMalApprovalItems(
5858
dataValueRepository,
5959
dataSetRepository,

src/scripts/generate-dataset-approval.ts

Lines changed: 111 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import _ from "lodash";
12
import "dotenv-flow/config";
23
import fs from "fs";
34
import { D2Api } from "../types/d2-api";
@@ -22,10 +23,21 @@ async function main() {
2223

2324
parser.add_argument("--post", {
2425
help: "Commit changes to DHIS2 (default: validate only)",
25-
action: "store_true",
2626
default: false,
2727
});
2828

29+
parser.add_argument("--dataElement-submission", {
30+
help: "Name/code for submission datetime dataElement",
31+
metavar: "name",
32+
required: false,
33+
});
34+
35+
parser.add_argument("--dataElement-approval", {
36+
help: "Name/code for approval datetime dataElement",
37+
metavar: "name",
38+
required: false,
39+
});
40+
2941
try {
3042
const args = parser.parse_args();
3143
const baseUrl = process.env.REACT_APP_DHIS2_BASE_URL || "";
@@ -36,14 +48,20 @@ async function main() {
3648

3749
const api = new D2Api({ baseUrl, auth: { username, password } });
3850

39-
await generateDataSetApproval({ api, dataSetCode: args.dataSet, commit: args.post });
51+
await generateDataSetApproval({
52+
api,
53+
dataSetCode: args.dataSet,
54+
commit: args.post,
55+
dataElementSubmission: args.dataElement_submission,
56+
dataElementApproval: args.dataElement_approval,
57+
});
4058
} catch (err) {
4159
console.error(err);
4260
process.exit(1);
4361
}
4462
}
4563

46-
type SkippedDataElement = {
64+
type WarningDataElement = {
4765
id: string;
4866
name: string;
4967
reason: string;
@@ -81,26 +99,46 @@ type NewSection = Omit<D2Section, "dataSet"> & {
8199
}>;
82100
};
83101

84-
async function generateDataSetApproval(options: { api: D2Api; dataSetCode: string; commit: boolean }): Promise<void> {
85-
const { api, dataSetCode, commit } = options;
102+
async function generateDataSetApproval(options: {
103+
api: D2Api;
104+
dataSetCode: string;
105+
commit: boolean;
106+
dataElementSubmission?: string;
107+
dataElementApproval?: string;
108+
}): Promise<void> {
109+
const { api, dataSetCode, commit, dataElementSubmission, dataElementApproval } = options;
86110

87111
const originalDataSet = await getDataSetByCode(api, dataSetCode);
88112

89113
const dataElementIds = originalDataSet.dataSetElements?.map(dse => dse.dataElement.id) ?? [];
90114

91115
const originalDataElements = await getDataElementsDetails(api, dataElementIds);
92116

93-
const { validDataElements, skippedDataElements } = filterDataElementsWithCode(originalDataElements);
117+
const { validDataElements, warningDataElements } = filterDataElementsWithCode(originalDataElements);
94118

95-
if (skippedDataElements.length > 0) {
96-
saveSkippedDataElementsToFile(skippedDataElements, dataSetCode);
119+
if (warningDataElements.length > 0) {
120+
saveWarningDataElementsToFile(warningDataElements, dataSetCode);
97121
console.warn(
98-
`Skipped ${skippedDataElements.length} dataElement(s) without code. See skipped file for details.`
122+
`Warning: ${warningDataElements.length} dataElement(s) without code. See warnings file for details.`
99123
);
100124
}
101125

102126
const newDataElements = validDataElements.map(transformDataElement);
103127

128+
const customDataElements: NewDataElement[] = [];
129+
130+
if (dataElementSubmission) {
131+
const submissionDE = createCustomDataElement(dataElementSubmission);
132+
customDataElements.push(submissionDE);
133+
}
134+
135+
if (dataElementApproval) {
136+
const approvalDE = createCustomDataElement(dataElementApproval);
137+
customDataElements.push(approvalDE);
138+
}
139+
140+
const allNewDataElements = [...newDataElements, ...customDataElements];
141+
104142
const dataElementIdMap = createDataElementIdMap(validDataElements, newDataElements);
105143

106144
const originalSections = await getSectionsByDataSet(api, originalDataSet.id);
@@ -119,18 +157,19 @@ async function generateDataSetApproval(options: { api: D2Api; dataSetCode: strin
119157
const newDataSet = transformDataSet(
120158
originalDataSet,
121159
dataElementIdMap,
122-
newSections.map(s => s.id)
160+
newSections.map(s => s.id),
161+
customDataElements.map(de => de.id)
123162
);
124163

125164
const existingMetadata = await getExistingMetadata(
126165
api,
127166
newDataSet.id,
128-
newDataElements.map(de => de.id),
167+
allNewDataElements.map(de => de.id),
129168
newSections.map(s => s.id)
130169
);
131170

132171
const finalMetadata = mergeWithExisting(
133-
{ dataSets: [newDataSet], dataElements: newDataElements, sections: newSections },
172+
{ dataSets: [newDataSet], dataElements: allNewDataElements, sections: newSections },
134173
existingMetadata
135174
);
136175

@@ -143,31 +182,28 @@ async function generateDataSetApproval(options: { api: D2Api; dataSetCode: strin
143182

144183
function filterDataElementsWithCode(dataElements: D2DataElement[]): {
145184
validDataElements: D2DataElement[];
146-
skippedDataElements: SkippedDataElement[];
185+
warningDataElements: WarningDataElement[];
147186
} {
148-
const validDataElements: D2DataElement[] = [];
149-
const skippedDataElements: SkippedDataElement[] = [];
187+
const warningDataElements: WarningDataElement[] = [];
150188

151189
for (const de of dataElements) {
152-
if (de.code && de.code.trim() !== "") {
153-
validDataElements.push(de);
154-
} else {
155-
skippedDataElements.push({
190+
if (!de.code || de.code.trim() === "") {
191+
warningDataElements.push({
156192
id: de.id,
157193
name: de.name,
158-
reason: "DataElement does not have a code and cannot be cloned",
194+
reason: "DataElement does not have a code - cloned with empty code",
159195
});
160196
}
161197
}
162198

163-
return { validDataElements, skippedDataElements };
199+
return { validDataElements: dataElements, warningDataElements };
164200
}
165201

166-
function saveSkippedDataElementsToFile(skipped: SkippedDataElement[], dataSetCode: string): void {
202+
function saveWarningDataElementsToFile(warnings: WarningDataElement[], dataSetCode: string): void {
167203
const timestamp = Date.now();
168-
const filename = `skipped_${dataSetCode}_${timestamp}.json`;
169-
fs.writeFileSync(filename, JSON.stringify(skipped, null, 2));
170-
console.debug(`Skipped dataElements saved to: ${filename}`);
204+
const filename = `warnings_dataelements_${dataSetCode}_${timestamp}.json`;
205+
fs.writeFileSync(filename, JSON.stringify(warnings, null, 2));
206+
console.debug(`Warning dataElements saved to: ${filename}`);
171207
}
172208

173209
type D2DataSet = {
@@ -308,6 +344,26 @@ function addSuffix(value: string): string {
308344
return `${value}${SUFFIX}`;
309345
}
310346

347+
function ensureSuffix(value: string): string {
348+
return value.endsWith(SUFFIX) ? value : addSuffix(value);
349+
}
350+
351+
function createCustomDataElement(name: string): NewDataElement {
352+
const nameWithSuffix = ensureSuffix(name);
353+
const shortName = nameWithSuffix.substring(0, MAX_SHORT_NAME_LENGTH);
354+
const id = getUidFromSeed(nameWithSuffix);
355+
356+
return {
357+
id,
358+
name: nameWithSuffix,
359+
shortName,
360+
code: nameWithSuffix,
361+
valueType: "DATETIME" as const,
362+
domainType: "AGGREGATE" as const,
363+
aggregationType: "NONE" as const,
364+
};
365+
}
366+
311367
function transformDataElement(original: D2DataElement): NewDataElement {
312368
const {
313369
dataElementGroups: _deGroups,
@@ -317,8 +373,9 @@ function transformDataElement(original: D2DataElement): NewDataElement {
317373
...rest
318374
} = original;
319375

320-
const newCode = addSuffix(original.code);
321-
const newId = getUidFromSeed(newCode);
376+
const hasCode = original.code && original.code.trim() !== "";
377+
const newCode = hasCode ? addSuffix(original.code) : "";
378+
const newId = hasCode ? getUidFromSeed(newCode) : getUidFromSeed(addSuffix(original.id));
322379

323380
return {
324381
...rest,
@@ -350,16 +407,26 @@ function transformSection(
350407
const newCode = addSuffix(original.code);
351408
const newId = getUidFromSeed(newCode);
352409

353-
const newDataElements = (original.dataElements ?? [])
354-
.filter(de => dataElementIdMap[de.id] !== undefined)
355-
.map(de => ({ id: dataElementIdMap[de.id]! }));
356-
357-
const newGreyedFields = (original.greyedFields ?? [])
358-
.filter(gf => dataElementIdMap[gf.dataElement.id] !== undefined)
359-
.map(gf => ({
360-
dataElement: { id: dataElementIdMap[gf.dataElement.id]! },
361-
categoryOptionCombo: { id: gf.categoryOptionCombo.id },
362-
}));
410+
const newDataElements = _(original.dataElements ?? [])
411+
.map(de => {
412+
const newId = dataElementIdMap[de.id];
413+
return newId ? { id: newId } : undefined;
414+
})
415+
.compact()
416+
.value();
417+
418+
const newGreyedFields = _(original.greyedFields ?? [])
419+
.map(gf => {
420+
const newDataElementId = dataElementIdMap[gf.dataElement.id];
421+
return newDataElementId
422+
? {
423+
dataElement: { id: newDataElementId },
424+
categoryOptionCombo: { id: gf.categoryOptionCombo.id },
425+
}
426+
: undefined;
427+
})
428+
.compact()
429+
.value();
363430

364431
return {
365432
...rest,
@@ -375,14 +442,14 @@ function transformSection(
375442
function transformDataSet(
376443
original: D2DataSet,
377444
dataElementIdMap: Record<string, string>,
378-
newSectionIds: string[]
445+
newSectionIds: string[],
446+
customDataElementIds: string[] = []
379447
): NewDataSet {
380448
const { workflow: _workflow, dataEntryForm: _dataEntryForm, id: _id, sections: _sections, ...rest } = original;
381449

382450
const newCode = addSuffix(original.code);
383451
const newId = getUidFromSeed(newCode);
384452

385-
// Filter out dataSetElements whose dataElement was skipped (no code)
386453
const newDataSetElements = (original.dataSetElements ?? [])
387454
.filter(dse => dataElementIdMap[dse.dataElement.id] !== undefined)
388455
.map(dse => {
@@ -397,13 +464,17 @@ function transformDataSet(
397464
};
398465
});
399466

467+
const customDataSetElements = customDataElementIds.map(id => ({
468+
dataElement: { id },
469+
}));
470+
400471
return {
401472
...rest,
402473
id: newId,
403474
name: addSuffix(original.name),
404475
shortName: addSuffix(original.shortName),
405476
code: newCode,
406-
dataSetElements: newDataSetElements,
477+
dataSetElements: [...newDataSetElements, ...customDataSetElements],
407478
sections: newSectionIds.map(id => ({ id })),
408479
};
409480
}
@@ -414,7 +485,6 @@ async function getExistingMetadata(
414485
dataElementIds: string[],
415486
sectionIds: string[]
416487
): Promise<ExistingMetadata> {
417-
// Fetch existing dataSet
418488
const dataSetResponse = await api.models.dataSets
419489
.get({
420490
fields: { $owner: true },
@@ -425,7 +495,6 @@ async function getExistingMetadata(
425495

426496
const existingDataSet = dataSetResponse.objects[0];
427497

428-
// Fetch existing dataElements using chunks
429498
const existingDataElements = await getInChunks(dataElementIds, async idsChunk => {
430499
const response = await api.models.dataElements
431500
.get({
@@ -438,7 +507,6 @@ async function getExistingMetadata(
438507
return response.objects;
439508
});
440509

441-
// Fetch existing sections using chunks
442510
const existingSections =
443511
sectionIds.length > 0
444512
? await getInChunks(sectionIds, async idsChunk => {
@@ -465,19 +533,16 @@ async function getExistingMetadata(
465533
}
466534

467535
function mergeWithExisting(newMetadata: Metadata, existingMetadata: ExistingMetadata): Metadata {
468-
// Merge dataSet: preserve existing fields, override with new values
469536
const newDataSet = newMetadata.dataSets[0];
470537
const mergedDataSet = existingMetadata.dataSet ? { ...existingMetadata.dataSet, ...newDataSet } : newDataSet;
471538

472-
// Merge dataElements: for each new element, if exists, merge with existing
473539
const existingDataElementMap = new Map(existingMetadata.dataElements.map(de => [de.id, de]));
474540

475541
const mergedDataElements = newMetadata.dataElements.map(newDE => {
476542
const existingDE = existingDataElementMap.get(newDE.id);
477543
return existingDE ? { ...existingDE, ...newDE } : newDE;
478544
});
479545

480-
// Merge sections: for each new section, if exists, merge with existing
481546
const existingSectionMap = new Map(existingMetadata.sections.map(s => [s.id, s]));
482547

483548
const mergedSections = newMetadata.sections.map(newSection => {
@@ -550,13 +615,11 @@ async function persistMetadata(
550615
)}`
551616
);
552617

553-
// Check for errors in typeReports even on success
554618
const errors = extractErrorsFromTypeReports(response.typeReports);
555619
if (errors.length > 0) {
556620
saveErrorsToFile(errors, dataSetCode);
557621
}
558622
} catch (err: unknown) {
559-
// Handle HTTP errors (e.g., 409 Conflict)
560623
if (isHttpError(err)) {
561624
const responseData = err.response?.data?.response as MetadataResponse | undefined;
562625
if (responseData?.typeReports) {

0 commit comments

Comments
 (0)