diff --git a/packages/dhis2/CHANGELOG.md b/packages/dhis2/CHANGELOG.md index a368a171d..0b0a7b317 100644 --- a/packages/dhis2/CHANGELOG.md +++ b/packages/dhis2/CHANGELOG.md @@ -1,5 +1,70 @@ # @openfn/language-dhis2 +## 6.0.0 + +### Major Changes + +- b44a3b1: Migrates the adaptor to the new Tracker API (v36+) for + `trackedEntities`, `enrollments`, `events` and `relationships`. Note that + `trackedEntities` is no longer used. + + This release is designed for compatibility with DHIS2 v42, which drops support + for a number of endpoints. + + The `create`, `update`, `upsert` and `destroy` functions will automatically + map affected resources to the new tracker API endpoint. + + If you have an existing workflow which uses these functions with + `trackedEntities`, `enrollments`, `events` or `relationships`, the data and + options you pass may be incompatible with the new tracker API. You should + review your code carefully against the + [DHIS2 Tracker Migration Guide](https://docs.dhis2.org/en/develop/using-the-api/dhis-core-version-241/tracker-deprecated.html#webapi_tracker_migration) + to see what's changed. + + For example, if you used to do: + + ```js + create('trackedEntityInstances', { + /*...*/ + }); + ``` + + You should now do: + + ```js + create('trackedEntities', { + /*...*/ + }); + ``` + + The payloads have also changed shape, so for example if you used to: + + ```js + create('events', { + trackedEntityInstance: 'eBAyeGv0exc', + eventDate: '2024-01-01', + /* ... */ + }); + ``` + + You should now do: + + ```js + create('events', { + trackedEntity: 'eBAyeGv0exc', + occurredAt: '2024-01-01', + /* ... */ + }); + ``` + + The HTTP APIs `get()`, `patch()`, and `post()` do not automatically map to the + new tracker: they continue to call the URL you provide with the data you send. + You can use this to continue to call the old tracker API directly. + +### Minor Changes + +- d30f39f: Added new post() operation + ## 5.0.8 ### Patch Changes diff --git a/packages/dhis2/ast.json b/packages/dhis2/ast.json index c33662197..ab0595f52 100644 --- a/packages/dhis2/ast.json +++ b/packages/dhis2/ast.json @@ -23,7 +23,7 @@ }, { "title": "param", - "description": "Type of resource to create. E.g. `trackedEntityInstances`, `programs`, `events`, ...", + "description": "Type of resource to create. E.g. `trackedEntities`, `programs`, `events`, ...", "type": { "type": "NameExpression", "name": "string" @@ -78,22 +78,22 @@ { "title": "example", "description": "create('programs', {\n name: 'name 20',\n shortName: 'n20',\n programType: 'WITHOUT_REGISTRATION',\n});", - "caption": "a program" + "caption": "Createa program" }, { "title": "example", "description": "create('events', {\n program: 'eBAyeGv0exc',\n orgUnit: 'DiszpKrYNg8',\n status: 'COMPLETED',\n});", - "caption": "an event" + "caption": "Create a single event" }, { "title": "example", - "description": "create('trackedEntityInstances', {\n orgUnit: 'TSyzvBiovKh',\n trackedEntityType: 'nEenWmSyUEp',\n attributes: [\n {\n attribute: 'w75KJ2mc4zz',\n value: 'Gigiwe',\n },\n ]\n});", - "caption": "a trackedEntityInstance" + "description": "create('trackedEntities', {\n orgUnit: 'TSyzvBiovKh',\n trackedEntityType: 'nEenWmSyUEp',\n attributes: [\n {\n attribute: 'w75KJ2mc4zz',\n value: 'Gigiwe',\n },\n ]\n});", + "caption": "Create a single tracker entity. See {@link https://docs.dhis2.org/en/develop/using-the-api/dhis-core-version-241/tracker.html#webapi_nti_import Create tracker docs}" }, { "title": "example", "description": "create('dataSets', { name: 'OpenFn Data Set', periodType: 'Monthly' });", - "caption": "a dataSet" + "caption": "Create a dataSet" }, { "title": "example", @@ -103,32 +103,37 @@ { "title": "example", "description": "create('dataElements', {\n aggregationType: 'SUM',\n domainType: 'AGGREGATE',\n valueType: 'NUMBER',\n name: 'Paracetamol',\n shortName: 'Para',\n});", - "caption": "a dataElement" + "caption": "Create a dataElement" }, { "title": "example", "description": "create('dataElementGroups', {\n name: 'Data Element Group 1',\n dataElements: [],\n});", - "caption": "a dataElementGroup" + "caption": "Create a dataElementGroup" }, { "title": "example", "description": "create('dataElementGroupSets', {\n name: 'Data Element Group Set 4',\n dataDimension: true,\n shortName: 'DEGS4',\n dataElementGroups: [],\n});", - "caption": "a dataElementGroupSet" + "caption": "Create a dataElementGroupSet" }, { "title": "example", "description": "create('dataValueSets', {\n dataElement: 'f7n9E0hX8qk',\n period: '201401',\n orgUnit: 'DiszpKrYNg8',\n value: '12',\n});", - "caption": "a dataValueSet" + "caption": "Create a dataValueSet" }, { "title": "example", "description": "create('dataValueSets', {\n dataSet: 'pBOMPrpg1QX',\n completeDate: '2014-02-03',\n period: '201401',\n orgUnit: 'DiszpKrYNg8',\n dataValues: [\n {\n dataElement: 'f7n9E0hX8qk',\n value: '1',\n },\n {\n dataElement: 'Ix2HsbDMLea',\n value: '2',\n },\n {\n dataElement: 'eY5ehpbEsB7',\n value: '3',\n },\n ],\n});", - "caption": "a dataValueSet with related dataValues" + "caption": "Create a dataValueSet with related dataValues" + }, + { + "title": "example", + "description": "create('enrollments', {\n trackedEntity: 'bmshzEacgxa',\n orgUnit: 'TSyzvBiovKh',\n program: 'gZBxv9Ujxg0',\n enrollmentDate: '2013-09-17',\n incidentDate: '2013-09-17',\n});", + "caption": "Create an enrollment" }, { "title": "example", - "description": "create('enrollments', {\n trackedEntityInstance: 'bmshzEacgxa',\n orgUnit: 'TSyzvBiovKh',\n program: 'gZBxv9Ujxg0',\n enrollmentDate: '2013-09-17',\n incidentDate: '2013-09-17',\n});", - "caption": "an enrollment" + "description": "create(\"tracker\", {\n enrollments: [\n {\n trackedEntity: \"bmshzEacgxa\",\n orgUnit: \"TSyzvBiovKh\",\n program: \"gZBxv9Ujxg0\",\n enrollmentDate: \"2013-09-17\",\n incidentDate: \"2013-09-17\",\n },\n ],\n trackedEntities: [\n {\n orgUnit: \"TSyzvBiovKh\",\n trackedEntityType: \"nEenWmSyUEp\",\n attributes: [\n {\n attribute: \"w75KJ2mc4zz\",\n value: \"Gigiwe\",\n },\n ],\n },\n ],\n});", + "caption": "Create an multiple objects with the Tracker API" } ] }, @@ -227,48 +232,48 @@ }, { "title": "example", - "description": "update('trackedEntityInstances', 'IeQfgUtGPq2', {\n created: '2015-08-06T21:12:37.256',\n orgUnit: 'TSyzvBiovKh',\n createdAtClient: '2015-08-06T21:12:37.256',\n trackedEntityInstance: 'IeQfgUtGPq2',\n lastUpdated: '2015-08-06T21:12:37.257',\n trackedEntityType: 'nEenWmSyUEp',\n inactive: false,\n deleted: false,\n featureType: 'NONE',\n programOwners: [\n {\n ownerOrgUnit: 'TSyzvBiovKh',\n program: 'IpHINAT79UW',\n trackedEntityInstance: 'IeQfgUtGPq2',\n },\n ],\n enrollments: [],\n relationships: [],\n attributes: [\n {\n lastUpdated: '2016-01-12T00:00:00.000',\n displayName: 'Last name',\n created: '2016-01-12T00:00:00.000',\n valueType: 'TEXT',\n attribute: 'zDhUuAYrxNC',\n value: 'Russell',\n },\n {\n lastUpdated: '2016-01-12T00:00:00.000',\n code: 'MMD_PER_NAM',\n displayName: 'First name',\n created: '2016-01-12T00:00:00.000',\n valueType: 'TEXT',\n attribute: 'w75KJ2mc4zz',\n value: 'Catherine',\n },\n ],\n});", - "caption": "a trackedEntityInstance" + "description": "update('trackedEntities', '', {\n createdAt: '2015-08-06T21:12:37.256',\n orgUnit: 'TSyzvBiovKh',\n createdAtClient: '2015-08-06T21:12:37.256',\n trackedEntity: 'IeQfgUtGPq2',\n trackedEntityType: 'nEenWmSyUEp',\n inactive: false,\n deleted: false,\n featureType: 'NONE',\n programOwners: [\n {\n ownerOrgUnit: 'TSyzvBiovKh',\n program: 'IpHINAT79UW',\n trackedEntity: 'IeQfgUtGPq2',\n },\n ],\n attributes: [\n {\n lastUpdated: '2016-01-12T00:00:00.000',\n displayName: 'Last name',\n created: '2016-01-12T00:00:00.000',\n valueType: 'TEXT',\n attribute: 'zDhUuAYrxNC',\n value: 'Russell',\n },\n {\n lastUpdated: '2016-01-12T00:00:00.000',\n code: 'MMD_PER_NAM',\n displayName: 'First name',\n created: '2016-01-12T00:00:00.000',\n valueType: 'TEXT',\n attribute: 'w75KJ2mc4zz',\n value: 'Catherine',\n },\n ],\n});", + "caption": "Update a tracker entity. See {@link https://docs.dhis2.org/en/develop/using-the-api/dhis-core-version-241/tracker.html#webapi_nti_import Update tracker docs}" }, { "title": "example", "description": "update('dataSets', 'lyLU2wR22tC', { name: 'OpenFN Data Set', periodType: 'Weekly' });", - "caption": "a dataSet" + "caption": "Update a dataSet" }, { "title": "example", - "description": "update('dataSetNotificationTemplates', 'VbQBwdm1wVP', {\n dataSetNotificationTrigger: 'DATA_SET_COMPLETION',\n notificationRecipient: 'ORGANISATION_UNIT_CONTACT',\n name: 'Notification',\n messageTemplate: 'Hello Updated,\n deliveryChannels: ['SMS'],\n dataSets: [],\n});", + "description": "update('dataSetNotificationTemplates', 'VbQBwdm1wVP', {\n dataSetNotificationTrigger: 'DATA_SET_COMPLETION',\n notificationRecipient: 'ORGANISATION_UNIT_CONTACT',\n name: 'Notification',\n messageTemplate: 'Hello Updated',\n deliveryChannels: ['SMS'],\n dataSets: [],\n});", "caption": "a dataSetNotification" }, { "title": "example", "description": "update('dataElements', 'FTRrcoaog83', {\n aggregationType: 'SUM',\n domainType: 'AGGREGATE',\n valueType: 'NUMBER',\n name: 'Paracetamol',\n shortName: 'Para',\n});", - "caption": "a dataElement" + "caption": "Update a dataElement" }, { "title": "example", "description": "update('dataElementGroups', 'QrprHT61XFk', {\n name: 'Data Element Group 1',\n dataElements: [],\n});", - "caption": "a dataElementGroup" + "caption": "Update a dataElementGroup" }, { "title": "example", "description": "update('dataElementGroupSets', 'VxWloRvAze8', {\n name: 'Data Element Group Set 4',\n dataDimension: true,\n shortName: 'DEGS4',\n dataElementGroups: [],\n});", - "caption": "a dataElementGroupSet" + "caption": "Update a dataElementGroupSet" }, { "title": "example", "description": "update('dataValueSets', 'AsQj6cDsUq4', {\n dataElement: 'f7n9E0hX8qk',\n period: '201401',\n orgUnit: 'DiszpKrYNg8',\n value: '12',\n});", - "caption": "a dataValueSet" + "caption": "Update a dataValueSet" }, { "title": "example", "description": "update('dataValueSets', 'Ix2HsbDMLea', {\n dataSet: 'pBOMPrpg1QX',\n completeDate: '2014-02-03',\n period: '201401',\n orgUnit: 'DiszpKrYNg8',\n dataValues: [\n {\n dataElement: 'f7n9E0hX8qk',\n value: '1',\n },\n {\n dataElement: 'Ix2HsbDMLea',\n value: '2',\n },\n {\n dataElement: 'eY5ehpbEsB7',\n value: '3',\n },\n ],\n});", - "caption": "a dataValueSet with related dataValues" + "caption": "Update a dataValueSet with related dataValues" }, { "title": "example", - "description": "update('enrollments', 'CmsHzercTBa' {\n trackedEntityInstance: 'bmshzEacgxa',\n orgUnit: 'TSyzvBiovKh',\n program: 'gZBxv9Ujxg0',\n enrollmentDate: '2013-10-17',\n incidentDate: '2013-10-17',\n});", - "caption": "a single enrollment" + "description": "update('enrollments', 'CmsHzercTBa' {\n trackedEntity: 'bmshzEacgxa',\n orgUnit: 'TSyzvBiovKh',\n program: 'gZBxv9Ujxg0',\n enrollmentDate: '2013-10-17',\n incidentDate: '2013-10-17',\n});", + "caption": "Update an enrollment given the provided ID" } ] }, @@ -283,7 +288,7 @@ "callback" ], "docs": { - "description": "Get data. Generic helper method for getting data of any kind from DHIS2.\n- This can be used to get `DataValueSets`,`events`,`trackedEntityInstances`,`etc.`", + "description": "Get data. Generic helper method for getting data of any kind from DHIS2.\n- This can be used to get `DataValueSets`,`events`,`trackers`,`etc.`", "tags": [ { "title": "public", @@ -297,7 +302,7 @@ }, { "title": "param", - "description": "The type of resource to get(use its `plural` name). E.g. `dataElements`, `trackedEntityInstances`,`organisationUnits`, etc.", + "description": "The type of resource to get(use its `plural` name). E.g. `dataElements`, `tracker/trackedEntities`,`organisationUnits`, etc.", "type": { "type": "NameExpression", "name": "string" @@ -348,22 +353,122 @@ { "title": "example", "description": "get('dataValueSets', {\n dataSet: 'pBOMPrpg1QX',\n orgUnit: 'DiszpKrYNg8',\n period: '201401',\n fields: '*',\n});", - "caption": "all data values for the 'pBOMPrpg1QX' dataset" + "caption": "Get all data values for the 'pBOMPrpg1QX' dataset" }, { "title": "example", "description": "get('programs', { orgUnit: 'TSyzvBiovKh', fields: '*' });", - "caption": "all programs for an organization unit" + "caption": "Get all programs for an organization unit" }, { "title": "example", - "description": "get('trackedEntityInstances', {\n ou: 'DiszpKrYNg8',\n filter: ['flGbXLXCrEo:Eq:124', 'w75KJ2mc4zz:Eq:John'],\n});", - "caption": "a single tracked entity instance by a unique external ID" + "description": "get('tracker/trackedEntities/F8yKM85NbxW');", + "caption": "Get a single tracked entity given the provided ID. See {@link https://docs.dhis2.org/en/develop/using-the-api/dhis-core-version-241/tracker.html#tracked-entities-get-apitrackertrackedentities TrackedEntities docs}" + }, + { + "title": "example", + "description": "get('tracker/enrollments/abcd');", + "caption": "Get an enrollment given the provided ID. See {@link https://docs.dhis2.org/en/develop/using-the-api/dhis-core-version-241/tracker.html#enrollments-get-apitrackerenrollments Enrollment docs}" + }, + { + "title": "example", + "description": "get('tracker/events');", + "caption": "Get all events matching given criteria. See {@link https://docs.dhis2.org/en/develop/using-the-api/dhis-core-version-241/tracker.html#events-get-apitrackerevents Events docs}" + }, + { + "title": "example", + "description": "get('tracker/relationships', {\n trackedEntity:['F8yKM85NbxW'],\n});", + "caption": "Get the relationship between two tracker entities. The only required parameters are 'trackedEntity', 'enrollment' or 'event'. See {@link https://docs.dhis2.org/en/develop/using-the-api/dhis-core-version-241/tracker.html#relationships-get-apitrackerrelationships Relationships docs}" } ] }, "valid": true }, + { + "name": "post", + "params": [ + "resourceType", + "data", + "query", + "options", + "callback" + ], + "docs": { + "description": "Post data. Generic helper method for posting data of any kind to DHIS2.\nThis can be used to create `DataValueSets`,`events`,`trackers`,etc.", + "tags": [ + { + "title": "public", + "description": null, + "type": null + }, + { + "title": "function", + "description": null, + "name": null + }, + { + "title": "param", + "description": "Type of resource to create. E.g. `trackedEntities`, `programs`, `events`, ...", + "type": { + "type": "NameExpression", + "name": "string" + }, + "name": "resourceType" + }, + { + "title": "magic", + "description": "resourceType $.children.resourceTypes[*]" + }, + { + "title": "param", + "description": "Object which defines data that will be used to create a given instance of resource. To create a single instance of a resource, `data` must be a javascript object, and to create multiple instances of a resources, `data` must be an array of javascript objects.", + "type": { + "type": "NameExpression", + "name": "Dhis2Data" + }, + "name": "data" + }, + { + "title": "param", + "description": "Optional `options` to define URL parameters via params (E.g. `filter`, `dimension` and other import parameters), request config (E.g. `auth`) and the DHIS2 apiVersion.", + "type": { + "type": "OptionalType", + "expression": { + "type": "NameExpression", + "name": "Object" + } + }, + "name": "options" + }, + { + "title": "param", + "description": "Optional callback to handle the response", + "type": { + "type": "OptionalType", + "expression": { + "type": "NameExpression", + "name": "function" + } + }, + "name": "callback" + }, + { + "title": "returns", + "description": "state", + "type": { + "type": "NameExpression", + "name": "Operation" + } + }, + { + "title": "example", + "description": "post(\"tracker\", {\n events: [\n {\n program: \"eBAyeGv0exc\",\n orgUnit: \"DiszpKrYNg8\",\n status: \"COMPLETED\",\n },\n ],\n});", + "caption": "Create an event" + } + ] + }, + "valid": false + }, { "name": "upsert", "params": [ @@ -388,7 +493,7 @@ }, { "title": "param", - "description": "The type of a resource to `upsert`. E.g. `trackedEntityInstances`", + "description": "The type of a resource to `upsert`. E.g. `trackedEntities`", "type": { "type": "NameExpression", "name": "string" @@ -480,8 +585,8 @@ }, { "title": "example", - "description": "upsert('trackedEntityInstances', {\n ou: 'TSyzvBiovKh',\n filter: ['w75KJ2mc4zz:Eq:Qassim'],\n}, {\n orgUnit: 'TSyzvBiovKh',\n trackedEntityType: 'nEenWmSyUEp',\n attributes: [\n {\n attribute: 'w75KJ2mc4zz',\n value: 'Qassim',\n },\n ],\n});", - "caption": "Example `expression.js` of upsert" + "description": "upsert('trackedEntities', {\n orgUnit: 'TSyzvBiovKh',\n filter: ['w75KJ2mc4zz:Eq:Qassim'],\n}, {\n orgUnit: 'TSyzvBiovKh',\n trackedEntityType: 'nEenWmSyUEp',\n attributes: [\n {\n attribute: 'w75KJ2mc4zz',\n value: 'Qassim',\n },\n ],\n});", + "caption": "Upsert a trackedEntity" } ] }, @@ -517,7 +622,7 @@ }, { "title": "param", - "description": "The path for a given endpoint. E.g. `/trackedEntityInstances` or `/dataValueSets`", + "description": "The path for a given endpoint. E.g. `/trackedEntities` or `/dataValueSets`", "type": { "type": "NameExpression", "name": "string" @@ -534,7 +639,7 @@ }, { "title": "example", - "description": "discover('post', '/trackedEntityInstances')", + "description": "discover('post', '/trackedEntities')", "caption": "a list of parameters allowed on a given endpoint for specific http method" } ] @@ -655,7 +760,7 @@ }, { "title": "param", - "description": "The type of resource to be deleted. E.g. `trackedEntityInstances`, `organisationUnits`, etc.", + "description": "The type of resource to be deleted. E.g. `trackedEntities`, `organisationUnits`, etc.", "type": { "type": "NameExpression", "name": "string" @@ -742,8 +847,8 @@ }, { "title": "example", - "description": "destroy('trackedEntityInstances', 'LcRd6Nyaq7T');", - "caption": "a tracked entity instance" + "description": "destroy('trackedEntities', 'LcRd6Nyaq7T');", + "caption": "a tracked entity instance. See {@link https://docs.dhis2.org/en/develop/using-the-api/dhis-core-version-241/tracker.html#webapi_nti_import Delete tracker docs}" } ] }, @@ -752,7 +857,7 @@ { "name": "findAttributeValue", "params": [ - "trackedEntityInstance", + "trackedEntity", "attributeDisplayName" ], "docs": { @@ -765,7 +870,7 @@ }, { "title": "example", - "description": "findAttributeValue(state.data.trackedEntityInstances[0], 'first name')" + "description": "findAttributeValue(state.data.trackedEntities[0], 'first name')" }, { "title": "function", @@ -779,7 +884,7 @@ "type": "NameExpression", "name": "Object" }, - "name": "trackedEntityInstance" + "name": "trackedEntity" }, { "title": "param", @@ -809,7 +914,7 @@ "value" ], "docs": { - "description": "Converts an attribute ID and value into a DSHI2 attribute object", + "description": "Converts an attribute ID and value into a DHIS2 attribute object", "tags": [ { "title": "public", @@ -862,7 +967,7 @@ "value" ], "docs": { - "description": "Converts a dataElement and value into a DSHI2 dataValue object", + "description": "Converts a dataElement and value into a DHIS2 dataValue object", "tags": [ { "title": "public", @@ -1473,6 +1578,92 @@ ] }, "valid": true + }, + { + "name": "cursor", + "params": [ + "value", + "options" + ], + "docs": { + "description": "Sets a cursor property on state.\nSupports natural language dates like `now`, `today`, `yesterday`, `n hours ago`, `n days ago`, and `start`,\nwhich will be converted relative to the environment (ie, the Lightning or CLI locale). Custom timezones\nare not yet supported.\nYou can provide a formatter to customise the final cursor value, which is useful for normalising\ndifferent inputs. The custom formatter runs after natural language date conversion.\nSee the usage guide at {@link https://docs.openfn.org/documentation/jobs/job-writing-guide#using-cursors}", + "tags": [ + { + "title": "public", + "description": null, + "type": null + }, + { + "title": "function", + "description": null, + "name": null + }, + { + "title": "example", + "description": "cursor($.cursor, { defaultValue: 'today' })", + "caption": "Use a cursor from state if present, or else use the default value" + }, + { + "title": "example", + "description": "cursor(22)", + "caption": "Use a pagination cursor" + }, + { + "title": "param", + "description": "the cursor value. Usually an ISO date, natural language date, or page number", + "type": { + "type": "NameExpression", + "name": "any" + }, + "name": "value" + }, + { + "title": "param", + "description": "options to control the cursor.", + "type": { + "type": "NameExpression", + "name": "object" + }, + "name": "options" + }, + { + "title": "param", + "description": "set the cursor key. Will persist through the whole run.", + "type": { + "type": "NameExpression", + "name": "string" + }, + "name": "options.key" + }, + { + "title": "param", + "description": "the value to use if value is falsy", + "type": { + "type": "NameExpression", + "name": "any" + }, + "name": "options.defaultValue" + }, + { + "title": "param", + "description": "custom formatter for the final cursor value", + "type": { + "type": "NameExpression", + "name": "Function" + }, + "name": "options.format" + }, + { + "title": "returns", + "description": null, + "type": { + "type": "NameExpression", + "name": "Operation" + } + } + ] + }, + "valid": false } ] } \ No newline at end of file diff --git a/packages/dhis2/package.json b/packages/dhis2/package.json index a40388fe1..fbf653c70 100644 --- a/packages/dhis2/package.json +++ b/packages/dhis2/package.json @@ -1,6 +1,6 @@ { "name": "@openfn/language-dhis2", - "version": "5.0.8", + "version": "6.0.0", "description": "DHIS2 Language Pack for OpenFn", "homepage": "https://docs.openfn.org", "repository": { diff --git a/packages/dhis2/src/Adaptor.js b/packages/dhis2/src/Adaptor.js index 7049723cb..b83e4618f 100644 --- a/packages/dhis2/src/Adaptor.js +++ b/packages/dhis2/src/Adaptor.js @@ -1,21 +1,19 @@ import axios from 'axios'; +import { execute as commonExecute } from '@openfn/language-common'; +import { expandReferences } from '@openfn/language-common/util'; import _ from 'lodash'; -import { - execute as commonExecute, - expandReferences, -} from '@openfn/language-common'; +const { indexOf } = _; import { CONTENT_TYPES, generateUrl, handleResponse, - nestArray, prettyJson, selectId, + shouldUseNewTracker, + ensureArray, } from './Utils'; import { request } from './Client'; -const { indexOf } = _; - /** * Execute a sequence of operations. * Wraps `language-common/execute`, and prepends initial state for DHIS2. @@ -36,10 +34,12 @@ export function execute(...operations) { return state => { const version = state.configuration?.apiVersion; - if (+version >= 42) + + if (+version < 36) { console.warn( - `WARNING: This adaptor is incompatible with DHIS2 API version 42+. See here: https://docs.dhis2.org/en/develop/using-the-api/dhis-core-version-master/tracker.html.` + `WARNING: This adaptor is INCOMPATIBLE with DHIS2 tracker API versions before v36. Some functionality may break. See https://docs.dhis2.org/en/develop/using-the-api/dhis-core-version-master/tracker.html` ); + } return commonExecute( configMigrationHelper, @@ -128,26 +128,26 @@ axios.interceptors.response.use( * Create a record * @public * @function - * @param {string} resourceType - Type of resource to create. E.g. `trackedEntityInstances`, `programs`, `events`, ... + * @param {string} resourceType - Type of resource to create. E.g. `trackedEntities`, `programs`, `events`, ... * @magic resourceType $.children.resourceTypes[*] * @param {Dhis2Data} data - Object which defines data that will be used to create a given instance of resource. To create a single instance of a resource, `data` must be a javascript object, and to create multiple instances of a resources, `data` must be an array of javascript objects. * @param {Object} [options] - Optional `options` to define URL parameters via params (E.g. `filter`, `dimension` and other import parameters), request config (E.g. `auth`) and the DHIS2 apiVersion. * @param {function} [callback] - Optional callback to handle the response * @returns {Operation} - * @example a program + * @example Create a program * create('programs', { * name: 'name 20', * shortName: 'n20', * programType: 'WITHOUT_REGISTRATION', * }); - * @example an event + * @example Create a single event * create('events', { * program: 'eBAyeGv0exc', * orgUnit: 'DiszpKrYNg8', * status: 'COMPLETED', * }); - * @example a trackedEntityInstance - * create('trackedEntityInstances', { + * @example Create a single tracker entity. See {@link https://docs.dhis2.org/en/develop/using-the-api/dhis-core-version-241/tracker.html#webapi_nti_import Create tracker docs} + * create('trackedEntities', { * orgUnit: 'TSyzvBiovKh', * trackedEntityType: 'nEenWmSyUEp', * attributes: [ @@ -157,7 +157,7 @@ axios.interceptors.response.use( * }, * ] * }); - * @example a dataSet + * @example Create a dataSet * create('dataSets', { name: 'OpenFn Data Set', periodType: 'Monthly' }); * @example a dataSetNotification * create('dataSetNotificationTemplates', { @@ -168,7 +168,7 @@ axios.interceptors.response.use( * deliveryChannels: ['SMS'], * dataSets: [], * }); - * @example a dataElement + * @example Create a dataElement * create('dataElements', { * aggregationType: 'SUM', * domainType: 'AGGREGATE', @@ -176,26 +176,26 @@ axios.interceptors.response.use( * name: 'Paracetamol', * shortName: 'Para', * }); - * @example a dataElementGroup + * @example Create a dataElementGroup * create('dataElementGroups', { * name: 'Data Element Group 1', * dataElements: [], * }); - * @example a dataElementGroupSet + * @example Create a dataElementGroupSet * create('dataElementGroupSets', { * name: 'Data Element Group Set 4', * dataDimension: true, * shortName: 'DEGS4', * dataElementGroups: [], * }); - * @example a dataValueSet + * @example Create a dataValueSet * create('dataValueSets', { * dataElement: 'f7n9E0hX8qk', * period: '201401', * orgUnit: 'DiszpKrYNg8', * value: '12', * }); - * @example a dataValueSet with related dataValues + * @example Create a dataValueSet with related dataValues * create('dataValueSets', { * dataSet: 'pBOMPrpg1QX', * completeDate: '2014-02-03', @@ -216,33 +216,69 @@ axios.interceptors.response.use( * }, * ], * }); - * @example an enrollment + * @example Create an enrollment * create('enrollments', { - * trackedEntityInstance: 'bmshzEacgxa', + * trackedEntity: 'bmshzEacgxa', * orgUnit: 'TSyzvBiovKh', * program: 'gZBxv9Ujxg0', * enrollmentDate: '2013-09-17', * incidentDate: '2013-09-17', * }); + * @example Create an multiple objects with the Tracker API + * create("tracker", { + * enrollments: [ + * { + * trackedEntity: "bmshzEacgxa", + * orgUnit: "TSyzvBiovKh", + * program: "gZBxv9Ujxg0", + * enrollmentDate: "2013-09-17", + * incidentDate: "2013-09-17", + * }, + * ], + * trackedEntities: [ + * { + * orgUnit: "TSyzvBiovKh", + * trackedEntityType: "nEenWmSyUEp", + * attributes: [ + * { + * attribute: "w75KJ2mc4zz", + * value: "Gigiwe", + * }, + * ], + * }, + * ], + * }); */ -export function create(resourceType, data, options = {}, callback = false) { +export function create(resourceType, data, options = {}, callback = s => s) { return state => { console.log(`Preparing create operation...`); - const resolvedResourceType = expandReferences(resourceType)(state); - const resolvedData = expandReferences(data)(state); - const resolvedOptions = expandReferences(options)(state); + const [resolvedResourceType, resolvedData, resolvedOptions] = + expandReferences(state, resourceType, data, options); const { params, requestConfig } = resolvedOptions; const { configuration } = state; - return request(configuration, { - method: 'post', - url: generateUrl(configuration, resolvedOptions, resolvedResourceType), - params, - data: nestArray(resolvedData, resolvedResourceType), - ...requestConfig, - }).then(result => { + let promise; + if (shouldUseNewTracker(resolvedResourceType)) { + promise = callNewTracker( + 'create', + configuration, + resolvedOptions, + resolvedResourceType, + resolvedData + ); + } else { + promise = request(configuration, { + method: 'post', + url: generateUrl(configuration, resolvedOptions, resolvedResourceType), + params, + data: resolvedData, + ...requestConfig, + }); + } + + return promise.then(result => { const details = `with response ${JSON.stringify(result.data, null, 2)}`; console.log(`Created ${resolvedResourceType} ${details}`); @@ -279,13 +315,12 @@ export function create(resourceType, data, options = {}, callback = false) { * storedBy: 'admin', * dataValues: [], * }); - * @example a trackedEntityInstance - * update('trackedEntityInstances', 'IeQfgUtGPq2', { - * created: '2015-08-06T21:12:37.256', + * @example Update a tracker entity. See {@link https://docs.dhis2.org/en/develop/using-the-api/dhis-core-version-241/tracker.html#webapi_nti_import Update tracker docs} + * update('trackedEntities', '', { + * createdAt: '2015-08-06T21:12:37.256', * orgUnit: 'TSyzvBiovKh', * createdAtClient: '2015-08-06T21:12:37.256', - * trackedEntityInstance: 'IeQfgUtGPq2', - * lastUpdated: '2015-08-06T21:12:37.257', + * trackedEntity: 'IeQfgUtGPq2', * trackedEntityType: 'nEenWmSyUEp', * inactive: false, * deleted: false, @@ -294,11 +329,9 @@ export function create(resourceType, data, options = {}, callback = false) { * { * ownerOrgUnit: 'TSyzvBiovKh', * program: 'IpHINAT79UW', - * trackedEntityInstance: 'IeQfgUtGPq2', + * trackedEntity: 'IeQfgUtGPq2', * }, * ], - * enrollments: [], - * relationships: [], * attributes: [ * { * lastUpdated: '2016-01-12T00:00:00.000', @@ -319,18 +352,18 @@ export function create(resourceType, data, options = {}, callback = false) { * }, * ], * }); - * @example a dataSet + * @example Update a dataSet * update('dataSets', 'lyLU2wR22tC', { name: 'OpenFN Data Set', periodType: 'Weekly' }); * @example a dataSetNotification * update('dataSetNotificationTemplates', 'VbQBwdm1wVP', { * dataSetNotificationTrigger: 'DATA_SET_COMPLETION', * notificationRecipient: 'ORGANISATION_UNIT_CONTACT', * name: 'Notification', - * messageTemplate: 'Hello Updated, + * messageTemplate: 'Hello Updated', * deliveryChannels: ['SMS'], * dataSets: [], * }); - * @example a dataElement + * @example Update a dataElement * update('dataElements', 'FTRrcoaog83', { * aggregationType: 'SUM', * domainType: 'AGGREGATE', @@ -338,26 +371,26 @@ export function create(resourceType, data, options = {}, callback = false) { * name: 'Paracetamol', * shortName: 'Para', * }); - * @example a dataElementGroup + * @example Update a dataElementGroup * update('dataElementGroups', 'QrprHT61XFk', { * name: 'Data Element Group 1', * dataElements: [], * }); - * @example a dataElementGroupSet + * @example Update a dataElementGroupSet * update('dataElementGroupSets', 'VxWloRvAze8', { * name: 'Data Element Group Set 4', * dataDimension: true, * shortName: 'DEGS4', * dataElementGroups: [], * }); - * @example a dataValueSet + * @example Update a dataValueSet * update('dataValueSets', 'AsQj6cDsUq4', { * dataElement: 'f7n9E0hX8qk', * period: '201401', * orgUnit: 'DiszpKrYNg8', * value: '12', * }); - * @example a dataValueSet with related dataValues + * @example Update a dataValueSet with related dataValues * update('dataValueSets', 'Ix2HsbDMLea', { * dataSet: 'pBOMPrpg1QX', * completeDate: '2014-02-03', @@ -378,9 +411,9 @@ export function create(resourceType, data, options = {}, callback = false) { * }, * ], * }); - * @example a single enrollment + * @example Update an enrollment given the provided ID * update('enrollments', 'CmsHzercTBa' { - * trackedEntityInstance: 'bmshzEacgxa', + * trackedEntity: 'bmshzEacgxa', * orgUnit: 'TSyzvBiovKh', * program: 'gZBxv9Ujxg0', * enrollmentDate: '2013-10-17', @@ -392,31 +425,42 @@ export function update( path, data, options = {}, - callback = false + callback = s => s ) { return state => { console.log(`Preparing update operation...`); - const resolvedResourceType = expandReferences(resourceType)(state); - const resolvedPath = expandReferences(path)(state); - const resolvedData = expandReferences(data)(state); - const resolvedOptions = expandReferences(options)(state); + const [resolvedResourceType, resolvedPath, resolvedData, resolvedOptions] = + expandReferences(state, resourceType, path, data, options); - const { params, requestConfig } = resolvedOptions; + const { requestConfig } = resolvedOptions; const { configuration } = state; - return request(configuration, { - method: 'put', - url: generateUrl( + let promise; + if (shouldUseNewTracker(resolvedResourceType)) { + promise = callNewTracker( + 'update', configuration, resolvedOptions, resolvedResourceType, - resolvedPath - ), - params, - data: resolvedData, - ...requestConfig, - }).then(result => { + resolvedData + ); + } else { + promise = request(configuration, { + method: 'put', + url: generateUrl( + configuration, + resolvedOptions, + resolvedResourceType, + resolvedPath + ), + options, + data: resolvedData, + ...requestConfig, + }); + } + + return promise.then(result => { console.log(`Updated ${resolvedResourceType} at ${resolvedPath}`); return handleResponse(result, state, callback); }); @@ -425,36 +469,40 @@ export function update( /** * Get data. Generic helper method for getting data of any kind from DHIS2. - * - This can be used to get `DataValueSets`,`events`,`trackedEntityInstances`,`etc.` + * - This can be used to get `DataValueSets`,`events`,`trackers`,`etc.` * @public * @function - * @param {string} resourceType - The type of resource to get(use its `plural` name). E.g. `dataElements`, `trackedEntityInstances`,`organisationUnits`, etc. + * @param {string} resourceType - The type of resource to get(use its `plural` name). E.g. `dataElements`, `tracker/trackedEntities`,`organisationUnits`, etc. * @param {Object} query - A query object that will limit what resources are retrieved when converted into request params. * @param {Object} [options] - Optional `options` to define URL parameters via params beyond filters, request configuration (e.g. `auth`) and DHIS2 api version to use. * @param {function} [callback] - Optional callback to handle the response * @returns {Operation} state - * @example all data values for the 'pBOMPrpg1QX' dataset + * @example Get all data values for the 'pBOMPrpg1QX' dataset * get('dataValueSets', { * dataSet: 'pBOMPrpg1QX', * orgUnit: 'DiszpKrYNg8', * period: '201401', * fields: '*', * }); - * @example all programs for an organization unit + * @example Get all programs for an organization unit * get('programs', { orgUnit: 'TSyzvBiovKh', fields: '*' }); - * @example a single tracked entity instance by a unique external ID - * get('trackedEntityInstances', { - * ou: 'DiszpKrYNg8', - * filter: ['flGbXLXCrEo:Eq:124', 'w75KJ2mc4zz:Eq:John'], + * @example Get a single tracked entity given the provided ID. See {@link https://docs.dhis2.org/en/develop/using-the-api/dhis-core-version-241/tracker.html#tracked-entities-get-apitrackertrackedentities TrackedEntities docs} + * get('tracker/trackedEntities/F8yKM85NbxW'); + * @example Get an enrollment given the provided ID. See {@link https://docs.dhis2.org/en/develop/using-the-api/dhis-core-version-241/tracker.html#enrollments-get-apitrackerenrollments Enrollment docs} + * get('tracker/enrollments/abcd'); + * @example Get all events matching given criteria. See {@link https://docs.dhis2.org/en/develop/using-the-api/dhis-core-version-241/tracker.html#events-get-apitrackerevents Events docs} + * get('tracker/events'); + * @example Get the relationship between two tracker entities. The only required parameters are 'trackedEntity', 'enrollment' or 'event'. See {@link https://docs.dhis2.org/en/develop/using-the-api/dhis-core-version-241/tracker.html#relationships-get-apitrackerrelationships Relationships docs} + * get('tracker/relationships', { + * trackedEntity:['F8yKM85NbxW'], * }); */ export function get(resourceType, query, options = {}, callback = false) { return state => { console.log('Preparing get operation...'); - const resolvedResourceType = expandReferences(resourceType)(state); - const resolvedQuery = expandReferences(query)(state); - const resolvedOptions = expandReferences(options)(state); + const [resolvedResourceType, resolvedQuery, resolvedOptions] = + expandReferences(state, resourceType, query, options); const { params, requestConfig } = resolvedOptions; const { configuration } = state; @@ -472,20 +520,72 @@ export function get(resourceType, query, options = {}, callback = false) { }; } +/** + * Post data. Generic helper method for posting data of any kind to DHIS2. + * This can be used to create `DataValueSets`,`events`,`trackers`,etc. + * @public + * @function + * @param {string} resourceType - Type of resource to create. E.g. `trackedEntities`, `programs`, `events`, ... + * @magic resourceType $.children.resourceTypes[*] + * @param {Dhis2Data} data - Object which defines data that will be used to create a given instance of resource. To create a single instance of a resource, `data` must be a javascript object, and to create multiple instances of a resources, `data` must be an array of javascript objects. + * @param {Object} [options] - Optional `options` to define URL parameters via params (E.g. `filter`, `dimension` and other import parameters), request config (E.g. `auth`) and the DHIS2 apiVersion. + * @param {function} [callback] - Optional callback to handle the response + * @returns {Operation} state + * @example Create an event + * post("tracker", { + * events: [ + * { + * program: "eBAyeGv0exc", + * orgUnit: "DiszpKrYNg8", + * status: "COMPLETED", + * }, + * ], + * }); + */ +export function post( + resourceType, + data, + query, + options = {}, + callback = s => s +) { + return state => { + console.log('Preparing post operation...'); + + const [resolvedResourceType, resolvedQuery, resolvedOptions, resolvedData] = + expandReferences(state, resourceType, query, options, data); + + const { params, requestConfig } = resolvedOptions; + const { configuration } = state; + + return request(configuration, { + method: 'post', + url: generateUrl(configuration, resolvedOptions, resolvedResourceType), + params: { ...resolvedQuery, ...params }, + responseType: 'json', + data: resolvedData, + ...requestConfig, + }).then(result => { + console.log(`Created ${resolvedResourceType}`); + return handleResponse(result, state, callback); + }); + }; +} + /** * Upsert a record. A generic helper function used to atomically either insert a row, or on the basis of the row already existing, UPDATE that existing row instead. * @public * @function - * @param {string} resourceType - The type of a resource to `upsert`. E.g. `trackedEntityInstances` + * @param {string} resourceType - The type of a resource to `upsert`. E.g. `trackedEntities` * @param {Object} query - A query object that allows to uniquely identify the resource to update. If no matches found, then the resource will be created. * @param {Object} data - The data to use for update or create depending on the result of the query. * @param {{ apiVersion: object, requestConfig: object, params: object }} [options] - Optional configuration that will be applied to both the `get` and the `create` or `update` operations. * @param {function} [callback] - Optional callback to handle the response * @throws {RangeError} - Throws range error * @returns {Operation} - * @example Example `expression.js` of upsert - * upsert('trackedEntityInstances', { - * ou: 'TSyzvBiovKh', + * @example Upsert a trackedEntity + * upsert('trackedEntities', { + * orgUnit: 'TSyzvBiovKh', * filter: ['w75KJ2mc4zz:Eq:Qassim'], * }, { * orgUnit: 'TSyzvBiovKh', @@ -503,19 +603,32 @@ export function upsert( query, // query supplied to the `get` data, // data supplied to the `create/update` options = {}, // options supplied to both the `get` and the `create/update` - callback = false // callback for the upsert itself. + callback = s => s // callback for the upsert itself. ) { return state => { - console.log(`Preparing upsert via 'get' then 'create' OR 'update'...`); - - // NOTE: that these parameters are all expanded by the `get`, `create`, and - // `update` functions used inside this composed "upsert" function. - return get( - resourceType, - query, - options - )(state) - .then(resp => { + const [resolvedResourceType, resolvedOptions, resolvedData] = + expandReferences(state, resourceType, options, data); + + let promise; + + if (shouldUseNewTracker(resolvedResourceType)) { + const { configuration } = state; + promise = callNewTracker( + 'create_and_update', + configuration, + resolvedOptions, + resolvedResourceType, + resolvedData + ); + } else { + // NOTE: that these parameters are all expanded by the `get`, `create`, and + // `update` functions used inside this composed "upsert" function. + console.log(`Preparing upsert via 'get' then 'create' OR 'update'...`); + promise = get( + resourceType, + query, + options + )(state).then(resp => { const resources = resp.data[resourceType]; if (resources.length > 1) { throw new RangeError( @@ -530,11 +643,13 @@ export function upsert( const path = resources[0][selectId(resourceType)]; return update(resourceType, path, data, options)(state); } - }) - .then(result => { - console.log(`Performed a "composed upsert" on ${resourceType}`); - return handleResponse(result, state, callback); }); + } + + return promise.then(result => { + console.log(`Performed a "composed upsert" on ${resourceType}`); + return handleResponse(result, state, callback); + }); }; } @@ -543,10 +658,10 @@ export function upsert( * @public * @function * @param {string} httpMethod - The HTTP to inspect parameter usage for a given endpoint, e.g., `get`, `post`,`put`,`patch`,`delete` - * @param {string} endpoint - The path for a given endpoint. E.g. `/trackedEntityInstances` or `/dataValueSets` + * @param {string} endpoint - The path for a given endpoint. E.g. `/trackedEntities` or `/dataValueSets` * @returns {Operation} * @example a list of parameters allowed on a given endpoint for specific http method - * discover('post', '/trackedEntityInstances') + * discover('post', '/trackedEntities') */ export function discover(httpMethod, endpoint) { return state => { @@ -654,15 +769,13 @@ export function patch( path, data, options = {}, - callback = false + callback = s => s ) { return state => { console.log('Preparing patch operation...'); - const resolvedResourceType = expandReferences(resourceType)(state); - const resolvedPath = expandReferences(path)(state); - const resolvedData = expandReferences(data)(state); - const resolvedOptions = expandReferences(options)(state); + const [resolvedResourceType, resolvedPath, resolvedData, resolvedOptions] = + expandReferences(state, resourceType, path, data, options); const { params, requestConfig } = resolvedOptions; const { configuration } = state; @@ -689,14 +802,14 @@ export function patch( * Delete a record. A generic helper function to delete an object * @public * @function - * @param {string} resourceType - The type of resource to be deleted. E.g. `trackedEntityInstances`, `organisationUnits`, etc. + * @param {string} resourceType - The type of resource to be deleted. E.g. `trackedEntities`, `organisationUnits`, etc. * @param {string} path - Can be an `id` of an `object` or `path` to the `nested object` to `delete`. * @param {Object} [data] - Optional. This is useful when you want to remove multiple objects from a collection in one request. You can send `data` as, for example, `{"identifiableObjects": [{"id": "IDA"}, {"id": "IDB"}, {"id": "IDC"}]}`. See more {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html#deleting-objects on DHIS2 API docs} * @param {{apiVersion: number,operationName: string,resourceType: string}} [options] - Optional `options` for `del` operation including params e.g. `{preheatCache: true, strategy: 'UPDATE', mergeMode: 'REPLACE'}`. Run `discover` or see {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html#create-update-parameters DHIS2 documentation}. Defaults to `{operationName: 'delete', apiVersion: state.configuration.apiVersion, responseType: 'json'}` * @param {function} [callback] - Optional callback to handle the response * @returns {Operation} - * @example a tracked entity instance - * destroy('trackedEntityInstances', 'LcRd6Nyaq7T'); + * @example a tracked entity instance. See {@link https://docs.dhis2.org/en/develop/using-the-api/dhis-core-version-241/tracker.html#webapi_nti_import Delete tracker docs} + * destroy('trackedEntities', 'LcRd6Nyaq7T'); */ export function destroy( resourceType, @@ -708,26 +821,37 @@ export function destroy( return state => { console.log('Preparing destroy operation...'); - const resolvedResourceType = expandReferences(resourceType)(state); - const resolvedPath = expandReferences(path)(state); - const resolvedData = expandReferences(data)(state); - const resolvedOptions = expandReferences(options)(state); + const [resolvedResourceType, resolvedPath, resolvedData, resolvedOptions] = + expandReferences(state, resourceType, path, data, options); const { params, requestConfig } = resolvedOptions; const { configuration } = state; - return request({ - method: 'delete', - url: generateUrl( + let promise; + if (shouldUseNewTracker(resolvedResourceType)) { + promise = callNewTracker( + 'delete', configuration, resolvedOptions, resolvedResourceType, - resolvedPath - ), - params, - resolvedData, - ...requestConfig, - }).then(result => { + resolvedData + ); + } else { + promise = request({ + method: 'delete', + url: generateUrl( + configuration, + resolvedOptions, + resolvedResourceType, + resolvedPath + ), + params, + resolvedData, + ...requestConfig, + }); + } + + return promise.then(result => { console.log(`Deleted ${resolvedResourceType} at ${resolvedPath}`); return handleResponse(result, state, callback); }); @@ -738,23 +862,20 @@ export function destroy( * Gets an attribute value by its case-insensitive display name * @public * @example - * findAttributeValue(state.data.trackedEntityInstances[0], 'first name') + * findAttributeValue(state.data.trackedEntities[0], 'first name') * @function - * @param {Object} trackedEntityInstance - A tracked entity instance (TEI) object + * @param {Object} trackedEntity - A tracked entity instance (TEI) object * @param {string} attributeDisplayName - The 'displayName' to search for in the TEI's attributes * @returns {string} */ -export function findAttributeValue( - trackedEntityInstance, - attributeDisplayName -) { - return trackedEntityInstance?.attributes?.find( +export function findAttributeValue(trackedEntity, attributeDisplayName) { + return trackedEntity?.attributes?.find( a => a?.displayName.toLowerCase() == attributeDisplayName.toLowerCase() )?.value; } /** - * Converts an attribute ID and value into a DSHI2 attribute object + * Converts an attribute ID and value into a DHIS2 attribute object * @public * @example * attr('w75KJ2mc4zz', 'Elias') @@ -768,7 +889,7 @@ export function attr(attribute, value) { } /** - * Converts a dataElement and value into a DSHI2 dataValue object + * Converts a dataElement and value into a DHIS2 dataValue object * @public * @example * dv('f7n9E0hX8qk', 12) @@ -781,11 +902,51 @@ export function dv(dataElement, value) { return { dataElement, value }; } +function callNewTracker( + type = 'update', + configuration, + options, + resourceType, + data = {} +) { + const { params, requestConfig, ...opts } = options; + let importStrategy; + switch (type) { + case 'create': + importStrategy = 'CREATE'; + break; + case 'update': + importStrategy = 'UPDATE'; + break; + case 'delete': + importStrategy = 'DELETE'; + break; + default: + importStrategy = 'CREATE_AND_UPDATE'; + } + + return request(configuration, { + method: 'post', + url: generateUrl( + configuration, + { + ...opts, + importStrategy, + }, + 'tracker' + ), + params: { async: false, ...params }, + data: ensureArray(data, resourceType), + ...requestConfig, + }); +} + export { alterState, dataPath, dataValue, dateFns, + cursor, each, field, fields, diff --git a/packages/dhis2/src/Utils.js b/packages/dhis2/src/Utils.js index a1a3bd5ff..53d96c3aa 100644 --- a/packages/dhis2/src/Utils.js +++ b/packages/dhis2/src/Utils.js @@ -1,5 +1,11 @@ import { composeNextState } from '@openfn/language-common'; +export function shouldUseNewTracker(resourceType) { + return /^(enrollments|relationships|events|trackedEntities)$/.test( + resourceType + ); +} + export const CONTENT_TYPES = { xml: 'application/xml', json: 'application/json', @@ -43,10 +49,8 @@ export function prettyJson(data) { return JSON.stringify(data, null, 2); } -const isArray = variable => !!variable && variable.constructor === Array; - -export function nestArray(data, key) { - return isArray(data) ? { [key]: data } : data; +export function ensureArray(data, key) { + return Array.isArray(data) ? { [key]: data } : { [key]: [data] }; } export function generateUrl(configuration, options, resourceType, path = null) { diff --git a/packages/dhis2/test/index.test.js b/packages/dhis2/test/index.test.js index 1f5aff40d..75c33eb21 100644 --- a/packages/dhis2/test/index.test.js +++ b/packages/dhis2/test/index.test.js @@ -1,7 +1,12 @@ import chai from 'chai'; import { execute, create, update, get, upsert } from '../src/Adaptor'; import { dataValue } from '@openfn/language-common'; -import { buildUrl, generateUrl, nestArray } from '../src/Utils'; +import { + buildUrl, + generateUrl, + ensureArray, + shouldUseNewTracker, +} from '../src/Utils'; import nock from 'nock'; const { expect } = chai; @@ -142,6 +147,33 @@ describe('get', () => { }); }); +describe('helperfunctions', () => { + it('should use the new tracker for enrollments', () => { + const result = shouldUseNewTracker('enrollments'); + expect(result).to.be.true; + }); + + it('should use the new tracker for events', () => { + const result = shouldUseNewTracker('events'); + expect(result).to.be.true; + }); + + it('should use the new tracker for trackedEntities', () => { + const result = shouldUseNewTracker('trackedEntities'); + expect(result).to.be.true; + }); + + it('should use the old API for dataValueSets', () => { + const result = shouldUseNewTracker('dataValueSets'); + expect(result).to.be.false; + }); + + it('should use the old API for dataElements', () => { + const result = shouldUseNewTracker('dataElements'); + expect(result).to.be.false; + }); +}); + describe('create', () => { const state = { configuration: { @@ -152,6 +184,7 @@ describe('create', () => { data: { program: 'program1', orgUnit: 'org50', + trackedEntityType: 'nEenWmSyUEp', status: 'COMPLETED', date: '02-02-20', }, @@ -159,12 +192,18 @@ describe('create', () => { it('should make an authenticated POST to the right url', async () => { testServer - .post('/api/events', { - program: 'program1', - orgUnit: 'org50', - status: 'COMPLETED', - date: '02-02-20', + .post('/api/tracker', { + events: [ + { + program: 'program1', + orgUnit: 'org50', + trackedEntityType: 'nEenWmSyUEp', + status: 'COMPLETED', + date: '02-02-20', + }, + ], }) + .query({ async: false }) .times(2) .matchHeader('authorization', 'Basic YWRtaW46ZGlzdHJpY3Q=') .reply(200, { @@ -172,7 +211,9 @@ describe('create', () => { message: 'the response', }); - const finalState = await execute(create('events', state.data))(state); + const finalState = await execute(create('events', state => state.data))( + state + ); expect(finalState.data).to.eql({ httpStatus: 'OK', @@ -182,10 +223,15 @@ describe('create', () => { it('should recursively expand references', async () => { testServer - .post('/api/events', { - program: 'abc', - orgUnit: 'org50', + .post('/api/tracker', { + events: [ + { + program: 'abc', + orgUnit: 'org50', + }, + ], }) + .query({ async: false }) .reply(200, { httpStatus: 'OK', message: 'the response', @@ -202,6 +248,85 @@ describe('create', () => { }); }); +describe('post', () => { + const state = { + configuration: { + username: 'admin', + password: 'district', + hostUrl: 'https://play.dhis2.org/2.36.4', + }, + data: { + program: 'program1', + orgUnit: 'org50', + trackedEntityType: 'nEenWmSyUEp', + status: 'COMPLETED', + date: '02-02-20', + }, + }; + + it('should make an authenticated POST to the right url', async () => { + testServer + .post('/api/tracker', { + events: [ + { + program: 'program1', + orgUnit: 'org50', + trackedEntityType: 'nEenWmSyUEp', + status: 'COMPLETED', + date: '02-02-20', + }, + ], + }) + .times(2) + .matchHeader('authorization', 'Basic YWRtaW46ZGlzdHJpY3Q=') + .reply(200, { + httpStatus: 'OK', + message: 'the response', + }); + + const finalState = await execute( + create('tracker', { events: [state.data] }) + )(state); + + expect(finalState.data).to.eql({ + httpStatus: 'OK', + message: 'the response', + }); + }); + + it('should recursively expand references', async () => { + testServer + .post('/api/tracker', { + relationships: [ + { + program: 'abc', + orgUnit: 'org50', + }, + ], + }) + .reply(200, { + httpStatus: 'OK', + message: 'the response', + }); + + const finalState = await execute( + create('tracker', { + relationships: [ + { + program: 'abc', + orgUnit: state => state.data.orgUnit, + }, + ], + }) + )(state); + + expect(finalState.data).to.eql({ + httpStatus: 'OK', + message: 'the response', + }); + }); +}); + describe('update', () => { const state = { configuration: { @@ -219,7 +344,7 @@ describe('update', () => { it('should make an authenticated PUT to the right url', async () => { testServer - .put('/api/events/qAZJCrNJK8H') + .put('/api/dataValueSets/AsQj6cDsUq4') .matchHeader('authorization', 'Basic YWRtaW46ZGlzdHJpY3Q=') .reply(200, { httpStatus: 'OK', @@ -227,7 +352,7 @@ describe('update', () => { }); const finalState = await execute( - update('events', 'qAZJCrNJK8H', state => ({ + update('dataValueSets', 'AsQj6cDsUq4', state => ({ ...state.data, date: state.data.currentDate, })) @@ -241,7 +366,7 @@ describe('update', () => { it('should recursively expand refs', async () => { testServer - .put('/api/events/qAZJCrNJK8H', { + .put('/api/dataValueSets/AsQj6cDsUq4', { program: 'program', orgUnit: 'hardcoded', date: '02-02-20', @@ -252,7 +377,7 @@ describe('update', () => { }); const finalState = await execute( - update('events', 'qAZJCrNJK8H', { + update('dataValueSets', 'AsQj6cDsUq4', { program: dataValue('program'), orgUnit: 'hardcoded', date: resp => resp.data.currentDate, @@ -281,38 +406,24 @@ describe('upsert', () => { it('should make a get and then an update if one item is found', async () => { testServer - .get( - '/api/trackedEntityInstances?ou=DiszpKrYNg8&filter=w75KJ2mc4zz:Eq:Johns&filter=zDhUuAYrxNC:Eq:Doe' - ) + .get('/api/dataValueSets?orgUnit=DiszpKrYNg8') .reply(200, { httpStatus: 'OK', message: 'the response', - trackedEntityInstances: [{ trackedEntityInstance: 123 }], + dataValueSets: [{ id: 123 }], }) - .put('/api/trackedEntityInstances/123') + .put('/api/dataValueSets/123') .reply(200, { httpStatus: 'OK', message: 'updated tei' }); const finalState = await execute( upsert( - 'trackedEntityInstances', + 'dataValueSets', { - ou: 'DiszpKrYNg8', - filter: ['w75KJ2mc4zz:Eq:Johns', 'zDhUuAYrxNC:Eq:Doe'], + orgUnit: 'DiszpKrYNg8', }, { orgUnit: 'DiszpKrYNg8', trackedEntityType: 'nEenWmSyUEp', - attributes: [ - { - lastUpdated: '2016-01-12T00:00:00.000', - code: 'MMD_PER_NAM', - displayName: 'First name', - created: '2016-01-12T00:00:00.000', - valueType: 'TEXT', - attribute: 'w75KJ2mc4zz', - value: 'Elias', - }, - ], } ) )(state); @@ -332,38 +443,24 @@ describe('upsert', () => { it('should make a get and then a create if nothing is found', async () => { testServer - .get( - '/api/trackedEntityInstances?ou=DiszpKrYNg8&filter=w75KJ2mc4zz:Eq:No&filter=zDhUuAYrxNC:Eq:One' - ) + .get('/api/dataValueSets?orgUnit=DiszpKrYNg8') .reply(200, { httpStatus: 'OK', message: 'the response', - trackedEntityInstances: [], + dataValueSets: [], }) - .post('/api/trackedEntityInstances') + .post('/api/dataValueSets') .reply(201, { httpStatus: 'OK', message: 'created tei' }); const finalState = await execute( upsert( - 'trackedEntityInstances', + 'dataValueSets', { - ou: 'DiszpKrYNg8', - filter: ['w75KJ2mc4zz:Eq:No', 'zDhUuAYrxNC:Eq:One'], + orgUnit: 'DiszpKrYNg8', }, { orgUnit: 'DiszpKrYNg8', trackedEntityType: 'nEenWmSyUEp', - attributes: [ - { - lastUpdated: '2016-01-12T00:00:00.000', - code: 'MMD_PER_NAM', - displayName: 'First name', - created: '2016-01-12T00:00:00.000', - valueType: 'TEXT', - attribute: 'w75KJ2mc4zz', - value: 'Elias', - }, - ], } ) )(state); @@ -382,19 +479,11 @@ describe('upsert', () => { }); it('should make a get and FAIL if more than one thing is found', async () => { - testServer - .get( - '/api/trackedEntityInstances?ou=DiszpKrYNg8&filter=w75KJ2mc4zz:Eq:John&filter=zDhUuAYrxNC:Eq:Doe' - ) - .reply(200, { - httpStatus: 'OK', - message: 'the response', - trackedEntityInstances: [ - { trackedEntityInstance: 1 }, - { trackedEntityInstance: 2 }, - { trackedEntityInstance: 3 }, - ], - }); + testServer.get('/api/dataValueSets?orgUnit=DiszpKrYNg8').reply(200, { + httpStatus: 'OK', + message: 'the response', + dataValueSets: [{ id: 1 }, { id: 2 }, { id: 3 }], + }); const expectThrowsAsync = async (method, errorMessage) => { let error = null; @@ -413,26 +502,69 @@ describe('upsert', () => { () => execute( upsert( - 'trackedEntityInstances', + 'dataValueSets', { - ou: 'DiszpKrYNg8', - filter: ['w75KJ2mc4zz:Eq:John', 'zDhUuAYrxNC:Eq:Doe'], + orgUnit: 'DiszpKrYNg8', }, { orgUnit: 'TSyzvBiovKh', trackedEntityType: 'nEenWmSyUEp', - attributes: [ - { - attribute: 'w75KJ2mc4zz', - value: 'Qassim', - }, - ], } ) )(state), 'Cannot upsert on Non-unique attribute. The operation found more than one records for your request.' ); }); + + it('should make a post only when new tracker is called', async () => { + testServer + .post('/api/tracker', { + events: [ + { + orgUnit: 'DiszpKrYNg8', + trackedEntityType: 'nEenWmSyUEp', + attributes: [ + { + attribute: 'w75KJ2mc4zz', + value: 'Qassim', + }, + ], + }, + ], + }) + .query({ async: false }) + .reply(200, { + httpStatus: 'OK', + message: 'created tei', + }); + + const finalState = await execute( + upsert( + 'events', + { + orgUnit: 'DiszpKrYNg8', + trackedEntities: ['F8yKM85NbxW'], + }, + [ + { + orgUnit: 'DiszpKrYNg8', + trackedEntityType: 'nEenWmSyUEp', + attributes: [ + { + attribute: 'w75KJ2mc4zz', + value: 'Qassim', + }, + ], + }, + ] + ) + )(state); + + expect(finalState.data).to.eql({ + httpStatus: 'OK', + message: 'created tei', + }); + }); }); describe('URL builders', () => { @@ -529,7 +661,7 @@ describe('URL builders', () => { }); }); -describe('nestArray', () => { +describe('ensureArray', () => { it('when an array is passed it gets nested inside that "entity" key', async () => { const state = { configuration: { @@ -541,7 +673,7 @@ describe('nestArray', () => { data: [{ a: 1 }], }; - const body = nestArray(state.data, 'events'); + const body = ensureArray(state.data, 'events'); expect(body).to.eql({ events: [{ a: 1 }] }); }); @@ -556,8 +688,8 @@ describe('nestArray', () => { data: { b: 2 }, }; - const body = nestArray(state.data, 'events'); + const body = ensureArray(state.data, 'events'); - expect(body).to.eql({ b: 2 }); + expect(body).to.eql({ events: [{ b: 2 }] }); }); }); diff --git a/packages/dhis2/test/integration.js b/packages/dhis2/test/integration.js index 684ce59a4..c13df5e1c 100644 --- a/packages/dhis2/test/integration.js +++ b/packages/dhis2/test/integration.js @@ -265,20 +265,20 @@ describe('Integration tests', () => { it('should get a single TEI based on multiple filters', async () => { const finalState = await execute( - get('trackedEntityInstances', { - program: 'IpHINAT79UW', - ou: 'DiszpKrYNg8', - filter: ['w75KJ2mc4zz:Eq:Sophia', 'zDhUuAYrxNC:Eq:Jackson'], + get('tracker/trackedEntities', { + program: 'fDd25txQckK', + orgUnit: 'DiszpKrYNg8', + filter: ['w75KJ2mc4zz:Eq:Elanor'], }) )(state); - expect(finalState.data.trackedEntityInstances.length).to.eq(1); + expect(finalState.data.instances.length).to.eq(1); const finalState2 = await execute( get('trackedEntityInstances', { - program: 'IpHINAT79UW', + program: 'fDd25txQckK', ou: 'DiszpKrYNg8', - filter: ['w75KJ2mc4zz:Eq:Sophia', 'zDhUuAYrxNC:Eq:NotJackson'], + filter: ['w75KJ2mc4zz:Eq:Elanor', 'zDhUuAYrxNC:Eq:NotJackson'], }) )(state); @@ -426,4 +426,4 @@ describe('Integration tests', () => { ); }); }); -}); +}); \ No newline at end of file