diff --git a/openfn/README.md b/openfn/README.md new file mode 100644 index 00000000..ef3c6117 --- /dev/null +++ b/openfn/README.md @@ -0,0 +1,99 @@ +# OpenFn Microservice + +A demo package using OpenFn Microservice, OpenHIM and HAPI FHIR using Instant OpenHIE. + +The `microservice` project.yaml file is located here: [`openfn/docker/config/project.yaml`](./docker/config/project.yaml). + +## Getting Started + +### Docker + +To start up the service + +```sh +yarn +yarn docker:build +yarn docker:instant init -t docker openfn +``` + +To tear down the deployment use the opposing command: + +```bash +yarn docker:instant down -t docker openfn +``` + +To start up the service after a tear down, use the following command: + +```bash +yarn docker:instant up -t docker openfn +``` + +To completely remove all package components use the following option: + +```bash +yarn docker:instant destroy -t docker openfn +``` + +### Kubernetes + +To start up the service + +```sh +yarn +yarn docker:build +yarn docker:instant init -t k8s openfn +``` + +To tear down the deployment use the opposing command: + +```bash +yarn docker:instant down -t k8s openfn +``` + +To start up the service after a tear down, use the following command: + +```bash +yarn docker:instant up -t k8s openfn +``` + +To completely remove all package components use the following option: + +```bash +yarn docker:instant destroy -t k8s openfn +``` + +## How data gets to HAPI FHIR + +Using the example payload [commcare_sample.json](./fixtures/commcare_sample.json) +we send that to the configured OpenFn Microservice. + +Microservice is configurated to run a job based on the shape of the incoming +payload _see [project.yaml](./docker/config/project.yaml)_. + +The job `commcare-to-him` will match against this message and will be invoked +performing the following actions: + +- creates a payload in the FHIR standard containing + - a Encounter resource that contains (`contained` resource field) a Patient resource +- sends the payload to OpenHIM +- which in turn sends the payload to HAPI FHIR + + +### In Docker + +* URL - `http://localhost:4001` + +### In Kubernetes + +Use the host ip or DNS, and the port `4001`. If the deployment has been done to a [minikube](https://kubernetes.io/docs/setup/learning-environment/minikube/) cluster, get the external ip and port for the `openfn-service` service by running the following command + +```sh +kubectl get services +``` + +## Notes + +- The HAPI FHIR service runs on port `3447` +- The OpenHIM channel that we go through is on port `5001`. + The API is identical, with the exception of a required `Authorization` header. + diff --git a/openfn/docker/compose.sh b/openfn/docker/compose.sh new file mode 100755 index 00000000..3a48c3a8 --- /dev/null +++ b/openfn/docker/compose.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +composeFilePath=$(dirname $(readlink -f $0)) + +# Create a volume and copy the project configuration in before starting the stack. +docker volume create microservice_him_hapi +docker container create --name dummy -v microservice_him_hapi:/mnt tianon/true +docker cp $composeFilePath/config/project.yaml dummy:/mnt/ +docker rm dummy + +if [ "$1" == "init" ]; then + docker-compose -p openfn \ + -f "$composeFilePath"/docker-compose.yml \ + -f "$composeFilePath"/docker-compose.dev.yml \ + -f "$composeFilePath"/docker-compose.config.yml \ + up -d +elif [ "$1" == "up" ]; then + docker-compose -p openfn \ + -f "$composeFilePath"/docker-compose.yml \ + -f "$composeFilePath"/docker-compose.dev.yml \ + -f "$composeFilePath"/docker-compose.config.yml \ + up -d +elif [ "$1" == "down" ]; then + docker-compose -p openfn \ + -f "$composeFilePath"/docker-compose.yml \ + -f "$composeFilePath"/docker-compose.dev.yml \ + -f "$composeFilePath"/docker-compose.config.yml \ + stop +elif [ "$1" == "destroy" ]; then + docker-compose -p openfn \ + -f "$composeFilePath"/docker-compose.yml \ + -f "$composeFilePath"/docker-compose.dev.yml \ + -f "$composeFilePath"/docker-compose.config.yml \ + down +else + echo "Valid options are: init, up, down, or destroy" +fi diff --git a/openfn/docker/config/project.yaml b/openfn/docker/config/project.yaml new file mode 100644 index 00000000..7c9eecfd --- /dev/null +++ b/openfn/docker/config/project.yaml @@ -0,0 +1,167 @@ +jobs: + create-patient: + expression: > + post('Patient', { + body: dataValue('$'), + headers: { + 'Content-Type': 'application/json', + Authorization: 'Custom test', + 'If-None-Exist': state => + `identifier=${dataValue('identifier[0].value')(state)}`, + }, + }); + adaptor: '@openfn/language-http' + trigger: patient-message + credential: openhim + + create-organization: + expression: > + post('Organization', { + body: dataValue('$'), + headers: { + 'Content-Type': 'application/json', + Authorization: 'Custom test', + 'If-None-Exist': state => + `identifier=${dataValue('identifier[0].value')(state)}`, + }, + }); + adaptor: '@openfn/language-http' + trigger: organization-message + credential: openhim + + commcare-to-openhim: + expression: > + post("Patient", { + body: fields( + field("resourceType", "Patient"), + field("id", dataValue("$form.patient_data.patient_id")), + field("identifier", [ + { + use: "usual", + type: { + coding: [ + { + system: "http://terminology.hl7.org/CodeSystem/v2-0203", + code: "MR", + }, + ], + }, + system: "urn:oid:0.1.2.3.4.5.6.7", + value: dataValue("$form.patient_data.patient_id"), + }, + ]), + field("name", [ + { + use: "official", + family: dataValue("$form.patient_data.age.last_name"), + given: [dataValue("$form.patient_data.age.first_name")], + }, + ]), + field("gender", dataValue("$form.patient_data.age.gender")), + field("birthDate", dataValue("$form.patient_data.age.birthDate")) + ), + headers: { + "Content-Type": "application/json", + Authorization: "Custom test", + "If-None-Exist": (state) => + `identifier=${dataValue("$form.patient_data.patient_id")(state)}`, + }, + }); + + post("Encounter", { + body: fields( + field("resourceType", "Encounter"), + field("status", "finished"), + field( + "subject", + field("reference", (state) => `Patient/${dataValue("id")(state)}`) + ) + ), + headers: { + "Content-Type": "application/json", + Authorization: "Custom test", + }, + }); + adaptor: '@openfn/language-http' + trigger: commcare-message + credential: openhim + + kobo-to-openhim: + expression: > + post("Patient", { + body: fields( + field("resourceType", "Patient"), + field("id", dataValue("$body.Godata_ID")), + field("identifier", [ + { + use: "usual", + type: { + coding: [ + { + system: "http://terminology.hl7.org/CodeSystem/v2-0203", + code: "MR", + }, + ], + }, + system: "urn:oid:0.1.2.3.4.5.6.7", + value: dataValue("$body.Godata_ID"), + }, + ]), + field("name", [ + { + use: "official", + text: dataValue("$body.Patient_name"), + }, + ]), + field("gender", dataValue("$body.Sex")), + field("birthDate", (state) => { + const year = + Intl.DateTimeFormat("en", { year: "numeric" }).format() - + Number(dataValue("$body.Age_in_year")(state)); + return `${year}-01-01`; + }) + ), + headers: { + "Content-Type": "application/json", + Authorization: "Custom test", + "If-None-Exist": (state) => + `identifier=${dataValue("$body.Godata_ID")(state)}`, + }, + }); + + post("Encounter", { + body: fields( + field("resourceType", "Encounter"), + field("status", "finished"), + field( + "subject", + field( + "reference", + (state) => `Patient/${dataValue("id")(state)}` + ) + ) + ), + headers: { + "Content-Type": "application/json", + Authorization: "Custom test", + }, + }); + + adaptor: '@openfn/language-http' + trigger: kobo-message + credential: openhim + + +triggers: + patient-message: + criteria: '{"resourceType":"Patient"}' + organization-message: + criteria: '{"resourceType":"Organization"}' + kobo-message: + criteria: '{"form":"covid19-registration"}' + commcare-message: + criteria: '{"app_id":"aa"}' + +credentials: + openhim: + baseUrl: "http://openhim-core:5001/fhir/" \ No newline at end of file diff --git a/openfn/docker/docker-compose.config.yml b/openfn/docker/docker-compose.config.yml new file mode 100644 index 00000000..7c8d76c6 --- /dev/null +++ b/openfn/docker/docker-compose.config.yml @@ -0,0 +1,18 @@ +version: "3.3" + +services: + openfn: + environment: + PROJECT_DIR: /opt/app/project + # MIX_ENV: prod + volumes: + - microservice_him_hapi:/opt/app/project + +volumes: + microservice_him_hapi: + external: true + +networks: + default: + external: + name: instant_default \ No newline at end of file diff --git a/openfn/docker/docker-compose.dev.yml b/openfn/docker/docker-compose.dev.yml new file mode 100644 index 00000000..9a683b0f --- /dev/null +++ b/openfn/docker/docker-compose.dev.yml @@ -0,0 +1,6 @@ +version: '3.3' + +services: + openfn: + ports: + - '4001:4001' \ No newline at end of file diff --git a/openfn/docker/docker-compose.yml b/openfn/docker/docker-compose.yml new file mode 100644 index 00000000..2cc78864 --- /dev/null +++ b/openfn/docker/docker-compose.yml @@ -0,0 +1,10 @@ +version: '3.3' + +services: + openfn: + image: openfn/microservice:v0.3.3 + container_name: openfn_him_hapi + environment: + HOST_URL: localhost + PORT: 4001 + ENDPOINT_STYLE: async \ No newline at end of file diff --git a/openfn/fixtures/commcare_sample.json b/openfn/fixtures/commcare_sample.json new file mode 100644 index 00000000..254f9a39 --- /dev/null +++ b/openfn/fixtures/commcare_sample.json @@ -0,0 +1,43 @@ +{ + "app_id": "aa", + "archived": false, + "attachments": { + "form.xml": { + "content_type": "text/xml", + "length": 11528, + "url": "https://www.commcarehq.org/a/x.xml" + } + }, + "form": { + "#type": "data", + "@name": "Register New Patient", + "@uiVersion": "1", + "@version": "1", + "patient_data": { + "age": { + "age_at_registration_days": "181", + "gender": "female", + "birthDate": "2020-10-11", + "first_name": "test", + "last_name": "case" + }, + "locations": { + "city": "k", + "clinic_code": "test_clinic", + "country": "Kenya" + }, + "patient_id": "test_clinic-0009-01" + }, + "case": { + "@case_id": "xxx", + "@date_modified": "2021-01-21T07:08:19.431000Z", + "@user_id": "aaa", + "@xmlns": "http://commcarehq.org/case/transaction/v2", + "create": { + "case_name": "test case", + "case_type": "patient", + "owner_id": "aaa" + } + } +} +} \ No newline at end of file diff --git a/openfn/fixtures/koboCaseRegistration.json b/openfn/fixtures/koboCaseRegistration.json new file mode 100644 index 00000000..b00f890b --- /dev/null +++ b/openfn/fixtures/koboCaseRegistration.json @@ -0,0 +1,45 @@ +{ + "__query_params": {}, + "body": { + "Age_in_year": "32", + "Case_ID": "PVT6546163", + "Covid_19_suspected_criteria/HF_visited": "no", + "Covid_19_suspected_criteria/Specimen_Collected": "not_collected", + "Covid_19_suspected_criteria/Symptoms": "difficulty_breathing", + "Covid_19_suspected_criteria/Travel_history": "no", + "Covid_19_suspected_criteria/contact_with_case": "no", + "Covid_19_suspected_criteria/risk_factors": "asthma", + "Date": "2020-12-20", + "Godata_ID": "C19-930020123", + "HF_Name": "375", + "Patient_name": "Aleksa Krolls", + "Sample_Classification": "n_id", + "Sex": "female", + "UID": "375", + "__version__": "vkHKMmRAxMM5TbxYeJxRrZ", + "_attachments": [], + "_geolocation": [ null, null ], + "_id": 145623988, + "_notes": [], + "_status": "submitted_via_web", + "_submission_time": "2020-12-15T14:56:25", + "_submitted_by": null, + "_tags": [], + "_uuid": "5e6fbde5-27ce-4829-8615-546c40aaec61", + "_validation_status": {}, + "_version_": "vDV7SMYHxkcCmzQSGFVSUS", + "_version__001": "vyWA2rz4Lz2ee5hMH6xEax", + "_version__002": "vzMPKoBxnNGuXLgUkzcoE6", + "_version__003": "vGxbxgVz3DRChZXUJuAFjz", + "_xform_id_string": "afSehzJiWwktDGYucGJq2X", + "end": "2020-12-15T09:56:14.066-05:00", + "formhub/uuid": "4778c297728b4303807a8647038c5011", + "meta/instanceID": "uuid:5e6fbde5-27ce-4829-8615-546c40aaec61", + "patient_address/Nationality": "fdmn", + "patient_address/Upazilla": "teknaf", + "patient_address/teknaf_Camp": "camp_23", + "sample_type_001": "ari_ili", + "start": "2020-12-15T09:49:44.452-05:00" + }, + "form": "covid19-registration" +} diff --git a/openfn/fixtures/organization.json b/openfn/fixtures/organization.json new file mode 100644 index 00000000..7eef6626 --- /dev/null +++ b/openfn/fixtures/organization.json @@ -0,0 +1,27 @@ +{ + "resourceType": "Organization", + "id": "1", + "text": { + "status": "generated", + "div": "
Gastroenterology @ Acme Hospital. ph: +1 555 234 3523, email: \n gastro@acme.org\n
\n \n| Name | \n\t\t\t\t\t\tPeter James \n Chalmers ("Jim")\n | \n\t\t\t\t\t
| Address | \n\t\t\t\t\t\t534 Erewhon, Pleasantville, Vic, 3999 | \n\t\t\t\t\t
| Contacts | \n\t\t\t\t\t\tHome: unknown. Work: (03) 5555 6473 | \n\t\t\t\t\t
| Id | \n\t\t\t\t\t\tMRN: 12345 (Acme Healthcare) | \n\t\t\t\t\t