Skip to content

Conversation

@MatiasArriola
Copy link

📌 References

📝 Implementation

  • Helper script to help setup the initial metadata required by the app.
  • Creates constants with the code format {questionCode}_{value} and updates dataElements codes to questionCode.
  • Based on the related plugin https://github.com/EyeSeeTea/extra-texts-for-options-capture-plugin/
  • Kept separate files for this script and didn't integrate to the app arch (create useCases, entities, repos) for simplicity. This is a one-time operation and does not need to be reused from the web.

📹 Screenshots/Screen capture

🔥 Notes to the tester

@bundlemon
Copy link

bundlemon bot commented Nov 13, 2025

BundleMon

No change in files bundle size

Groups updated (1)
Status Path Size Limits
Build Folder
./**/*
858.93KB (+38B 0%) +20%

Final result: ✅

View report in BundleMon website ➡️


Current branch size history | Target branch size history

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces a helper script to import and set up DHIS2 constants and data elements from an Excel file. The script creates constants with formatted codes and updates data element codes based on question metadata.

  • Script infrastructure for parsing Excel files and importing metadata to DHIS2
  • Constants generation with format {questionCode}_{score} for each option
  • Data element code updates based on Excel input

Reviewed Changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
package.json Added xlsx library dependency and updated script command
yarn.lock Added xlsx and related dependencies (adler-32, cfb, codepage, crc-32, ssf, wmf, word)
src/scripts/tsconfig.json Updated include path from "src" to "." to match current directory structure
src/scripts/import-constants.ts Main script entry point with CLI argument parsing and orchestration logic
src/scripts/import-constants/parseExcelFile.ts Excel parsing logic with validation for sheet structure and data format
src/scripts/import-constants/constants.ts Constant building and DHIS2 import logic
src/scripts/import-constants/dataElements.ts Data element retrieval and update logic
src/scripts/import-constants/ImportInput.ts Type definitions for parsed Excel data
src/scripts/example.ts Removed example script file
README.md Added comprehensive documentation for the import-constants script usage

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

});
}

