From 443277c9cd2ab06cc533a200e20ed32a1419b8e6 Mon Sep 17 00:00:00 2001 From: jnsereko Date: Wed, 17 Jul 2024 14:08:21 +0300 Subject: [PATCH 1/4] Add OpenFn to LIME --- OpenFn/config.json | 5 + OpenFn/openfn.env | 188 +++++++++++ OpenFn/project.yaml | 796 ++++++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 57 +++- 4 files changed, 1045 insertions(+), 1 deletion(-) create mode 100644 OpenFn/config.json create mode 100644 OpenFn/openfn.env create mode 100644 OpenFn/project.yaml diff --git a/OpenFn/config.json b/OpenFn/config.json new file mode 100644 index 0000000..b5bb34c --- /dev/null +++ b/OpenFn/config.json @@ -0,0 +1,5 @@ +{ + "endpoint": "http://localhost:4000", + "statePath": "./projectState.json", + "specPath": "./project.yaml" +} diff --git a/OpenFn/openfn.env b/OpenFn/openfn.env new file mode 100644 index 0000000..af4964a --- /dev/null +++ b/OpenFn/openfn.env @@ -0,0 +1,188 @@ +# Default values are optimized for production to avoid having to configure +# much in production. +# +# However it should be easy to get going in development too. If you see an +# uncommented option that means it's either mandatory to set or it's being +# overwritten in development to make your life easier. + +# Set this up to handle GitHub App configuration +# GITHUB_APP_ID=12345 +# GITHUB_CERT=Base64-encoded-private-key + +# Set this up to handle SalesForce OAuth credentials +# SALESFORCE_CLIENT_ID=3MVG9_ghE +# SALESFORCE_CLIENT_SECRET=703777B + +# Set this up to handle Google OAuth credentials (ex: GoogleSheets) +# GOOGLE_CLIENT_ID=660274980707 +# GOOGLE_CLIENT_SECRET=GOCSPX-ua + +# Choose an admin email address and configure a mailer. If you don't specify +# mailer details the local test adaptor will be used and mail previews can be +# viewed at localhost:4000/dev/mailbox +EMAIL_ADMIN='admin@openfn.org' +# MAILGUN_API_KEY='some-key' +# MAILGUN_DOMAIN='some-domain' + +# You should generate a random string of 64+ characters for this value in prod. +# You can generate a secure secret by running: ./run secret +SECRET_KEY_BASE=please_generate_a_more_secure_unique_secret_value_for_your_project + +# Which environment is running? MIX_ENV should be "dev" or "prod" and NODE_ENV +# should be "production" or "development". When MIX_ENV is set to prod you'll +# automatically be set to build and run releases instead of using mix. +#MIX_ENV=prod +#NODE_ENV=production +MIX_ENV=dev +NODE_ENV=development + +# Override the default log level +# Must be a valid level, see: https://hexdocs.pm/logger/1.12.3/Logger.html#module-levels +#LOG_LEVEL=debug + +# The URL that will be generated through out your app. When you combine all 3 +# values it should be the URL that visitors access in their browser / client. +#URL_SCHEME=https +#URL_HOST= +#URL_PORT=443 +URL_SCHEME=http +URL_HOST=localhost +URL_PORT=4000 + +# If you're using a CDN you can customize which URL gets used for your static +# files. If left commented out it will fall back to using your URL_HOST. +#URL_STATIC_HOST= + +# The address and bind port for the web server. +# (See: endpoint config in runtime.exs and Cowboy.) +LISTEN_ADDRESS=0.0.0.0 +PORT=4000 + +# The origins from which you want to allow requests (comma separated) +ORIGINS=//localhost:* + +# You can configure error reporting via Sentry by providing a DSN. +# SENTRY_DSN=https://some-url.ingest.sentry.io/some-id + +# ============================================================================== +# <><><> JOB EXECUTION SETTINGS <><><> + +# You can configure the max run duration for jobs in milliseconds. This should +# be lower than the pod termination grace period if using Kubernetes. +WORKER_MAX_RUN_DURATION_SECONDS=60 +WORKER_MAX_RUN_MEMORY_MB=500 +WORKER_CAPACITY=4 + +MAX_DATACLIP_SIZE_MB=10 + +# ------------------------------------------------------------------------------ + +# ============================================================================== +# <><><> DATABASE SETTINGS <><><> + +# Disable SSL connections for Postgres +# In production mode, SSL connections are enforced by default - uncomment to +# disable this behaviour. +DISABLE_DB_SSL=true + +# You you're using Docker for postgres, set POSTGRES_USER and POSTGRES_PASSWORD +# since the postgres Docker image uses them for its default database user and +# password. The database URL will be composed from these variables: +POSTGRES_USER=hello +POSTGRES_PASSWORD=password +POSTGRES_HOST=postgres +#POSTGRES_PORT=5432 +POSTGRES_DB="lightning_${MIX_ENV}" +DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT:-5432}/${POSTGRES_DB}" + +# If you're not using docker, but running postgres locally and migrating/running +# using `env $(cat .env | grep -v "#" | xargs )` set the database url directly: +# DATABASE_URL=postgres://hello:password@localhost/lightning_dev + +# ============================================================================== + +# Generate secure keys, see ./DEPLOYMENT.md +PRIMARY_ENCRYPTION_KEY=0bJ9w+hn4ebQrsCaWXuA9JY49fP9kbHmywGd5K7k+/s= +WORKER_RUNS_PRIVATE_KEY="LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb2dJQkFBS0NBUUVBeWJER3JEbFBWd05yWTA4czF3VkU5SktDSWhhditwT1ZXVSsyR2pSekFneEI5dW5CCjFrejdJQTh3SUIyZ1NseDhYWHlPZDA5QmhlMHBiRERxV2Q5YWV5OGZNb2tTMUVkcGFBT1k0YnRPYlIwbDFlM2wKazBQelBIc2lITWlFVWpTQm5yS2ZJZVJjd1VKK3NPaTQxVjYrTVV4V1FhSFBPTXRrWjRMNFdUOTVvV0paNG8wdQpQd0pXS0V0cmh3cHdoSldHeFl1Ym51TVhJdW1PTW5USFZ4cmlpdGRjODdyMDhuUTF3eDJjT3JkUkVBK25mWjVWCml6VHprUVFBTG1PNUI4V3IrS1ZYRW5HRUVHVTR2alRMYkQ5blAyclVXMXYvSFBTamgyZjlOaDlxd1RDQm04bGoKb1JBQ3BUTUhlS2VxYmpQOVgxM2V1bFp0a2hhK05kRndkeDdPZ1FJREFRQUJBb0lCQURrU2hkV2NUZ0F3WG9YMgpsSml2ekFodElOZm1sWnVSZ1pTSlF0MTlkQUhqV0JNM3FIc3N3MjhaL1NOSlh0OUw5b0U1eXRLbUljTjFEZUNvCm90Z1ZwUFB3cktKUE9YM0tTMkI4akJsc09GQVdER3ZSNnNIV1c1RUV3dTFrTEZWYXVFY2hBbmpEdHgrVTRtYkwKSStwMDZkcm5ZQTBvYll3RHVnQzBoZlF6U3diSVhaM3d6V25BVlRaZE4yTGFqRTA2UHRNSkxsSzZZZ3FyWmpObQpzeXpKSVgwRmg4WlFlOHZVMmF1U205UnIvTFhwMEN6TFpDYmxUT2RRcXlRdFQ3cHR5c2ZBUWZvZkkrMFNWcVZ2CkhqQUhaTU5ZZk9mSFJiMVBjVzFKdjZlSUJERFZiYXNCZVl5bWlQRStXVEpkOU1hbFNFSlI5NDFmaWFBQ1UwVEcKRlhLclQ4RUNnWUVBL0JaNEJvbkdONkVsQUd5akhldU42eXhka3JCUHJhUWh4am5ScEZxMGQzTjFISUdBS2pEVQpvQStEU0JzMG1NY2xRWm8yT1dHVTdZbzhwNkEzZVMyNTc2aVF6a1pndnNhS3hXcnFWTTBCRHdlOU5mdEYyVXQwCjhoczNZS1V2ek1yNEhiL1FTVFZkWldWZkR0YUdBY3kxTkFnVlRJcE9PK1FDTlk5V3p1cVVlT2tDZ1lFQXpOSVUKNm1HOGcxOUtqN1lJb3hmd1E1c3E1WGhWSk8zQ3MvVXpqSU5HKzNSRnZDWDRxd2VoTVdUY0NBVVFicnl5MGlJSApYdGpXR1B3clM5Z0JiZzdsS2VJdkdQYXRlZGhrZUNqMDJzVVp3SnZoUGowaHRXTnZWckRnb1doWG03M0xNUU1lCnRrOTdDejZta0J5RWw0VGcwMTVLL1JHK2hVLzl0aVhWRDBoUEtka0NnWUFqV0c4cDA0V0VaVWJQNFd1WmxWNkgKdStlKzJwUEJjQU1BVFRrVXgxY0liSnJlRFZaUUZCcXIrcURZcWwvY2tBZXNSQmdZUVpObEh2M1VMd1c0S3U0bwpLVVZzZHJlMzZCU3JDNHVocWtEY3Y2UUsvcGxUbDEzbFdHV1NXbmJ5U3Y4eEJLVUtycjNTcXIwQ1VwZmxock5kCmdVaWpPNzB1YnBEVXU2MWJRODdmaVFLQmdFQkhBYWRZaXNlVHBSdWFuZlZJOHU3VWlFN0JSNzh5R25OTlZTTVkKbzdNUUZ6NW5rRFZrVEpMcXV4Nk5NRTRBVEFJa0Nib2JSSDFNemUyY1dUNkgwQ1VueFc0SkpBSGtCZ3VybHNQOQpMUXJFSUpqZXFIQjdSeHFtb2FnbHpiQ2pqRnZTUmRZaTlWTmZFdmlRNm85K2RPd0FZSG94RW1CVjdTSTNsemlYCmtiaHBBb0dBYjBrVUVpanl0akZlUWJBRGYvRk92VlRVdmUxZW9PK3JuWmJ2V3NhVWhVSGRuMTdDUXc0Y1ZjK0UKbHQzWXhHVmMvNldIV3E3azB4YXBlVWJucEF4NjNIMTlNZTRjTmJFaXZSb2d4bzdHWERnRDIxbENGUHlCUmZKagpLN2g1VE1lQnRnZjhibGdrVzcxenkyWFdNWnBJRXVRT3ZCZjJqRVJuU0hYTDFrL2NObDQ9Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg" +WORKER_SECRET="dECXNlqctXJ/a+1FI4AaeLZY4Rp+Pxo23WwmJxC2xew=" + +# Start your app with RTM=false to manage your runtime manually. You might be +# doing this so that you can run `ws-worker` by hand on a local branch, rather +# than using the NPM installed version. +# RTM=false + +# Should Docker restart your containers if they go down in unexpected ways? +#DOCKER_RESTART_POLICY=unless-stopped +DOCKER_RESTART_POLICY=no + +# What health check test command do you want to run? In development, having it +# curl your web server will result in a lot of log spam, so setting it to +# /bin/true is an easy way to make the health check do basically nothing. +#DOCKER_WEB_HEALTHCHECK_TEST=curl localhost:4000/health_check +DOCKER_WEB_HEALTHCHECK_TEST=/bin/true + +# What ip:port should be published back to the Docker host for the app server? +# If you're using Docker Toolbox or a custom VM you can't use 127.0.0.1. This +# is being overwritten in dev to be compatible with more dev environments. +# +# If you have a port conflict because something else is using 4000 then you +# can either stop that process or change 4000 to be something else. +# +# Use the default in production to avoid having it directly accessible to +# the internet without assistance from a cloud based firewall. +#LIGHTNING_EXTERNAL_PORT=127.0.0.1:4000 +LIGHTNING_EXTERNAL_PORT=4000 + +# What volume path should be used? In dev we want to volume mount everything +# so we can develop our code without rebuilding our Docker images. +# Note that by mounting the whole project into the container, you will need to +# follow the Contributing steps in the README. +#LIGHTNING_VOLUME=.:/app + +# What CPU and memory constraints will be added to your services? When left at +# 0, they will happily use as much as needed. +#DOCKER_POSTGRES_CPUS=0 +#DOCKER_POSTGRES_MEMORY=0 +#DOCKER_WEB_CPUS=0 +#DOCKER_WEB_MEMORY=0 + +# Give this variable the value of true if you want the system to create a sample project for a new registered user +INIT_PROJECT_FOR_NEW_USER=false + +# If not provided, PURGE_DELETED_AFTER_DAYS defaults to 7. Set to 0 to never purge deleted records. +PURGE_DELETED_AFTER_DAYS=7 + +# To use https://plausible.io/ analytics, provide the SRC for your script and +# your data-domain below. +# PLAUSIBLE_SRC=https://plausible.io/js/script.js +# PLAUSIBLE_DATA_DOMAIN=openfn.org + +# If you wish to enable PromEx-driven Prometheus/Grafana monitoring use the following: +# PROMEX_ENABLED=true +# PROMEX_GRAFANA_HOST=http://localhost:3000 +# PROMEX_GRAFANA_USER=admin +# PROMEX_GRAFANA_PASSWORD=admin +# PROMEX_UPLOAD_GRAFANA_DASHBOARDS_ON_START=true +# PROMEX_DATASOURCE_ID=promex +# PROMEX_METRICS_ENDPOINT_AUTHORIZATION_REQUIRED=yes +# PROMEX_METRICS_ENDPOINT_TOKEN=foobar +# PROMEX_ENDPOINT_SCHEME=http + +# The length of time a Run must remain in the `available` state before it is +# considered `stalled`. +# METRICS_STALLED_RUN_THRESHOLD_SECONDS=300 + +# The maximum age of a Run that will be considered when measuring Run performance. +# METRICS_RUN_PERFORMANCE_AGE_SECONDS=120 + +# To disable the reporting of anonymised metrics to the OpenFn Usage tracker, set +# USAGE_TRACKING_ENABLED to `false`. +# USAGE_TRACKING_ENABLED=false + +# To submit cleartext UUIDs to the usage tracker (default: hashed_only), +# set USAGE_TRACKING_UUIDS=cleartext. +# USAGE_TRACKING_UUIDS=hashed_only + +# By default, impact tracking metrics will be reported to https://impact.openfn.org. Use the +# below if you wish to change that. +# USAGE_TRACKER_HOST=https://impact.openfn.org diff --git a/OpenFn/project.yaml b/OpenFn/project.yaml new file mode 100644 index 0000000..1716ce0 --- /dev/null +++ b/OpenFn/project.yaml @@ -0,0 +1,796 @@ +name: msf-lime +globals: +workflows: + Workflow-[TEST]-Get-TEIs-from-DHIS2: + name: Workflow [TEST] Get TEIs from DHIS2 + jobs: + [TEST]-Get-TEIs-from-DHIS2: + name: [TEST] Get TEIs from DHIS2 + adaptor: '@openfn/dhis2@v3.2.11' + enabled: false + globals: + body: | + get('trackedEntityInstances', { + ou: 'l22DQq4iV3G', + program: 'uGHvY5HFoLG', + programStatus: 'ACTIVE' + //filter: ['enrollmentStatus:Eq:ACTIVE'], + }, state => { + console.log('# of TEIs ::', state.data.trackedEntityInstances.length); + console.log('TEI data ::', JSON.stringify(state.data)); + return state; + }); + + + + // get('enrollments', { + // ou: 'l22DQq4iV3G', + // program: 'uGHvY5HFoLG', + // trackedEntityInstance: 'uDuQMpXynsZ' + // }, state => { + // console.log('TEI data ::', JSON.stringify(state.data)); + // return state; + // }); + + triggers: + Daily-@-6:00AM: + type: cron + cron_expression: '0 6 * * *' + edges: + Daily-@-6:00AM->[TEST]-Get-TEIs-from-DHIS2: + source_trigger: Daily-@-6:00AM + target_job: [TEST]-Get-TEIs-from-DHIS2 + condition: always + Workflow-WF2-1.-Get-patients-from-OpenMRS: + name: Workflow WF2-1. Get patients from OpenMRS + jobs: + WF2-1.-Get-patients-from-OpenMRS: + name: WF2-1. Get patients from OpenMRS + adaptor: '@openfn/openmrs@v1.1.1' + enabled: true + globals: + body: | + // here we define the date cursor + fn(state => { + //manualCursor at beggining of the project 2023-05-20T06:01:24.000+0000 + const manualCursor = '2023-07-27T07:16:24.544Z'; + + const cursor = + state.lastRunDateTime != null && state.lastRunDateTime != '' + ? state.lastRunDateTime + : manualCursor; + + console.log( + 'Date cursor to filter & get only recent OMRS records ::', + cursor + ); + + return { ...state, cursor }; + }); + + 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 { body } = state.data; + + const getPatientByUuid = (uuid) => { + return body.results.find(patient=> patient.uuid === uuid) + } + // console.log('dateCreated for patient uuid ...2c6dbfc5acc8',getPatientByUuid("31b4d9c8-f7cc-4c26-ae61-2c6dbfc5acc8").auditInfo.dateCreated) + + //console.log(JSON.stringify(state.data, null, 2)); + + + console.log('Filtering patients to only sync most recent records...'); + + const patients = body.results.filter( + patient => + (patient.auditInfo.dateChanged === null + ? patient.auditInfo.dateCreated + : patient.auditInfo.dateChanged) > state.cursor + ); + console.log('# of new patients to sync to dhis2 ::', patients.length); + // console.log(JSON.stringify(patients, null, 2)); + + const lastRunDateTime = new Date().toISOString(); + console.log('Updating cursor; next sync start date:', lastRunDateTime); + + return { ...state, data: {}, references: [], patients, lastRunDateTime }; + }); + + WF2-2.-Upsert-TEIs-in-DHIS2: + name: WF2-2. Upsert TEIs in DHIS2 + adaptor: '@openfn/dhis2@v3.2.11' + enabled: true + globals: + body: | + fn(state => { + const genderOptions = { + M: 'male', + F: 'female', + U: 'unknown', + O: 'prefer_not_to_answer', + }; + + const DHIS2_PATIENT_NUMBER = '8d79403a-c2cc-11de-8d13-0010c6dffd0f'; + const OPENMRS_AUTO_ID = '05a29f94-c0ed-11e2-94be-8c13b969e334'; + const patientsUpsert = []; + + const buildPatientsUpsert = (patient, isNewPatient) => { + const dateCreated = patient.auditInfo.dateCreated.substring(0, 10); + + const { identifier } = + patient.identifiers.find( + i => i.identifierType.uuid === DHIS2_PATIENT_NUMBER + ) || + patient.identifiers.find(i => i.identifierType.uuid === OPENMRS_AUTO_ID); + + const enrollments = [ + { + orgUnit: 'l22DQq4iV3G', + program: 'uGHvY5HFoLG', + programStage: 'hfKSeo6nZK0', + enrollmentDate: dateCreated, + }, + ]; + + const payload = { + query: { + ou: 'l22DQq4iV3G', + filter: [`jGNhqEeXy2L:Eq:${patient.uuid}`], + }, + data: { + program: 'uGHvY5HFoLG', + orgUnit: 'l22DQq4iV3G', + trackedEntityType: 'cHlzCA2MuEF', + attributes: [ + { + attribute: 'P4wdYGkldeG', + value: identifier, + }, + { + attribute: 'jGNhqEeXy2L', + value: patient.uuid, + }, + { + attribute: 'qptKDiv9uPl', + value: genderOptions[patient.person.gender], + }, + { + attribute: 'T1iX2NuPyqS', + value: patient.person.age, + }, + ], + }, + }; + + if (isNewPatient) { + console.log('create enrollmenet'); + payload.data.enrollments = enrollments; + } + + return patientsUpsert.push(payload); + }; + + return { + ...state, + genderOptions, + patientsUpsert, + buildPatientsUpsert, + }; + }); + + fn(async state => { + const { buildPatientsUpsert, patients } = state; + + const getPatient = async patient => { + await new Promise(resolve => setTimeout(resolve, 2000)); + await get( + 'trackedEntityInstances', + { + ou: 'l22DQq4iV3G', + filter: [`jGNhqEeXy2L:Eq:${patient.uuid}`], + }, + {}, + state => { + const { trackedEntityInstances } = state.data; + const isNewPatient = trackedEntityInstances.length === 0; + + buildPatientsUpsert(patient, isNewPatient); + return state; + } + )(state); + }; + + for (const patient of patients) { + console.log(patient.uuid, 'patient uuid'); + await getPatient(patient); + } + return state; + }); + + // Prepare DHIS2 data model for patients + // each( + // 'patients[*]', + // get( + // 'trackedEntityInstances', + // state => ({ + // ou: 'l22DQq4iV3G', + // filter: [`jGNhqEeXy2L:Eq:${state.data.uuid}`], + // }), + // {}, + // state => { + // const { buildPatientsUpsert, references, data } = state; + // const { trackedEntityInstances } = data; + // const patient = references[0]; + + // console.log(patient.uuid); + + // const isNewPatient = trackedEntityInstances.length === 0; + + // buildPatientsUpsert(patient, isNewPatient); + // return state; + // } + // ) + // ); + + // Upsert TEIs to DHIS2 + each( + 'patientsUpsert[*]', + upsert( + 'trackedEntityInstances', + state => state.data.query, + state => state.data.data + ) + ); + + // Clean up state + fn(state => ({ ...state, data: {} })); + + WF2-3.-Get-new-Encounters-from-OMRS: + name: WF2-3. Get new Encounters from OMRS + adaptor: '@openfn/openmrs@v1.1.1' + enabled: true + globals: + body: | + // 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 }); + + // Update cursor and return encounters + fn(state => { + const { cursor, data } = state; + 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( + encounter => encounter.encounterDatetime >= cursor + ); + console.log('# of new encounters to sync to dhis2 ::', encounters.length); + + return { ...state, data: {}, references: [], encounters }; + }); + + WF2-4.-Get-OCL-Mapping: + name: WF2-4. Get OCL Mapping + adaptor: '@openfn/ocl@v1.1.0' + enabled: true + globals: + body: | + // 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 }; + // } + // ); + + WF2-5.-Create-Events-in-DHIS2: + name: WF2-5. Create Events in DHIS2 + adaptor: '@openfn/dhis2@v3.2.11' + enabled: true + globals: + body: | + fn(state => { + const TEIs = {}; + return { ...state, TEIs }; + }); + + // Fetch TEI's for each patient + // each( + // 'encounters[*]', + // get( + // 'trackedEntityInstances', + // state => ({ + // ou: 'l22DQq4iV3G', + // filter: [`jGNhqEeXy2L:Eq:${state.data.patient.uuid}`], + // }), + // {}, + // state => { + + // const encounter = state.references[0]; + + // console.log(encounter.patient.uuid, 'patient uuid') + // console.log(state.data.trackedEntityInstances) + // state.TEIs[encounter.patient.uuid] = + // state.data.trackedEntityInstances[0].trackedEntityInstance; + + // return state; + // } + // ) + // ); + + fn(async state => { + const { encounters } = state; + + const getTEI = async encounter => { + await new Promise(resolve => setTimeout(resolve, 2000), 'OCL Mappings'); + await get( + 'trackedEntityInstances', + { + ou: 'l22DQq4iV3G', + filter: [`jGNhqEeXy2L: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: 'uGHvY5HFoLG', + orgUnit: 'l22DQq4iV3G', + programStage: 'hfKSeo6nZK0', + trackedEntityInstance: TEIs[data.patient.uuid], + eventDate: encounterDate, + 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) + ); + + // Clean up state + fn(state => ({ ...state, data: {}, references: [] })); + + triggers: + Daily-@-6:00AM: + type: cron + cron_expression: '0 6 * * *' + edges: + Daily-@-6:00AM->WF2-1.-Get-patients-from-OpenMRS: + source_trigger: Daily-@-6:00AM + target_job: WF2-1.-Get-patients-from-OpenMRS + condition: always + WF2-1.-Get-patients-from-OpenMRS->WF2-2.-Upsert-TEIs-in-DHIS2: + source_job: WF2-1.-Get-patients-from-OpenMRS + target_job: WF2-2.-Upsert-TEIs-in-DHIS2 + condition: on_job_success + WF2-2.-Upsert-TEIs-in-DHIS2->WF2-3.-Get-new-Encounters-from-OMRS: + source_job: WF2-2.-Upsert-TEIs-in-DHIS2 + target_job: WF2-3.-Get-new-Encounters-from-OMRS + condition: on_job_success + WF2-3.-Get-new-Encounters-from-OMRS->WF2-4.-Get-OCL-Mapping: + source_job: WF2-3.-Get-new-Encounters-from-OMRS + target_job: WF2-4.-Get-OCL-Mapping + condition: on_job_success + WF2-4.-Get-OCL-Mapping->WF2-5.-Create-Events-in-DHIS2: + source_job: WF2-4.-Get-OCL-Mapping + target_job: WF2-5.-Create-Events-in-DHIS2 + condition: on_job_success + Workflow-WF1-1.-Get-active-TEIs-from-DHIS2: + name: Workflow WF1-1. Get active TEIs from DHIS2 + jobs: + WF1-1.-Get-active-TEIs-from-DHIS2: + name: WF1-1. Get active TEIs from DHIS2 + adaptor: '@openfn/dhis2@v3.2.11' + enabled: true + globals: + body: | + fn(state => { + const manualCursor = '2023-06-20T17:00:00.00'; + + const cursor = + state.lastRunDateTime != null && state.lastRunDateTime != '' + ? state.lastRunDateTime + : manualCursor; + + console.log('Date cursor to filter TEI extract ::', cursor); + + return { ...state, cursor }; + }); + + // Get trackedEntityInstances that are "active" in the target program + get( + 'trackedEntityInstances', + { + ou: 'l22DQq4iV3G', + program: 'uGHvY5HFoLG', + programStatus: 'ACTIVE', + }, + {}, + state => { + const trackedEntityInstances = state.data.trackedEntityInstances.filter( + tei => tei.created > state.cursor + ); + const offset = 2; // GMT+2 (Geneva time) + const currentDateTime = new Date(); + currentDateTime.setHours(currentDateTime.getHours() + offset); + + const lastRunDateTime = currentDateTime.toISOString().replace('Z', ''); + + console.log('# of TEIs extracted ::', trackedEntityInstances.length); + console.log( + 'trackedEntityInstance IDs ::', + trackedEntityInstances.map(tei => tei.trackedEntityInstance) + ); + + console.log('Next sync start date:', lastRunDateTime); + return { + ...state, + data: {}, + references: [], + trackedEntityInstances, + lastRunDateTime, + }; + } + ); + + WF1-2.-Create-Patients-in-OpenMRS: + name: WF1-2. Create Patients in OpenMRS + adaptor: '@openfn/openmrs@v1.0.1' + enabled: true + globals: + body: | + //Define gender options and prepare newPatientUuid and identifiers + fn(state => { + const genderOptions = { + male: 'M', + female: 'F', + unknown: 'U', + //TODO: Ask MSF for updated category option values + transgender_female: 'O', + transgender_male: 'O', + Prefer_not_to_answer: 'O', + gender_variant_non_conforming: 'O', + }; + + const identifiers = []; + const newPatientUuid = []; + + const { trackedEntityInstances } = state; + if (trackedEntityInstances.length > 0) + console.log( + '# of TEIs to send to OpenMRS: ', + trackedEntityInstances.length + ); + if (trackedEntityInstances.length === 0) + console.log('No data fetched in step prior to sync.'); + + return { + ...state, + genderOptions, + newPatientUuid, + identifiers, + }; + }); + + //First we generate a unique OpenMRS ID for each patient + each('trackedEntityInstances[*]', state => { + return post( + 'idgen/identifiersource/8549f706-7e85-4c1d-9424-217d50a2988b/identifier', + {} + )(state).then(state => { + state.identifiers.push(state.data.body.identifier); + return state; + }); + }); + + // Then we map trackedEntityInstances to openMRS data model + fn(state => { + const { trackedEntityInstances, identifiers, genderOptions } = state; + + const getValueForCode = (attributes, code) => { + const result = attributes.find(attribute => attribute.code === code); + return result ? result.value : undefined; + }; + + const calculateDOB = age => { + const currentDate = new Date(); + const currentYear = currentDate.getFullYear(); + const birthYear = currentYear - age; + + const birthday = new Date( + birthYear, + currentDate.getMonth(), + currentDate.getDay() + ); + + return birthday.toISOString().replace(/\.\d+Z$/, '+0000'); + }; + + const patients = trackedEntityInstances.map((d, i) => { + const patientNumber = getValueForCode(d.attributes, 'patient_number'); // Add random number for testing + Math.random() + + return { + patientNumber: patientNumber, + identifiers: [ + { + identifier: identifiers[i], //map ID value from DHIS2 attribute + identifierType: '05a29f94-c0ed-11e2-94be-8c13b969e334', + location: '44c3efb0-2583-4c80-a79e-1f756a03c0a1', //default location + preferred: true, + }, + { + identifier: patientNumber, + identifierType: '8d79403a-c2cc-11de-8d13-0010c6dffd0f', //Old Identification number + location: '44c3efb0-2583-4c80-a79e-1f756a03c0a1', //default location + preferred: false, //default value for this identifiertype + }, + ], + person: { + gender: genderOptions[getValueForCode(d.attributes, 'sex')], + age: getValueForCode(d.attributes, 'age'), + birthdate: calculateDOB(getValueForCode(d.attributes, 'age')), + birthdateEstimated: true, + names: [ + { + familyName: patientNumber, + givenName: 'Patient', + }, + ], + }, + }; + }); + + return { ...state, patients }; + }); + + // Creating patients in openMRS + each('patients[*]', state => { + const { patientNumber, ...patient } = state.data; + + console.log('Creating patient record\n', JSON.stringify(patient, null, 2)); + + return createPatient(patient)(state).then(state => { + state.newPatientUuid.push({ + patient_number: patientNumber, + uuid: state.data.body.uuid, + }); + return state; + }); + }); + + // Clean up state + fn(state => ({ ...state, data: {}, references: [] })); + + WF1-3.-Update-DHIS2-TEIs-with-uuid: + name: WF1-3. Update DHIS2 TEIs with uuid + adaptor: '@openfn/dhis2@v3.2.11' + enabled: true + globals: + body: | + fn(state => { + if (state.newPatientUuid.length === 0) + console.log('No data fetched in step prior to sync.'); + return state; + }); + + console.log('newPatientUuid ::', JSON.stringify(state.newPatientUuid, null, 2)); + + // Update TEI on DHIS2 + each( + 'newPatientUuid[*]', + upsert( + 'trackedEntityInstances', + state => ({ + ou: 'l22DQq4iV3G', + filter: [`P4wdYGkldeG:Eq:${state.data.patient_number}`], + }), + { + orgUnit: 'l22DQq4iV3G', + program: 'uGHvY5HFoLG', + trackedEntityType: 'cHlzCA2MuEF', + attributes: [ + { + attribute: 'P4wdYGkldeG', + value: dataValue('patient_number') + //value: `${state.data.patient_number}`, + }, + { + attribute: 'jGNhqEeXy2L', + value: dataValue('uuid') + //value: `${state.data.uuid}`, + }, + ], + } + ) + ); + + triggers: + Yearly-on-Jan-1: + type: cron + cron_expression: '0 0 1 1 *' + edges: + Yearly-on-Jan-1->WF1-1.-Get-active-TEIs-from-DHIS2: + source_trigger: Yearly-on-Jan-1 + target_job: WF1-1.-Get-active-TEIs-from-DHIS2 + condition: always + WF1-1.-Get-active-TEIs-from-DHIS2->WF1-2.-Create-Patients-in-OpenMRS: + source_job: WF1-1.-Get-active-TEIs-from-DHIS2 + target_job: WF1-2.-Create-Patients-in-OpenMRS + condition: on_job_success + WF1-2.-Create-Patients-in-OpenMRS->WF1-3.-Update-DHIS2-TEIs-with-uuid: + source_job: WF1-2.-Create-Patients-in-OpenMRS + target_job: WF1-3.-Update-DHIS2-TEIs-with-uuid + condition: on_job_success + Workflow-test-http-get: + name: Workflow test http get + jobs: + test-http-get: + name: test http get + adaptor: '@openfn/dhis2@v3.2.11' + enabled: false + globals: + body: | + //get('programs', { orgUnit: 'l22DQq4iV3G', fields: '*' }); + get('https://api.openconceptlab.org/orgs/MSFOCG/collections/lime-demo/HEAD/expansions/autoexpand-HEAD/mappings', { + query: { + page: 1, + limit: 500, + verbose: false, + fromConceptOwner: 'MSFOCG', + toConceptOwner: 'MSFOCG', + toConceptSource: 'DHIS2DataElements', + sortDesc: '_score' + }, + headers: {'content-type': 'application/json'}, + }); + + fn(state => { + console.log('ocl response', JSON.stringify(state.data,null,2)); + return state; + + // }) + + triggers: + Daily-@-6:00AM: + type: cron + cron_expression: '0 6 * * *' + edges: + Daily-@-6:00AM->test-http-get: + source_trigger: Daily-@-6:00AM + target_job: test-http-get + condition: always diff --git a/docker-compose.yml b/docker-compose.yml index c659061..4c1c0ac 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -156,7 +156,62 @@ services: MYSQL_DATABASE: "zabbix" MYSQL_ROOT_PASSWORD: "root_password" + postgres: + deploy: + resources: + limits: + cpus: '${DOCKER_POSTGRES_CPUS:-0}' + memory: '${DOCKER_POSTGRES_MEMORY:-0}' + env_file: + - 'OpenFn/openfn.env' + image: 'postgres:14.2-alpine' + restart: '${DOCKER_RESTART_POLICY:-unless-stopped}' + stop_grace_period: '3s' + volumes: + - 'postgres:/var/lib/postgresql/data' + + web: + image: 'openfn/lightning:latest' + deploy: + resources: + limits: + cpus: '${DOCKER_WEB_CPUS:-0}' + memory: '${DOCKER_WEB_MEMORY:-0}' + env_file: + - 'OpenFn/openfn.env' + depends_on: + - postgres + healthcheck: + test: '${DOCKER_WEB_HEALTHCHECK_TEST:-curl localhost:4000/health_check}' + interval: '10s' + timeout: '3s' + start_period: '5s' + retries: 3 + ports: + - '${LIGHTNING_EXTERNAL_PORT:-127.0.0.1:${PORT:-4000}}:${URL_PORT:-4000}' + + worker: + image: 'openfn/ws-worker:latest' + deploy: + resources: + limits: + cpus: '${DOCKER_WORKER_CPUS:-0}' + memory: '${DOCKER_WEB_MEMORY:-0}' + depends_on: + web: + condition: service_healthy + restart: true + env_file: + - 'OpenFn/openfn.env' + command: + ['pnpm', 'start:prod', '-l', 'ws://web:${URL_PORT:-4000}/worker'] + restart: '${DOCKER_RESTART_POLICY:-unless-stopped}' + stop_grace_period: '3s' + expose: + - '2222' + volumes: openmrs-data: ~ db-data: ~ - zabbix-db-storage: ~ \ No newline at end of file + zabbix-db-storage: ~ + postgres: {} From 488ca1ff9a46058c48289561d73c204d0c021eb3 Mon Sep 17 00:00:00 2001 From: jnsereko Date: Wed, 24 Jul 2024 12:57:40 +0300 Subject: [PATCH 2/4] add command to start the openfn server and run migrations --- OpenFn/openfn.env | 3 +++ docker-compose.yml | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/OpenFn/openfn.env b/OpenFn/openfn.env index af4964a..cb4c358 100644 --- a/OpenFn/openfn.env +++ b/OpenFn/openfn.env @@ -186,3 +186,6 @@ PURGE_DELETED_AFTER_DAYS=7 # By default, impact tracking metrics will be reported to https://impact.openfn.org. Use the # below if you wish to change that. # USAGE_TRACKER_HOST=https://impact.openfn.org + +# Solving "ArgumentError" https://community.openfn.org/t/lightning-prebuilt-images-throw-no-matching-manifest-for-linux-arm64-v8-in-the-manifest-list-entries/465/15?u=jnsereko +ERL_FLAGS="+JPperf true" diff --git a/docker-compose.yml b/docker-compose.yml index 4c1c0ac..bbd113e 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -157,6 +157,7 @@ services: MYSQL_ROOT_PASSWORD: "root_password" postgres: + platform: linux/x86_64/v8 deploy: resources: limits: @@ -171,7 +172,9 @@ services: - 'postgres:/var/lib/postgresql/data' web: + platform: linux/x86_64/v8 image: 'openfn/lightning:latest' + command: /bin/sh -c "/app/bin/lightning eval Lightning.Release.migrate && /app/bin/server" deploy: resources: limits: @@ -191,6 +194,7 @@ services: - '${LIGHTNING_EXTERNAL_PORT:-127.0.0.1:${PORT:-4000}}:${URL_PORT:-4000}' worker: + platform: linux/x86_64/v8 image: 'openfn/ws-worker:latest' deploy: resources: From 7629b3ec45528043464f49e4942900dcb02b86bb Mon Sep 17 00:00:00 2001 From: jnsereko Date: Tue, 30 Jul 2024 09:52:44 +0300 Subject: [PATCH 3/4] patch project.yaml error to allow deployment using CLI --- OpenFn/config.json | 3 ++- OpenFn/project.yaml | 28 +++++++++++++++++++--------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/OpenFn/config.json b/OpenFn/config.json index b5bb34c..ded4bd9 100644 --- a/OpenFn/config.json +++ b/OpenFn/config.json @@ -1,5 +1,6 @@ { "endpoint": "http://localhost:4000", "statePath": "./projectState.json", - "specPath": "./project.yaml" + "specPath": "./project.yaml", + "apiKey": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJKb2tlbiIsImlhdCI6MTcyMjMxOTY4NiwiaXNzIjoiSm9rZW4iLCJqdGkiOiIydmplaDg4cWtka2toMW41aDAwMDA1MjIiLCJuYmYiOjE3MjIzMTk2ODYsInVzZXJfaWQiOiI4NzA2MDBmNi1jMWEzLTRmNGQtOTc5OC1hOTAyNjkwODA3MzgifQ.kTiymGqw5nH07DPrY93K7gqo3WuGPhe7vD2wsGCLl5U" } diff --git a/OpenFn/project.yaml b/OpenFn/project.yaml index 1716ce0..4ddc4e7 100644 --- a/OpenFn/project.yaml +++ b/OpenFn/project.yaml @@ -5,7 +5,7 @@ workflows: name: Workflow [TEST] Get TEIs from DHIS2 jobs: [TEST]-Get-TEIs-from-DHIS2: - name: [TEST] Get TEIs from DHIS2 + name: TEST Get TEIs from DHIS2 adaptor: '@openfn/dhis2@v3.2.11' enabled: false globals: @@ -41,11 +41,12 @@ workflows: source_trigger: Daily-@-6:00AM target_job: [TEST]-Get-TEIs-from-DHIS2 condition: always + condition_type: always Workflow-WF2-1.-Get-patients-from-OpenMRS: name: Workflow WF2-1. Get patients from OpenMRS jobs: WF2-1.-Get-patients-from-OpenMRS: - name: WF2-1. Get patients from OpenMRS + name: WF2-1 Get patients from OpenMRS adaptor: '@openfn/openmrs@v1.1.1' enabled: true globals: @@ -101,7 +102,7 @@ workflows: }); WF2-2.-Upsert-TEIs-in-DHIS2: - name: WF2-2. Upsert TEIs in DHIS2 + name: WF2-2 Upsert TEIs in DHIS2 adaptor: '@openfn/dhis2@v3.2.11' enabled: true globals: @@ -250,7 +251,7 @@ workflows: fn(state => ({ ...state, data: {} })); WF2-3.-Get-new-Encounters-from-OMRS: - name: WF2-3. Get new Encounters from OMRS + name: WF2-3 Get new Encounters from OMRS adaptor: '@openfn/openmrs@v1.1.1' enabled: true globals: @@ -278,7 +279,7 @@ workflows: }); WF2-4.-Get-OCL-Mapping: - name: WF2-4. Get OCL Mapping + name: WF2-4 Get OCL Mapping adaptor: '@openfn/ocl@v1.1.0' enabled: true globals: @@ -350,7 +351,7 @@ workflows: // ); WF2-5.-Create-Events-in-DHIS2: - name: WF2-5. Create Events in DHIS2 + name: WF2-5 Create Events in DHIS2 adaptor: '@openfn/dhis2@v3.2.11' enabled: true globals: @@ -494,27 +495,32 @@ workflows: source_trigger: Daily-@-6:00AM target_job: WF2-1.-Get-patients-from-OpenMRS condition: always + condition_type: always WF2-1.-Get-patients-from-OpenMRS->WF2-2.-Upsert-TEIs-in-DHIS2: source_job: WF2-1.-Get-patients-from-OpenMRS target_job: WF2-2.-Upsert-TEIs-in-DHIS2 condition: on_job_success + condition_type: always WF2-2.-Upsert-TEIs-in-DHIS2->WF2-3.-Get-new-Encounters-from-OMRS: source_job: WF2-2.-Upsert-TEIs-in-DHIS2 target_job: WF2-3.-Get-new-Encounters-from-OMRS condition: on_job_success + condition_type: always WF2-3.-Get-new-Encounters-from-OMRS->WF2-4.-Get-OCL-Mapping: source_job: WF2-3.-Get-new-Encounters-from-OMRS target_job: WF2-4.-Get-OCL-Mapping condition: on_job_success + condition_type: always WF2-4.-Get-OCL-Mapping->WF2-5.-Create-Events-in-DHIS2: source_job: WF2-4.-Get-OCL-Mapping target_job: WF2-5.-Create-Events-in-DHIS2 condition: on_job_success + condition_type: always Workflow-WF1-1.-Get-active-TEIs-from-DHIS2: name: Workflow WF1-1. Get active TEIs from DHIS2 jobs: WF1-1.-Get-active-TEIs-from-DHIS2: - name: WF1-1. Get active TEIs from DHIS2 + name: WF1-1 Get active TEIs from DHIS2 adaptor: '@openfn/dhis2@v3.2.11' enabled: true globals: @@ -569,7 +575,7 @@ workflows: ); WF1-2.-Create-Patients-in-OpenMRS: - name: WF1-2. Create Patients in OpenMRS + name: WF1-2 Create Patients in OpenMRS adaptor: '@openfn/openmrs@v1.0.1' enabled: true globals: @@ -697,7 +703,7 @@ workflows: fn(state => ({ ...state, data: {}, references: [] })); WF1-3.-Update-DHIS2-TEIs-with-uuid: - name: WF1-3. Update DHIS2 TEIs with uuid + name: WF1-3 Update DHIS2 TEIs with uuid adaptor: '@openfn/dhis2@v3.2.11' enabled: true globals: @@ -748,14 +754,17 @@ workflows: source_trigger: Yearly-on-Jan-1 target_job: WF1-1.-Get-active-TEIs-from-DHIS2 condition: always + condition_type: always WF1-1.-Get-active-TEIs-from-DHIS2->WF1-2.-Create-Patients-in-OpenMRS: source_job: WF1-1.-Get-active-TEIs-from-DHIS2 target_job: WF1-2.-Create-Patients-in-OpenMRS condition: on_job_success + condition_type: always WF1-2.-Create-Patients-in-OpenMRS->WF1-3.-Update-DHIS2-TEIs-with-uuid: source_job: WF1-2.-Create-Patients-in-OpenMRS target_job: WF1-3.-Update-DHIS2-TEIs-with-uuid condition: on_job_success + condition_type: always Workflow-test-http-get: name: Workflow test http get jobs: @@ -794,3 +803,4 @@ workflows: source_trigger: Daily-@-6:00AM target_job: test-http-get condition: always + condition_type: always From 5a37434bd777dc16feb3e55a24990dc3d4a84cf0 Mon Sep 17 00:00:00 2001 From: jnsereko Date: Tue, 30 Jul 2024 15:01:53 +0300 Subject: [PATCH 4/4] separate migtations service from web --- OpenFn/openfn.env | 2 +- docker-compose.yml | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/OpenFn/openfn.env b/OpenFn/openfn.env index cb4c358..0f2bdf2 100644 --- a/OpenFn/openfn.env +++ b/OpenFn/openfn.env @@ -59,7 +59,7 @@ LISTEN_ADDRESS=0.0.0.0 PORT=4000 # The origins from which you want to allow requests (comma separated) -ORIGINS=//localhost:* +ORIGINS=//localhost:4000 # You can configure error reporting via Sentry by providing a DSN. # SENTRY_DSN=https://some-url.ingest.sentry.io/some-id diff --git a/docker-compose.yml b/docker-compose.yml index bbd113e..c01b6ea 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -171,10 +171,19 @@ services: volumes: - 'postgres:/var/lib/postgresql/data' + migrations: + platform: linux/x86_64/v8 + image: 'openfn/lightning:latest' + command: ["/app/bin/lightning", "eval", "Lightning.Release.migrate"] + env_file: + - 'OpenFn/openfn.env' + depends_on: + - postgres + web: platform: linux/x86_64/v8 image: 'openfn/lightning:latest' - command: /bin/sh -c "/app/bin/lightning eval Lightning.Release.migrate && /app/bin/server" + command: ["/app/bin/server"] deploy: resources: limits: @@ -183,7 +192,11 @@ services: env_file: - 'OpenFn/openfn.env' depends_on: - - postgres + migrations: + condition: service_completed_successfully + postgres: + condition: service_started + healthcheck: test: '${DOCKER_WEB_HEALTHCHECK_TEST:-curl localhost:4000/health_check}' interval: '10s'