From e17ba94c5958790b7611635c14edbe545dfe35d9 Mon Sep 17 00:00:00 2001 From: Emmanuel Evance Date: Fri, 6 Sep 2024 15:36:09 +0300 Subject: [PATCH] Update second workflow (#5) * add program * code format * wip: update mappings for create encounter * cereate-events * Update workflow file path * remove ocl mapping --- ...dd57-9a3c-4318-bdcb-f57a386cf811-spec.yaml | 37 ++---- workflows/wf2/1-get-patients.js | 10 +- workflows/wf2/2-upsert-teis.js | 12 +- workflows/wf2/3-get-encounters.js | 20 ++-- workflows/wf2/4-get-oclmap.js | 65 ----------- workflows/wf2/4-get-options-map.js | 60 ++++++++++ workflows/wf2/5-create-events.js | 107 ------------------ workflows/wf2/5-get-teis.js | 24 ++++ workflows/wf2/6-create-events.js | 74 ++++++++++++ workflows/wf2/workflow.json | 20 +++- 10 files changed, 207 insertions(+), 222 deletions(-) delete mode 100644 workflows/wf2/4-get-oclmap.js create mode 100644 workflows/wf2/4-get-options-map.js delete mode 100644 workflows/wf2/5-create-events.js create mode 100644 workflows/wf2/5-get-teis.js create mode 100644 workflows/wf2/6-create-events.js diff --git a/openfn-cd92dd57-9a3c-4318-bdcb-f57a386cf811-spec.yaml b/openfn-cd92dd57-9a3c-4318-bdcb-f57a386cf811-spec.yaml index 138d39c..efd73f8 100644 --- a/openfn-cd92dd57-9a3c-4318-bdcb-f57a386cf811-spec.yaml +++ b/openfn-cd92dd57-9a3c-4318-bdcb-f57a386cf811-spec.yaml @@ -9,55 +9,42 @@ workflows: name: Get Patients adaptor: '@openfn/language-openmrs@latest' credential: null - body: | - - // Check out the Job Writing Guide for help getting started: - // https://docs.openfn.org/documentation/jobs/job-writing-guide + body: + path: workflows/wf2/1-get-patients.js Upsert-TEIs: name: Upsert TEIs adaptor: '@openfn/language-dhis2@latest' credential: null - body: | - - // Check out the Job Writing Guide for help getting started: - // https://docs.openfn.org/documentation/jobs/job-writing-guide - + body: + path: workflows/wf2/2-upsert-teis.js Get-Encounters: name: Get Encounters adaptor: '@openfn/language-openmrs@latest' credential: null - body: | - - // Check out the Job Writing Guide for help getting started: - // https://docs.openfn.org/documentation/jobs/job-writing-guide + body: + path: workflows/wf2/3-get-encounters.js Get-Options-Map: name: Get Options Map adaptor: '@openfn/language-http@latest' credential: null - body: | - - // Check out the Job Writing Guide for help getting started: - // https://docs.openfn.org/documentation/jobs/job-writing-guide + body: + path: workflows/wf2/4-get-options-map.js Get-TEIs: name: Get TEIs adaptor: '@openfn/language-dhis2@latest' credential: null - body: | - - // Check out the Job Writing Guide for help getting started: - // https://docs.openfn.org/documentation/jobs/job-writing-guide + body: + path: workflows/wf2/5-get-teis.js Create-Events: name: Create Events adaptor: '@openfn/language-dhis2@latest' credential: null - body: | - - // Check out the Job Writing Guide for help getting started: - // https://docs.openfn.org/documentation/jobs/job-writing-guide + body: + path: workflows/wf2/6-create-events.js triggers: webhook: diff --git a/workflows/wf2/1-get-patients.js b/workflows/wf2/1-get-patients.js index cc8d1cb..f4f18f0 100644 --- a/workflows/wf2/1-get-patients.js +++ b/workflows/wf2/1-get-patients.js @@ -13,18 +13,18 @@ fn(state => { return state; }); -searchPatient({ q: 'Patient', v: 'full', limit: '100' }); +searchPatient({ q: 'Aisha', v: 'full', limit: '1' }); +// searchPatient({ q: 'Patient', v: 'full', limit: '100' }); //Query all patients (q=all) not supported on demo OpenMRS; needs to be configured //...so we query all Patients with name "Patient" instead fn(state => { const { results } = state.data; - const getPatientByUuid = uuid => { - return results.find(patient => patient.uuid === uuid); - }; - // console.log('dateCreated for patient uuid ...2c6dbfc5acc8',getPatientByUuid("31b4d9c8-f7cc-4c26-ae61-2c6dbfc5acc8").auditInfo.dateCreated) + const getPatientByUuid = uuid => + results.find(patient => patient.uuid === uuid).auditInfo.dateCreated; + // console.log('dateCreated for patient uuid ...2c6dbfc5acc8',getPatientByUuid("31b4d9c8-f7cc-4c26-ae61-2c6dbfc5acc8")) //console.log(JSON.stringify(state.data, null, 2)); console.log('Filtering patients to only sync most recent records...'); diff --git a/workflows/wf2/2-upsert-teis.js b/workflows/wf2/2-upsert-teis.js index db194a1..b36cb7c 100644 --- a/workflows/wf2/2-upsert-teis.js +++ b/workflows/wf2/2-upsert-teis.js @@ -31,6 +31,7 @@ fn(state => { const payload = { query: { ou: 'OPjuJMZFLop', + program: 'w9MSPn5oSqp', filter: [`AYbfTPYMNJH:Eq:${patient.uuid}`], }, data: { @@ -83,7 +84,8 @@ fn(async state => { 'trackedEntityInstances', { ou: 'OPjuJMZFLop', - filter: [`jGNhqEeXy2L:Eq:${patient.uuid}`], + filter: [`AYbfTPYMNJH:Eq:${patient.uuid}`], + program: 'w9MSPn5oSqp', }, {}, state => { @@ -106,12 +108,8 @@ fn(async state => { // Upsert TEIs to DHIS2 each( 'patientsUpsert[*]', - upsert( - 'trackedEntityInstances', - state => state.data.query, - state => state.data.data - ) + upsert('trackedEntityInstances', $.data.query, $.data.data) ); // Clean up state -fn(state => ({ ...state, data: {} })); +fn(({ data, ...state }) => state); diff --git a/workflows/wf2/3-get-encounters.js b/workflows/wf2/3-get-encounters.js index 9fec695..0fa41d6 100644 --- a/workflows/wf2/3-get-encounters.js +++ b/workflows/wf2/3-get-encounters.js @@ -1,18 +1,24 @@ // Fetch encounters from the date of cursor // OpenMRS demo instance does not support querying ALL records (q=all) -getEncounters({ q: 'Patient', v: 'full', limit: 100 }); +// getEncounters({ q: 'Patient', v: 'full', limit: 100 }); +getEncounters({ + q: 'Aisha', + v: 'full', + limit: 1, + encounterType: '95d68645-1b72-4290-be0b-ec1fb64bc067', +}); // Update cursor and return encounters fn(state => { const { cursor, data } = state; - console.log("cursor datetime::", cursor); + console.log('cursor datetime::', cursor); console.log('Filtering encounters to only get recent records...'); - console.log( - 'Encounters returned before we filter for most recent ::', - JSON.stringify(data, null, 2) - ); - const encounters = data.body.results.filter( + // console.log( + // 'Encounters returned before we filter for most recent ::', + // JSON.stringify(data, null, 2) + // ); + const encounters = data.results.filter( encounter => encounter.encounterDatetime >= cursor ); console.log('# of new encounters to sync to dhis2 ::', encounters.length); diff --git a/workflows/wf2/4-get-oclmap.js b/workflows/wf2/4-get-oclmap.js deleted file mode 100644 index da121b3..0000000 --- a/workflows/wf2/4-get-oclmap.js +++ /dev/null @@ -1,65 +0,0 @@ -// Fetch OCL mappings using ocl get() -get( - 'orgs/MSFOCG/collections/lime-demo/HEAD/expansions/autoexpand-HEAD/mappings/', - { - page: 1, - limit: 1000, - verbose: false, - fromConceptOwner: 'MSFOCG', - toConceptOwner: 'MSFOCG', - toConceptSource: 'DHIS2DataElements', - sortDesc: '_score', - lookupToConcept: true, - verbose: true, - }, - state => { - // Add state oclMappings - const oclMappings = state.data; - console.log(JSON.stringify(oclMappings, null, 2), 'OCL Mappings'); - return { ...state, data: {}, references: [], response: {}, oclMappings }; - } -); -// Job versions if using different adaptor functions -// Fetch mappings using ocl getMappings() function -// getMappings( -// 'MSFOCG', -// 'lime-demo', -// { -// page: 1, -// limit: 1000, -// verbose: false, -// fromConceptOwner: 'MSFOCG', -// toConceptOwner: 'MSFOCG', -// toConceptSource: 'DHIS2DataElements', -// sortDesc: '_score', -// }, -// state => { -// // Add state oclMappings -// const oclMappings = state.data; -// return { ...state, data: {}, references: [], response: {}, oclMappings }; -// } -// ); - -/* - * Fetching mappings using http get() - **/ -// get( -// 'orgs/MSFOCG/collections/lime-demo/HEAD/expansions/autoexpand-HEAD/mappings/', -// { -// query: { -// page: 1, -// exact_match: 'off', -// limit: 1000, -// verbose: false, -// sortDesc: '_score', -// fromConceptOwner: 'MSFOCG', -// toConceptOwner: 'MSFOCG', -// toConceptSource: 'DHIS2DataElements', -// }, -// }, -// state => { -// // Add state oclMappings -// const oclMappings = state.data; -// return { ...state, data: {}, references: [], response: {}, oclMappings }; -// } -// ); diff --git a/workflows/wf2/4-get-options-map.js b/workflows/wf2/4-get-options-map.js new file mode 100644 index 0000000..8eeec6c --- /dev/null +++ b/workflows/wf2/4-get-options-map.js @@ -0,0 +1,60 @@ +get( + 'https://gist.githubusercontent.com/aleksa-krolls/b22987f7569bc069e963973401832349/raw/ccc21979aab33e8b5caa931d648698753516011b/msf_mhBaseline_optionsMap.json' +); + +fn(state => { + state.optsMap = state.data; + // console.log(JSON.stringify(state.optsMap, null, 2), 'Options Map'); + delete state.data; + delete state.references; + delete state.response; + return state; +}); + +fn(state => { + state.mhpssMap = { + dfdv3SkeXKe: 'a6c5188c-29f0-4d3d-8cf5-7852998df86f', + hWMBCCA2yy1: 'abede172-ba87-4ebe-8054-3afadb181ea3', + TWuCY5r2wx7: 'ccc4f06c-b76a-440d-9b7e-c48ba2c4a0ab', + QHrIUMhjZlO: 'd516de07-979b-411c-b7e4-bd09cf7d9d91', + H1fMCaOzr8F: '3e97c2d0-15c1-4cfd-884f-7a4721079217', + yCwuZ0htrlH: '5f6e245c-83fc-421b-8d46-061ac773ae71', + RiiH9A53rvG: '6d3876be-0a27-466d-ad58-92edcc8c31fb', + pN4iQH4AEzk: '722dd83a-c1cf-48ad-ac99-45ac131ccc96', + qgfKPlIHjcD: 'd8c84af2-bd9b-4bf3-a815-81652cb0b0bc', + rSIazMFEBjD: '4dae5b12-070f-4153-b1ca-fbec906106e1', + qptKDiv9uPl: 'ec42d68d-3e23-43de-b8c5-a03bb538e7c7', + KSBMR1BDGwx: '1a8bf24f-4f36-4971-aad9-ae77f3525738', + WDY6MkQWyHb: '722dd83a-c1cf-48ad-ac99-45ac131ccc96', + AuDPJg6gZE7: '82978311-bef9-46f9-9a9a-cc62254b00a6', + KeyiEPc4pII: '82978311-bef9-46f9-9a9a-cc62254b00a6', + qfYPXP76j8g: 'c3c86c1b-07be-4506-ab25-8f35f4389b19', + PCGI7EnvCQS: '45b39cbf-0fb2-4682-8544-8aaf3e07a744', + RnbiVrrSFdm: 'ee1b7973-e931-494e-a9cb-22b814b4d8ed', + CUdI1BJ5W8G: '92a92f62-3ff6-4944-9ea9-a7af23949bad', + YfcNA5bvkxT: '9a8204ca-d908-4157-9285-7c970dbb5287', + vC3bg9NwJ78: '3edcfddb-7988-4ce5-97a0-d4c46b267a04', + RqsvaPH9vHt: '22809b19-54ca-4d88-8d26-9577637c184e', + qacGXlyyQOS: 'a1a75011-0fef-460a-b666-dda2d171f39b', + S22iy8o0iLg: 'aae000c3-5242-4e3c-bd1f-7e922a6d3d34', + v0qFX0qv1tX: 'd5e3d927-f7ce-4fdd-ac4e-6ad0b510b608', + SsQqwDBGxjh: '54a9b20e-bce5-4d4a-8c9c-e0248a182586', + FLIlRjAwn4G: 'e0d4e006-85b5-41cb-8a21-e013b1978b8b', + JUabDHhT1wJ: 'c1a3ed2d-6d9a-453d-9d93-749164a76413', + DlqJSA5VApl: '8fb3bb7d-c935-4b57-8444-1b953470e109', + DMaLm9u4GCq: 'b87a93ff-a4a1-4601-b35d-1e42bfa7e194', + CLGnlnFqqnk: '0a0c70d2-2ba5-4cb3-941f-b4a9a4a7ec6d', + f64XCwzJW02: '41e68dee-a2a3-4e6c-9d96-53def5caff52', + YeaUNruqmca: '08cd4b4a-4b0b-4391-987b-b5b3d770d30f', + KjgDauY9v4J: 'e08d532b-e56c-43dc-b831-af705654d2dc', + pj5hIE6iyAR: 'e08d532b-e56c-43dc-b831-af705654d2dc', + pj5hIE6iyAR: 'e08d532b-e56c-43dc-b831-af705654d2dc', + W7cPAi8iXLZ: '819f79e7-b9af-4afd-85d4-2ab677223113', + MF3RML0HLbP: 'b2c5b6e0-66f0-4b9d-8576-b6f48e0a06df', + m8qis4iUOTo: '790b41ce-e1e7-11e8-b02f-0242ac130002', + // tsFOVnlc6lz: '5f3d618e-5c89-43bd-8c79-07e4e98c2f23', + // OZViJk8FPVd: 'c2664992-8a5a-4a6d-9238-5df591307d55', + }; + + return state; +}); diff --git a/workflows/wf2/5-create-events.js b/workflows/wf2/5-create-events.js deleted file mode 100644 index 0b7bcd2..0000000 --- a/workflows/wf2/5-create-events.js +++ /dev/null @@ -1,107 +0,0 @@ -fn(state => { - const TEIs = {}; - return { ...state, TEIs }; -}); - -fn(async state => { - const { encounters } = state; - - const getTEI = async encounter => { - await new Promise(resolve => setTimeout(resolve, 2000), 'OCL Mappings'); - await get( - 'trackedEntityInstances', - { - ou: 'OPjuJMZFLop', - filter: [`AYbfTPYMNJH:Eq:${encounter.patient.uuid}`], - }, - {}, - state => { - console.log(encounter.patient.uuid, 'Encounter patient uuid'); - state.TEIs[encounter.patient.uuid] = - state.data.trackedEntityInstances[0].trackedEntityInstance; - - return state; - } - )(state); - }; - - for (const encounter of encounters) { - await getTEI(encounter); - } - return state; -}); - -// Prepare DHIS2 data model for create events -fn(state => { - const { oclMappings, TEIs } = state; - - //console.log(JSON.stringify(oclMappings, null, 2)); - - const encountersMapping = state.encounters.map(data => { - const encounterDate = data.encounterDatetime.replace('+0000', ''); - - const pluckObs = arg => data.obs.find(ob => ob.concept.uuid === arg); - //console.log('Observation ::', pluckObs); - // const pluckOcl = arg => - // oclMappings.find(ocl => ocl.from_concept_name_resolved === arg); //TODO: map using concept uid, not name - const pluckOcl = arg => - oclMappings.find(ocl => ocl.from_concept_code === arg); - //console.log('OCL code match ::', pluckOcl); - - const obs1 = pluckObs('da33d74e-33b3-495a-9d7c-aa00a-aa0160'); - const obs2 = pluckObs('da33d74e-33b3-495a-9d7c-aa00a-aa0177'); - - // const oclMap1 = obs1 && pluckOcl(obs1.value.display); - // const oclMap2 = obs2 && pluckOcl(obs2.value.display); - const cleanedObs1 = obs1.value.uuid.split('-').pop().toUpperCase(); - const cleanedObs2 = obs2.value.uuid.split('-').pop().toUpperCase(); - console.log('cleanedObs1 ', cleanedObs1); - console.log('cleanedObs2 ', cleanedObs2); - - const oclMap1 = obs1 && pluckOcl(cleanedObs1); - const oclMap2 = obs2 && pluckOcl(cleanedObs2); - console.log('oclMapping for Obs1 ', JSON.stringify(oclMap1, null, 2)); - console.log('oclMapping for Obs2 ', JSON.stringify(oclMap2, null, 2)); - - // const valueForEncounter1 = oclMap1 ? oclMap1.to_concept_name_resolved : ''; - // const valueForEncounter2 = oclMap2 ? oclMap2.to_concept_name_resolved : ''; - const valueForEncounter1 = oclMap1 - ? oclMap1.to_concept.extras.dhis2_option_code - : ''; - const valueForEncounter2 = oclMap2 - ? oclMap2.to_concept.extras.dhis2_option_code - : ''; - console.log('valueForEncounter1', valueForEncounter1); - console.log('valueForEncounter2', valueForEncounter2); - - return { - program: 'w9MSPn5oSqp', - orgUnit: 'OPjuJMZFLop', - programStage: 'EZJ9FsNau7Q', - trackedEntityInstance: TEIs[data.patient.uuid], - eventDate: encounterDate, - //=== TODO: REPLACE & ADD NEW DATAVALUES TO MAP ====================// - dataValues: [ - { - dataElement: 'ZTSBtZKc8Ff', //diagnosis - value: valueForEncounter1, - }, - { - dataElement: 'vqGFXhDM1XG', //entry triage color - value: valueForEncounter2, - }, - ], - //==================================================================// - }; - }); - return { ...state, encountersMapping }; -}); - -// Create events fore each encounter -each( - 'encountersMapping[*]', - create('events', state => state.data) //TODO: Add query parameter '/events?dataElementIdScheme=UID' -); - -// Clean up state -fn(state => ({ ...state, data: {}, references: [] })); diff --git a/workflows/wf2/5-get-teis.js b/workflows/wf2/5-get-teis.js new file mode 100644 index 0000000..82c9144 --- /dev/null +++ b/workflows/wf2/5-get-teis.js @@ -0,0 +1,24 @@ +const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); + +each( + 'encounters[*]', + get( + 'trackedEntityInstances', + { + ou: 'OPjuJMZFLop', + program: 'w9MSPn5oSqp', + filter: [`AYbfTPYMNJH:Eq:${$.data.patient.uuid}`], + }, + {}, + async state => { + const encounter = state.references.at(-1); + console.log(encounter.patient.uuid, 'Encounter patient uuid'); + state.TEIs ??= {}; + state.TEIs[encounter.patient.uuid] = + state.data.trackedEntityInstances[0].trackedEntityInstance; + + await delay(2000); + return state; + } + ) +); diff --git a/workflows/wf2/6-create-events.js b/workflows/wf2/6-create-events.js new file mode 100644 index 0000000..2daef79 --- /dev/null +++ b/workflows/wf2/6-create-events.js @@ -0,0 +1,74 @@ +// Prepare DHIS2 data model for create events +fn(state => { + const { TEIs, mhpssMap } = state; + const optsMap = JSON.parse(state.optsMap); + + const dataValuesMapping = data => { + return Object.keys(mhpssMap) + .map(k => { + let value; + const dataElement = k; + const conceptUuid = mhpssMap[k]; + const answer = data.obs.find(o => o.concept.uuid === conceptUuid); + + if (answer) { + if (typeof answer.value === 'string') { + value = answer.value; + } + if (typeof answer.value === 'object') { + if ( + answer.value.uuid === '278401ee-3d6f-4c65-9455-f1c16d0a7a98' && + conceptUuid === '722dd83a-c1cf-48ad-ac99-45ac131ccc96' + ) { + value = 'TRUE'; + } else { + value = optsMap.find( + o => o['value.uuid - External ID'] == answer?.value?.uuid + )?.['DHIS2 Option UID']; + } + } + } + if (!answer) { + value = ''; + } + return { dataElement, value }; + }) + .filter(d => d); + }; + + state.encountersMapping = state.encounters.map(data => { + const dataValues = dataValuesMapping(data); + const encounterDate = data.encounterDatetime.replace('+0000', ''); + + return { + program: 'w9MSPn5oSqp', + orgUnit: 'OPjuJMZFLop', + programStage: 'MdTtRixaC1B', + trackedEntityInstance: TEIs[data.patient.uuid], + eventDate: encounterDate, + dataValues, + }; + }); + + return state; +}); + +// Create events fore each encounter +each( + '$.encountersMapping[*]', + create( + 'events', + state => { + // console.log(state.data); + return state.data; + }, + { + params: { + dataElementIdScheme: 'UID', + }, + } + ) +); + +// Clean up state +fn(({ data, references, ...state }) => state); diff --git a/workflows/wf2/workflow.json b/workflows/wf2/workflow.json index 5350c60..9bb812c 100644 --- a/workflows/wf2/workflow.json +++ b/workflows/wf2/workflow.json @@ -28,14 +28,22 @@ "configuration": "../tmp/openmrs-creds.json", "expression": "3-get-encounters.js", "next": { - "get-oclmap": true + "get-options-map": true } }, { - "id": "get-oclmap", - "adaptor": "ocl", - "configuration": "../tmp/ocl-creds.json", - "expression": "4-get-oclmap.js", + "id": "get-options-map", + "adaptor": "http", + "expression": "4-get-options-map.js", + "next": { + "get-teis": true + } + }, + { + "id": "get-teis", + "adaptor": "dhis2", + "configuration": "../tmp/dhis2-creds.json", + "expression": "5-get-teis.js", "next": { "create-events": true } @@ -44,7 +52,7 @@ "id": "create-events", "adaptor": "dhis2", "configuration": "../tmp/dhis2-creds.json", - "expression": "5-create-events.js" + "expression": "6-create-events.js" } ] }