export async function importDataElements(api: D2Api, dataElements: any[]): Promise<any[]> {
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent use of any type weakens type safety. The dataElements parameter should be more specifically typed. Consider creating a proper type for data elements or using a type from the D2Api models, such as extracting the type from the API response.

Copilot uses AI. Check for mistakes.
};
if (!option.score || !option.definition || !option.nonConformity) {
throw new Error(
`Missing one of${Object.values(optionMappings).join(", ")} in sheet "${sheetName}"`
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message format is inconsistent. On line 83, the error message template string uses ${Object.values(optionMappings).join(", ")} which will output "Report, Score, NonConformity", but the text says "Missing one of" which is grammatically incorrect. It should be "Missing one or more of the following fields:" or similar to be clear and grammatically correct.

Suggested change
`Missing one of${Object.values(optionMappings).join(", ")} in sheet "${sheetName}"`
`Missing one or more of the following fields: ${Object.values(optionMappings).join(", ")} in sheet "${sheetName}"`

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +20
const dataElementsResponse = await api.models.dataElements
.get({
fields: { $all: true },
paging: false,
filter: { id: { in: Array.from(idToCodeMap.keys()) } },
})
.getData();
const dataElements = dataElementsResponse.objects;

const dataElementsNotFound = Array.from(idToCodeMap.keys()).filter(
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential performance issue: The code performs Array.from(idToCodeMap.keys()) twice - once on line 15 and again on line 20. Consider storing the result in a variable to avoid redundant iteration.

Suggested change
const dataElementsResponse = await api.models.dataElements
.get({
fields: { $all: true },
paging: false,
filter: { id: { in: Array.from(idToCodeMap.keys()) } },
})
.getData();
const dataElements = dataElementsResponse.objects;
const dataElementsNotFound = Array.from(idToCodeMap.keys()).filter(
const idToCodeKeys = Array.from(idToCodeMap.keys());
const dataElementsResponse = await api.models.dataElements
.get({
fields: { $all: true },
paging: false,
filter: { id: { in: idToCodeKeys } },
})
.getData();
const dataElements = dataElementsResponse.objects;
const dataElementsNotFound = idToCodeKeys.filter(

Copilot uses AI. Check for mistakes.
Comment on lines +53 to +68
const elements: ImportElement[] = [];
let currentElement: ImportElement | null = null;
for (const row of data) {
if (typeof row !== "object" || row === null) {
throw new Error(`Invalid row format in sheet "${sheetName}"`);
}
const newElement = mapRow(sheetName, row, currentElement);
if (newElement && newElement !== currentElement) {
elements.push(newElement);
currentElement = newElement;
}
}
return {
programId: programId,
elements: elements,
};
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a sheet has no valid data rows after parsing (empty sheet or all rows fail validation), the function returns an ImportInput with an empty elements array. This could lead to creating program entries with no actual data. Consider validating that at least one element was parsed successfully before returning, or documenting this behavior.

Copilot uses AI. Check for mistakes.
}
if (Object.prototype.hasOwnProperty.call(data, mappings.constantCode)) {
const constantCode = String((data as any)[mappings.constantCode] || "").trim();
if (!currentElement && !constantCode) {
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error validation logic has an issue. When validating constantCode on line 88, it only checks if the code is missing when !currentElement, but the code can also be an empty string even if currentElement exists. If someone provides an empty constantCode in the middle of the file, it will create a new element with an empty constantCode, which is invalid. Consider adding validation: if (!constantCode) throw new Error(...)

Suggested change
if (!currentElement && !constantCode) {
if (!constantCode) {

Copilot uses AI. Check for mistakes.
} else {
currentElement.options.push(option);
}
return currentElement;
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return type of mapRow is declared as ImportElement, but the function can return currentElement which is ImportElement | null. When currentElement is passed in and options are added to it (line 103), the function returns currentElement which could technically be null according to the parameter type. TypeScript should catch this, but for clarity, consider handling this case explicitly or asserting that currentElement is not null at line 103.

Suggested change
return currentElement;
return currentElement!;

Copilot uses AI. Check for mistakes.
Builds constants for each option of each question with:

- Code: `{Key}_{Score}` (e.g., `MAL_MEAT_CM_CAPACITY_1`, `MAL_MEAT_CM_CAPACITY_2`)
- Value: 1 if `NonConformity`is `YES`. 0 otherwise.
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing space after "NonConformity" - should be "NonConformity is" instead of "NonConformity`is".

Suggested change
- Value: 1 if `NonConformity`is `YES`. 0 otherwise.
- Value: 1 if `NonConformity` is `YES`. 0 otherwise.

Copilot uses AI. Check for mistakes.
run(cmd, process.argv.slice(2));
}

async function saveJsonToFile(data: any, outputFilePath: string) {
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent use of any type weakens type safety. The data parameter should have a more specific type. Consider defining an interface for the expected structure or using TypeScript's Record type.

Suggested change
async function saveJsonToFile(data: any, outputFilePath: string) {
async function saveJsonToFile(data: Record<string, unknown>, outputFilePath: string) {

Copilot uses AI. Check for mistakes.
}

export function buildShortName(constantCode: string, score: number): string {
return `${constantCode.split("_").join(" - ")} (${score})`;
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The function buildShortName uses .split("_").join(" - ") which could be simplified to .replace(/_/g, " - ") for better readability. However, this is a minor style preference.

Suggested change
return `${constantCode.split("_").join(" - ")} (${score})`;
return `${constantCode.replace(/_/g, " - ")} (${score})`;

Copilot uses AI. Check for mistakes.
Comment on lines +76 to +81
const option: ImportElementOption = {
definition: String((data as any)[optionMappings.definition] || "").trim(),
score: Number((data as any)[optionMappings.score] || 0),
nonConformity: String((data as any)[optionMappings.nonConformity] || "").trim(),
};
if (!option.score || !option.definition || !option.nonConformity) {
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation for nonConformity on line 81 checks if it's truthy after trimming, but doesn't validate that the value is actually "YES" or "NO" (or any other expected values). This could allow invalid values to pass through. While line 38 in constants.ts handles the conversion with .toUpperCase() === "YES", it would be better to validate the input format at parse time to catch errors early.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants