1+ import _ from "lodash" ;
12import "dotenv-flow/config" ;
23import fs from "fs" ;
34import { 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
144183function 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
173209type 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+
311367function 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(
375442function 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
467535function 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