-
Notifications
You must be signed in to change notification settings - Fork 0
Import script #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: development
Are you sure you want to change the base?
Conversation
BundleMonNo change in files bundle size Groups updated (1)
Final result: ✅ View report in BundleMon website ➡️ |
There was a problem hiding this 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[]> { |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
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.
| }; | ||
| if (!option.score || !option.definition || !option.nonConformity) { | ||
| throw new Error( | ||
| `Missing one of${Object.values(optionMappings).join(", ")} in sheet "${sheetName}"` |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
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.
| `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}"` |
| 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( |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
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.
| 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( |
| 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, | ||
| }; |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
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.
| } | ||
| if (Object.prototype.hasOwnProperty.call(data, mappings.constantCode)) { | ||
| const constantCode = String((data as any)[mappings.constantCode] || "").trim(); | ||
| if (!currentElement && !constantCode) { |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
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(...)
| if (!currentElement && !constantCode) { | |
| if (!constantCode) { |
| } else { | ||
| currentElement.options.push(option); | ||
| } | ||
| return currentElement; |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
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.
| return currentElement; | |
| return currentElement!; |
| 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. |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
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".
| - Value: 1 if `NonConformity`is `YES`. 0 otherwise. | |
| - Value: 1 if `NonConformity` is `YES`. 0 otherwise. |
| run(cmd, process.argv.slice(2)); | ||
| } | ||
|
|
||
| async function saveJsonToFile(data: any, outputFilePath: string) { |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
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.
| async function saveJsonToFile(data: any, outputFilePath: string) { | |
| async function saveJsonToFile(data: Record<string, unknown>, outputFilePath: string) { |
| } | ||
|
|
||
| export function buildShortName(constantCode: string, score: number): string { | ||
| return `${constantCode.split("_").join(" - ")} (${score})`; |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
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.
| return `${constantCode.split("_").join(" - ")} (${score})`; | |
| return `${constantCode.replace(/_/g, " - ")} (${score})`; |
| 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) { |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
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.
📌 References
📝 Implementation
{questionCode}_{value}and updates dataElements codes toquestionCode.📹 Screenshots/Screen capture
🔥 Notes to the tester