From 1473668a525b4ddf3a2980b6b40a77f7e5e93a67 Mon Sep 17 00:00:00 2001 From: "Elias W. BA" Date: Wed, 8 Dec 2021 13:11:17 +0000 Subject: [PATCH 01/26] Refactor and test: create, update, get and upsert --- .babelrc | 11 +- ast.json | 2773 +++++-------------------------------------- lib/Adaptor.js | 1907 ++++++++--------------------- lib/Client.js | 35 + lib/Utils.js | 213 +++- lib/index.js | 10 +- package.json | 5 +- src/Adaptor.js | 2009 +++++++------------------------ src/Client.js | 10 + src/Utils.js | 98 +- test/index.js | 915 +++----------- test/integration.js | 484 ++++++++ 12 files changed, 2100 insertions(+), 6370 deletions(-) create mode 100644 lib/Client.js create mode 100644 src/Client.js create mode 100644 test/integration.js diff --git a/.babelrc b/.babelrc index 623aef6..70c5283 100644 --- a/.babelrc +++ b/.babelrc @@ -6,5 +6,14 @@ "@babel/plugin-proposal-private-methods", "@babel/plugin-proposal-nullish-coalescing-operator" ], - "presets": ["@babel/preset-env"] + "presets": [ + [ + "@babel/preset-env", + { + "targets": { + "node": "10" + } + } + ] + ] } diff --git a/ast.json b/ast.json index c3c4cc0..aa9cffa 100644 --- a/ast.json +++ b/ast.json @@ -1,2214 +1,15 @@ { "operations": [ { - "name": "getTEIs", - "params": [ - "params", - "options", - "callback" - ], - "docs": { - "description": "Get Tracked Entity Instance(s).", - "tags": [ - { - "title": "public", - "description": null, - "type": null - }, - { - "title": "function", - "description": null, - "name": null - }, - { - "title": "param", - "description": "Optional `query parameters` e.g. `{ou: 'DiszpKrYNg8', filters: ['lZGmxYbs97q':GT:5']}`. Run `discover` or see {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html DHIS2 docs} for more details on which params to use when querying tracked entities instances.", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "Object" - } - }, - "name": "params" - }, - { - "title": "param", - "description": "`Optional` options for `getTEIs` operation. Defaults to `{apiVersion: state.configuration.apiVersion, responseType: 'json'}`.", - "type": { - "type": "OptionalType", - "expression": { - "type": "RecordType", - "fields": [ - { - "type": "FieldType", - "key": "apiVersion", - "value": { - "type": "NameExpression", - "name": "number" - } - }, - { - "type": "FieldType", - "key": "responseType", - "value": { - "type": "NameExpression", - "name": "string" - } - } - ] - } - }, - "name": "options" - }, - { - "title": "param", - "description": "Optional callback to handle the response.", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "function" - } - }, - "name": "callback" - }, - { - "title": "returns", - "description": null, - "type": { - "type": "NameExpression", - "name": "Operation" - } - }, - { - "title": "example", - "description": "getTEIs({\n fields: '*',\n ou: 'CMqUILyVnBL',\n trackedEntityInstance: 'HNTA9qD6EEG',\n skipPaging: true,\n});", - "caption": "- Example `getTEIs` `expression.js` for fetching a `single` `Tracked Entity Instance` with all the fields included." - } - ] - }, - "valid": true - }, - { - "name": "upsertTEI", - "params": [ - "uniqueAttributeId", - "data", - "options", - "callback" - ], - "docs": { - "description": "Update TEI if exists otherwise create.\n- Update if the record exists otherwise insert a new record.\n- This is useful for idempotency and duplicate record management.", - "tags": [ - { - "title": "public", - "description": null, - "type": null - }, - { - "title": "function", - "description": null, - "name": null - }, - { - "title": "param", - "description": "Tracked Entity Instance unique identifier attribute used during matching.", - "type": { - "type": "NameExpression", - "name": "string" - }, - "name": "uniqueAttributeId" - }, - { - "title": "param", - "description": "Payload data for new tracked entity instance or updated data for an existing tracked entity instance.", - "type": { - "type": "NameExpression", - "name": "Object" - }, - "name": "data" - }, - { - "title": "param", - "description": "`Optional` options for `upsertTEI` operation. Defaults to `{apiVersion: state.configuration.apiVersion,strict: true,responseType: 'json'}`.", - "type": { - "type": "OptionalType", - "expression": { - "type": "RecordType", - "fields": [ - { - "type": "FieldType", - "key": "apiVersion", - "value": { - "type": "NameExpression", - "name": "number" - } - }, - { - "type": "FieldType", - "key": "strict", - "value": { - "type": "NameExpression", - "name": "boolean" - } - }, - { - "type": "FieldType", - "key": "responseType", - "value": { - "type": "NameExpression", - "name": "string" - } - } - ] - } - }, - "name": "options" - }, - { - "title": "param", - "description": "Optional `callback` to handle the response.", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "function" - } - }, - "name": "callback" - }, - { - "title": "throws", - "description": "Throws `RangeError` when `uniqueAttributeId` is `invalid` or `not unique`.", - "type": { - "type": "NameExpression", - "name": "RangeError" - } - }, - { - "title": "returns", - "description": null, - "type": { - "type": "NameExpression", - "name": "Operation" - } - }, - { - "title": "example", - "description": "upsertTEI('lZGmxYbs97q', {\n orgUnit: 'TSyzvBiovKh',\n trackedEntityType: 'nEenWmSyUEp',\n attributes: [\n {\n attribute: 'lZGmxYbs97q',\n value: '77790012',\n },\n {\n attribute: 'w75KJ2mc4zz',\n value: 'Gigiwe',\n },\n {\n attribute: 'zDhUuAYrxNC',\n value: 'Mwanza',\n },\n ],\n});", - "caption": "- Example `expression.js` for upserting a tracked entity instance on attribute with Id `lZGmxYbs97q`." - } - ] - }, - "valid": true - }, - { - "name": "createTEI", - "params": [ - "data", - "params", - "options", - "callback" - ], - "docs": { - "description": "Create Tracked Entity Instance.", - "tags": [ - { - "title": "public", - "description": null, - "type": null - }, - { - "title": "function", - "description": null, - "name": null - }, - { - "title": "param", - "description": "The update data containing new values.", - "type": { - "type": "NameExpression", - "name": "Object" - }, - "name": "data" - }, - { - "title": "param", - "description": "Optional `import parameters` for a given a resource. E.g. `{dryRun: true, importStrategy: CREATE}` See {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html#import-parameters_1 DHIS2 Import parameters documentation} or run `discover`. Defauls to `DHIS2 default import parameters`.", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "Object" - } - }, - "name": "params" - }, - { - "title": "param", - "description": "`Optional` options for `createTEI` operation. Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}`.", - "type": { - "type": "OptionalType", - "expression": { - "type": "RecordType", - "fields": [ - { - "type": "FieldType", - "key": "apiVersion", - "value": { - "type": "NameExpression", - "name": "number" - } - }, - { - "type": "FieldType", - "key": "responseType", - "value": { - "type": "NameExpression", - "name": "string" - } - } - ] - } - }, - "name": "options" - }, - { - "title": "param", - "description": "Optional callback to handle the response.", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "function" - } - }, - "name": "callback" - }, - { - "title": "returns", - "description": null, - "type": { - "type": "NameExpression", - "name": "Operation" - } - }, - { - "title": "example", - "description": "createTEI({\n orgUnit: 'TSyzvBiovKh',\n trackedEntityType: 'nEenWmSyUEp',\n attributes: [\n {\n attribute: 'lZGmxYbs97q',\n value: valUpsertTEI,\n },\n {\n attribute: 'w75KJ2mc4zz',\n value: 'Gigiwe',\n },\n ],\n enrollments: [\n {\n orgUnit: 'TSyzvBiovKh',\n program: 'fDd25txQckK',\n programState: 'lST1OZ5BDJ2',\n enrollmentDate: '2021-01-04',\n incidentDate: '2021-01-04',\n },\n ],\n});", - "caption": "- Example `expression.js` of `createTEI`." - } - ] - }, - "valid": true - }, - { - "name": "updateTEI", - "params": [ - "path", - "data", - "params", - "options", - "callback" - ], - "docs": { - "description": "Update a Tracked Entity Instance.", - "tags": [ - { - "title": "public", - "description": null, - "type": null - }, - { - "title": "function", - "description": null, - "name": null - }, - { - "title": "param", - "description": "Path to the object being updated. This can be an `id` or path to an `object` in a `nested collection` on the object(E.g. `/api/{collection-object}/{collection-object-id}/{collection-name}/{object-id}`).", - "type": { - "type": "NameExpression", - "name": "string" - }, - "name": "path" - }, - { - "title": "param", - "description": "The update data containing new values.", - "type": { - "type": "NameExpression", - "name": "Object" - }, - "name": "data" - }, - { - "title": "param", - "description": "Optional `import parameters` for a given a resource. E.g. `{dryRun: true, importStrategy: CREATE, filters:[]}` See {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html#import-parameters_1 DHIS2 Import parameters documentation} or run `discover`. Defauls to `DHIS2 default import parameters`.", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "Object" - } - }, - "name": "params" - }, - { - "title": "param", - "description": "`Optional` options for `updateTEI` operation. Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}`.", - "type": { - "type": "OptionalType", - "expression": { - "type": "RecordType", - "fields": [ - { - "type": "FieldType", - "key": "apiVersion", - "value": { - "type": "NameExpression", - "name": "number" - } - }, - { - "type": "FieldType", - "key": "responseType", - "value": { - "type": "NameExpression", - "name": "string" - } - } - ] - } - }, - "name": "options" - }, - { - "title": "param", - "description": "Optional callback to handle the response.", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "function" - } - }, - "name": "callback" - }, - { - "title": "returns", - "description": null, - "type": { - "type": "NameExpression", - "name": "Operation" - } - }, - { - "title": "example", - "description": "updateTEI('PVqUD2hvU4E', {\n orgUnit: 'TSyzvBiovKh',\n trackedEntityType: 'nEenWmSyUEp',\n attributes: [\n {\n attribute: 'lZGmxYbs97q',\n value: valUpsertTEI,\n },\n {\n attribute: 'w75KJ2mc4zz',\n value: 'Gigiwe',\n },\n ],\n enrollments: [\n {\n orgUnit: 'TSyzvBiovKh',\n program: 'fDd25txQckK',\n programState: 'lST1OZ5BDJ2',\n enrollmentDate: '2021-01-04',\n incidentDate: '2021-01-04',\n },\n ],\n});", - "caption": "- Example `expression.js` of `updateTEI`." - } - ] - }, - "valid": true - }, - { - "name": "getEvents", - "params": [ - "params", - "options", - "callback" - ], - "docs": { - "description": "Get annonymous events or tracker events.", - "tags": [ - { - "title": "public", - "description": null, - "type": null - }, - { - "title": "function", - "description": null, - "name": null - }, - { - "title": "param", - "description": "`import` parameters for `getEvents`. See examples here", - "type": { - "type": "NameExpression", - "name": "Object" - }, - "name": "params" - }, - { - "title": "param", - "description": "`Optional` options for `getEvents` operation. Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}`.", - "type": { - "type": "OptionalType", - "expression": { - "type": "RecordType", - "fields": [ - { - "type": "FieldType", - "key": "apiVersion", - "value": { - "type": "NameExpression", - "name": "number" - } - }, - { - "type": "FieldType", - "key": "responseType", - "value": { - "type": "NameExpression", - "name": "string" - } - } - ] - } - }, - "name": "options" - }, - { - "title": "param", - "description": "Optional callback to handle the response.", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "function" - } - }, - "name": "callback" - }, - { - "title": "returns", - "description": null, - "type": { - "type": "NameExpression", - "name": "Operation" - } - }, - { - "title": "example", - "description": "getEvents({ orgUnit: 'YuQRtpLP10I', ouMode: 'CHILDREN' });", - "caption": "- Query for `all events` with `children` of a certain `organisation unit`" - } - ] - }, - "valid": true - }, - { - "name": "createEvents", - "params": [ - "data", - "params", - "options", - "callback" - ], - "docs": { - "description": "Create DHIS2 Events\n- You will need a `program` which can be looked up using the `getPrograms` operation, an `orgUnit` which can be looked up using the `getMetadata` operation and passing `{organisationUnits: true}` as `resources` param, and a list of `valid data element identifiers` which can be looked up using the `getMetadata` passing `{dataElements: true}` as `resources` param.\n- For events with registration, a `tracked entity instance identifier is required`\n- For sending `events` to `programs with multiple stages`, you will need to also include the `programStage` identifier, the identifiers for `programStages` can be found in the `programStages` resource via a call to `getMetadata` operation.", - "tags": [ - { - "title": "public", - "description": null, - "type": null - }, - { - "title": "function", - "description": null, - "name": null - }, - { - "title": "param", - "description": "The payload containing new values", - "type": { - "type": "NameExpression", - "name": "Object" - }, - "name": "data" - }, - { - "title": "param", - "description": "Optional `import parameters` for events. E.g. `{dryRun: true, importStrategy: CREATE, filters:[]}` See {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html#events DHIS2 Event Import parameters documentation} or run `discover`. Defauls to `DHIS2 default import parameters`.", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "Object" - } - }, - "name": "params" - }, - { - "title": "param", - "description": "Optional `flags` for the behavior of the `createEvents` operation.Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}`", - "type": { - "type": "OptionalType", - "expression": { - "type": "RecordType", - "fields": [ - { - "type": "FieldType", - "key": "apiVersion", - "value": { - "type": "NameExpression", - "name": "number" - } - }, - { - "type": "FieldType", - "key": "responseType", - "value": { - "type": "NameExpression", - "name": "string" - } - } - ] - } - }, - "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": "createEvents({\n program: 'eBAyeGv0exc',\n orgUnit: 'DiszpKrYNg8',\n eventDate: date,\n status: 'COMPLETED',\n completedDate: date,\n storedBy: 'admin',\n coordinate: {\n latitude: 59.8,\n longitude: 10.9,\n },\n dataValues: [\n {\n dataElement: 'qrur9Dvnyt5',\n value: '33',\n },\n {\n dataElement: 'oZg33kd9taw',\n value: 'Male',\n },\n {\n dataElement: 'msodh3rEMJa',\n value: date,\n },\n ],\n});", - "caption": "- Example `expression.js` of `createEvents` for a `single event` can look like this:" - } - ] - }, - "valid": true - }, - { - "name": "updateEvents", - "params": [ - "path", - "data", - "params", - "options", - "callback" - ], - "docs": { - "description": "Update DHIS2 Event.\n- To update an existing event, the format of the payload is the same as that of `creating an event` via `createEvents` operations\n- But you should supply the `identifier` of the object you are updating\n- The payload has to contain `all`, even `non-modified`, `attributes`.\n- Attributes that were present before and are not present in the current payload any more will be removed by DHIS2.\n- If you do not want this behavior, please use `upsert` operation to upsert your events.", - "tags": [ - { - "title": "public", - "description": null, - "type": null - }, - { - "title": "function", - "description": null, - "name": null - }, - { - "title": "param", - "description": "Path to the object being updated. This can be an `id` or path to an `object` in a `nested collection` on the object(E.g. `/api/{collection-object}/{collection-object-id}/{collection-name}/{object-id}`)", - "type": { - "type": "NameExpression", - "name": "string" - }, - "name": "path" - }, - { - "title": "param", - "description": "The update data containing new values", - "type": { - "type": "NameExpression", - "name": "Object" - }, - "name": "data" - }, - { - "title": "param", - "description": "Optional `import` parameters for `updateEvents`. E.g. `{dryRun: true, IdScheme: 'CODE'}. Defaults to DHIS2 `default params`", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "Object" - } - }, - "name": "params" - }, - { - "title": "param", - "description": "Optional `flags` for the behavior of the `updateEvents` operation.Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}`", - "type": { - "type": "OptionalType", - "expression": { - "type": "RecordType", - "fields": [ - { - "type": "FieldType", - "key": "apiVersion", - "value": { - "type": "NameExpression", - "name": "number" - } - }, - { - "type": "FieldType", - "key": "responseType", - "value": { - "type": "NameExpression", - "name": "string" - } - } - ] - } - }, - "name": "options" - }, - { - "title": "param", - "description": "Optional callback to handle the response", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "function" - } - }, - "name": "callback" - }, - { - "title": "returns", - "description": null, - "type": { - "type": "NameExpression", - "name": "Operation" - } - }, - { - "title": "example", - "description": "updateEvents('PVqUD2hvU4E', { events: [\n {\n program: 'eBAyeGv0exc',\n orgUnit: 'DiszpKrYNg8',\n eventDate: date,\n status: 'COMPLETED',\n storedBy: 'admin',\n coordinate: {\n latitude: '59.8',\n longitude: '10.9',\n },\n dataValues: [\n {\n dataElement: 'qrur9Dvnyt5',\n value: '22',\n },\n {\n dataElement: 'oZg33kd9taw',\n value: 'Male',\n },\n ],\n }]\n});", - "caption": "- Example `expression.js` of `updateEvents`" - } - ] - }, - "valid": true - }, - { - "name": "getPrograms", - "params": [ - "params", - "options", - "callback" - ], - "docs": { - "description": "Get DHIS2 Tracker Programs.", - "tags": [ - { - "title": "public", - "description": null, - "type": null - }, - { - "title": "function", - "description": null, - "name": null - }, - { - "title": "param", - "description": "`import` parameters for `getPrograms`. See {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html#tracker-web-api DHIS2 api documentation for allowed query parameters }", - "type": { - "type": "NameExpression", - "name": "Object" - }, - "name": "params" - }, - { - "title": "param", - "description": "Optional `flags` for the behavior of the `getPrograms` operation.Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}`", - "type": { - "type": "OptionalType", - "expression": { - "type": "RecordType", - "fields": [ - { - "type": "FieldType", - "key": "apiVersion", - "value": { - "type": "NameExpression", - "name": "number" - } - }, - { - "type": "FieldType", - "key": "responseType", - "value": { - "type": "NameExpression", - "name": "string" - } - } - ] - } - }, - "name": "options" - }, - { - "title": "param", - "description": "Optional callback to handle the response", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "function" - } - }, - "name": "callback" - }, - { - "title": "returns", - "description": null, - "type": { - "type": "NameExpression", - "name": "Operation" - } - }, - { - "title": "example", - "description": "getPrograms({ orgUnit: 'DiszpKrYNg8' , fields: '*' });", - "caption": "- Query for `all programs` with a certain `organisation unit`" - } - ] - }, - "valid": true - }, - { - "name": "createPrograms", - "params": [ - "data", - "params", - "options", - "callback" - ], - "docs": { - "description": "Create a DHIS2 Tracker Program", - "tags": [ - { - "title": "public", - "description": null, - "type": null - }, - { - "title": "function", - "description": null, - "name": null - }, - { - "title": "param", - "description": "The update data containing new values", - "type": { - "type": "NameExpression", - "name": "Object" - }, - "name": "data" - }, - { - "title": "param", - "description": "Optional `import` parameters for `createPrograms`. E.g. `{dryRun: true, IdScheme: 'CODE'}. Defaults to DHIS2 `default params`", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "Object" - } - }, - "name": "params" - }, - { - "title": "param", - "description": "Optional `flags` for the behavior of the `getPrograms` operation.Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}`", - "type": { - "type": "OptionalType", - "expression": { - "type": "RecordType", - "fields": [ - { - "type": "FieldType", - "key": "apiVersion", - "value": { - "type": "NameExpression", - "name": "number" - } - }, - { - "type": "FieldType", - "key": "responseType", - "value": { - "type": "NameExpression", - "name": "string" - } - } - ] - } - }, - "name": "options" - }, - { - "title": "param", - "description": "Optional callback to handle the response", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "function" - } - }, - "name": "callback" - }, - { - "title": "returns", - "description": null, - "type": { - "type": "NameExpression", - "name": "Operation" - } - }, - { - "title": "example", - "description": "createPrograms(state.data);", - "caption": "- Example `expression.js` of `createPrograms` for a `single program` can look like this:" - } - ] - }, - "valid": true - }, - { - "name": "updatePrograms", - "params": [ - "path", - "data", - "params", - "options", - "callback" - ], - "docs": { - "description": "Update DHIS2 Tracker Programs\n- To update an existing program, the format of the payload is the same as that of `creating an event` via `createEvents` operations\n- But you should supply the `identifier` of the object you are updating\n- The payload has to contain `all`, even `non-modified`, `attributes`.\n- Attributes that were present before and are not present in the current payload any more will be removed by DHIS2.\n- If you do not want this behavior, please use `upsert` operation to upsert your events.", - "tags": [ - { - "title": "public", - "description": null, - "type": null - }, - { - "title": "function", - "description": null, - "name": null - }, - { - "title": "param", - "description": "Path to the object being updated. This can be an `id` or path to an `object` in a `nested collection` on the object(E.g. `/api/{collection-object}/{collection-object-id}/{collection-name}/{object-id}`)", - "type": { - "type": "NameExpression", - "name": "string" - }, - "name": "path" - }, - { - "title": "param", - "description": "The update data containing new values", - "type": { - "type": "NameExpression", - "name": "Object" - }, - "name": "data" - }, - { - "title": "param", - "description": "Optional `import` parameters for `updatePrograms`. E.g. `{dryRun: true, IdScheme: 'CODE'}. Defaults to DHIS2 `default params`", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "Object" - } - }, - "name": "params" - }, - { - "title": "param", - "description": "Optional `flags` for the behavior of the `getPrograms` operation.Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}`", - "type": { - "type": "OptionalType", - "expression": { - "type": "RecordType", - "fields": [ - { - "type": "FieldType", - "key": "apiVersion", - "value": { - "type": "NameExpression", - "name": "number" - } - }, - { - "type": "FieldType", - "key": "responseType", - "value": { - "type": "NameExpression", - "name": "string" - } - } - ] - } - }, - "name": "options" - }, - { - "title": "param", - "description": "Optional callback to handle the response", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "function" - } - }, - "name": "callback" - }, - { - "title": "returns", - "description": null, - "type": { - "type": "NameExpression", - "name": "Operation" - } - }, - { - "title": "example", - "description": "updatePrograms('PVqUD2hvU4E', state.data);", - "caption": "- Example `expression.js` of `updatePrograms`" - } - ] - }, - "valid": true - }, - { - "name": "getEnrollments", - "params": [ - "params", - "options", - "callback" - ], - "docs": { - "description": "Get DHIS2 Enrollments", - "tags": [ - { - "title": "public", - "description": null, - "type": null - }, - { - "title": "function", - "description": null, - "name": null - }, - { - "title": "param", - "description": "`Query` parameters for `getEnrollments`. See {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html#enrollment-management here}", - "type": { - "type": "NameExpression", - "name": "Object" - }, - "name": "params" - }, - { - "title": "param", - "description": "Optional `flags` for the behavior of the `getEnrollments` operation.Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}`", - "type": { - "type": "OptionalType", - "expression": { - "type": "RecordType", - "fields": [ - { - "type": "FieldType", - "key": "apiVersion", - "value": { - "type": "NameExpression", - "name": "number" - } - }, - { - "type": "FieldType", - "key": "responseType", - "value": { - "type": "NameExpression", - "name": "string" - } - } - ] - } - }, - "name": "options" - }, - { - "title": "param", - "description": "Optional callback to handle the response", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "function" - } - }, - "name": "callback" - }, - { - "title": "returns", - "description": null, - "type": { - "type": "NameExpression", - "name": "Operation" - } - }, - { - "title": "example", - "description": "getEnrollments({\n ou: 'O6uvpzGd5pu',\n ouMode: 'DESCENDANTS',\n program: 'ur1Edk5Oe2n',\n fields: '*',\n});", - "caption": "- To constrain the response to `enrollments` which are part of a `specific program` you can include a `program query parameter`" - } - ] - }, - "valid": true - }, - { - "name": "enrollTEI", - "params": [ - "data", - "params", - "options", - "callback" - ], - "docs": { - "description": "Enroll a TEI into a program\n- Enrolling a tracked entity instance into a program\n- For enrolling `persons` into a `program`, you will need to first get the `identifier of the person` from the `trackedEntityInstances resource` via the `getTEIs` operation.\n- Then, you will need to get the `program identifier` from the `programs` resource via the `getPrograms` operation.", - "tags": [ - { - "title": "public", - "description": null, - "type": null - }, - { - "title": "function", - "description": null, - "name": null - }, - { - "title": "param", - "description": "The enrollment data. See example {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html#enrollment-management here }", - "type": { - "type": "NameExpression", - "name": "Object" - }, - "name": "data" - }, - { - "title": "param", - "description": "Optional `import` parameters for `createEnrollment`. E.g. `{dryRun: true, IdScheme: 'CODE'}. Defaults to DHIS2 `default params`", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "Object" - } - }, - "name": "params" - }, - { - "title": "param", - "description": "Optional `flags` for the behavior of the `enrollTEI` operation.Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}`", - "type": { - "type": "OptionalType", - "expression": { - "type": "RecordType", - "fields": [ - { - "type": "FieldType", - "key": "apiVersion", - "value": { - "type": "NameExpression", - "name": "number" - } - }, - { - "type": "FieldType", - "key": "responseType", - "value": { - "type": "NameExpression", - "name": "string" - } - } - ] - } - }, - "name": "options" - }, - { - "title": "param", - "description": "Optional callback to handle the response", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "function" - } - }, - "name": "callback" - }, - { - "title": "returns", - "description": null, - "type": { - "type": "NameExpression", - "name": "Operation" - } - }, - { - "title": "example", - "description": "enrollTEI({\n trackedEntity: 'tracked-entity-id',\n orgUnit: 'org-unit-id',\n attributes: [\n {\n attribute: 'attribute-id',\n value: 'attribute-value',\n },\n ],\n enrollments: [\n {\n orgUnit: 'org-unit-id',\n program: 'program-id',\n enrollmentDate: '2013-09-17',\n incidentDate: '2013-09-17',\n },\n ],\n});", - "caption": "- Example `expression.js` of `createEnrollment` of a `person` into a `program` can look like this:" - } - ] - }, - "valid": true - }, - { - "name": "updateEnrollments", - "params": [ - "path", - "data", - "params", - "options", - "callback" - ], - "docs": { - "description": "Update a DHIS2 Enrollemts\n- To update an existing enrollment, the format of the payload is the same as that of `creating an event` via `createEvents` operations\n- But you should supply the `identifier` of the object you are updating\n- The payload has to contain `all`, even `non-modified`, `attributes`.\n- Attributes that were present before and are not present in the current payload any more will be removed by DHIS2.\n- If you do not want this behavior, please use `upsert` operation to upsert your events.", - "tags": [ - { - "title": "public", - "description": null, - "type": null - }, - { - "title": "function", - "description": null, - "name": null - }, - { - "title": "param", - "description": "Path to the object being updated. This can be an `id` or path to an `object` in a `nested collection` on the object(E.g. `/api/{collection-object}/{collection-object-id}/{collection-name}/{object-id}`)", - "type": { - "type": "NameExpression", - "name": "string" - }, - "name": "path" - }, - { - "title": "param", - "description": "The update data containing new values", - "type": { - "type": "NameExpression", - "name": "Object" - }, - "name": "data" - }, - { - "title": "param", - "description": "Optional `import` parameters for `updateEnrollments`. E.g. `{dryRun: true, IdScheme: 'CODE'}. Defaults to DHIS2 `default params`", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "Object" - } - }, - "name": "params" - }, - { - "title": "param", - "description": "Optional `flags` for the behavior of the `updateEnrollments` operation.Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}`", - "type": { - "type": "OptionalType", - "expression": { - "type": "RecordType", - "fields": [ - { - "type": "FieldType", - "key": "apiVersion", - "value": { - "type": "NameExpression", - "name": "number" - } - }, - { - "type": "FieldType", - "key": "responseType", - "value": { - "type": "NameExpression", - "name": "string" - } - } - ] - } - }, - "name": "options" - }, - { - "title": "param", - "description": "Optional callback to handle the response", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "function" - } - }, - "name": "callback" - }, - { - "title": "returns", - "description": null, - "type": { - "type": "NameExpression", - "name": "Operation" - } - }, - { - "title": "example", - "description": "updateEnrollments('PVqUD2hvU4E', state.data);", - "caption": "- Example `expression.js` of `updateEnromments`" - } - ] - }, - "valid": true - }, - { - "name": "cancelEnrollment", - "params": [ - "enrollmentId", - "params", - "options", - "callback" - ], - "docs": { - "description": "Cancel a DHIS2 Enrollment\n- To cancel an existing enrollment, you should supply the `enrollment identifier`(`enrollemt-id`)", - "tags": [ - { - "title": "public", - "description": null, - "type": null - }, - { - "title": "function", - "description": null, - "name": null - }, - { - "title": "param", - "description": "The `enrollment-id` of the enrollment you wish to cancel", - "type": { - "type": "NameExpression", - "name": "string" - }, - "name": "enrollmentId" - }, - { - "title": "param", - "description": "Optional `import` parameters for `cancelEnrollment`. E.g. `{dryRun: true, IdScheme: 'CODE'}. Defaults to DHIS2 `default params`", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "Object" - } - }, - "name": "params" - }, - { - "title": "param", - "description": "Optional `flags` for the behavior of the `cancelEnrollment` operation.Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}`", - "type": { - "type": "OptionalType", - "expression": { - "type": "RecordType", - "fields": [ - { - "type": "FieldType", - "key": "apiVersion", - "value": { - "type": "NameExpression", - "name": "number" - } - }, - { - "type": "FieldType", - "key": "responseType", - "value": { - "type": "NameExpression", - "name": "string" - } - } - ] - } - }, - "name": "options" - }, - { - "title": "param", - "description": "Optional callback to handle the response", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "function" - } - }, - "name": "callback" - }, - { - "title": "returns", - "description": null, - "type": { - "type": "NameExpression", - "name": "Operation" - } - }, - { - "title": "example", - "description": "cancelEnrollments('PVqUD2hvU4E');", - "caption": "- Example `expression.js` of `cancelEnrollment`" - } - ] - }, - "valid": true - }, - { - "name": "completeEnrollment", - "params": [ - "enrollmentId", - "params", - "options", - "callback" - ], - "docs": { - "description": "Complete a DHIS2 Enrollment\n- To complete an existing enrollment, you should supply the `enrollment identifier`(`enrollemt-id`)", - "tags": [ - { - "title": "public", - "description": null, - "type": null - }, - { - "title": "function", - "description": null, - "name": null - }, - { - "title": "param", - "description": "The `enrollment-id` of the enrollment you wish to cancel", - "type": { - "type": "NameExpression", - "name": "string" - }, - "name": "enrollmentId" - }, - { - "title": "param", - "description": "Optional `import` parameters for `completeEnrollment`. E.g. `{dryRun: true, IdScheme: 'CODE'}. Defaults to DHIS2 `default params`", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "Object" - } - }, - "name": "params" - }, - { - "title": "param", - "description": "Optional `flags` for the behavior of the `completeEnrollment` operation.Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}`", - "type": { - "type": "OptionalType", - "expression": { - "type": "RecordType", - "fields": [ - { - "type": "FieldType", - "key": "apiVersion", - "value": { - "type": "NameExpression", - "name": "number" - } - }, - { - "type": "FieldType", - "key": "responseType", - "value": { - "type": "NameExpression", - "name": "string" - } - } - ] - } - }, - "name": "options" - }, - { - "title": "param", - "description": "Optional callback to handle the response", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "function" - } - }, - "name": "callback" - }, - { - "title": "returns", - "description": null, - "type": { - "type": "NameExpression", - "name": "Operation" - } - }, - { - "title": "example", - "description": "completeEnrollment('PVqUD2hvU4E');", - "caption": "- Example `expression.js` of `completeEnrollment`" - } - ] - }, - "valid": true - }, - { - "name": "getRelationships", - "params": [ - "params", - "options", - "callback" - ], - "docs": { - "description": "Get DHIS2 Relationships(links) between two entities in tracker. These entities can be tracked entity instances, enrollments and events.\n- All the tracker operations, `getTEIs`, `getEnrollments` and `getEvents` also list their relationships if requested in the `field` filter.\n- To list all relationships, this requires you to provide the UID of the trackedEntityInstance, Enrollment or event that you want to list all the relationships for.", - "tags": [ - { - "title": "public", - "description": null, - "type": null - }, - { - "title": "function", - "description": null, - "name": null - }, - { - "title": "param", - "description": "`Query` parameters for `getRelationships`. See examples {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html#relationships here}", - "type": { - "type": "NameExpression", - "name": "Object" - }, - "name": "params" - }, - { - "title": "param", - "description": "Optional `flags` for the behavior of the `getRelationships` operation.Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}`", - "type": { - "type": "OptionalType", - "expression": { - "type": "RecordType", - "fields": [ - { - "type": "FieldType", - "key": "apiVersion", - "value": { - "type": "NameExpression", - "name": "number" - } - }, - { - "type": "FieldType", - "key": "responseType", - "value": { - "type": "NameExpression", - "name": "string" - } - } - ] - } - }, - "name": "options" - }, - { - "title": "param", - "description": "Optional callback to handle the response", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "function" - } - }, - "name": "callback" - }, - { - "title": "returns", - "description": null, - "type": { - "type": "NameExpression", - "name": "Operation" - } - }, - { - "title": "example", - "description": "getRelationships({ tei: 'F8yKM85NbxW', fields: '*' });", - "caption": "- A query for `all relationships` associated with a `specific tracked entity instance` can look like this:" - } - ] - }, - "valid": true - }, - { - "name": "getDataValues", - "params": [ - "params", - "options", - "callback" - ], - "docs": { - "description": "Get DHIS2 Data Values.\n- This operation retrives data values from DHIS2 Web API by interacting with the `dataValueSets` resource\n- Data values can be retrieved in XML, JSON and CSV format.", - "tags": [ - { - "title": "public", - "description": null, - "type": null - }, - { - "title": "function", - "description": null, - "name": null - }, - { - "title": "param", - "description": "`Query` parameters for `getDataValues`. E.g. `{dataset: 'pBOMPrpg1QX', limit: 3, period: 2021, orgUnit: 'DiszpKrYNg8'} Run `discover` or see {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html#data-values DHIS2 API docs} for available `Data Value Set Query Parameters`.", - "type": { - "type": "NameExpression", - "name": "Object" - }, - "name": "params" - }, - { - "title": "param", - "description": "Optional `options` for `getDataValues` operation. Defaults to `{apiVersion: state.configuration.apiVersion, responseType: 'json'}`", - "type": { - "type": "OptionalType", - "expression": { - "type": "RecordType", - "fields": [ - { - "type": "FieldType", - "key": "apiVersion", - "value": { - "type": "NameExpression", - "name": "number" - } - }, - { - "type": "FieldType", - "key": "responseType", - "value": { - "type": "NameExpression", - "name": "string" - } - } - ] - } - }, - "name": "options" - }, - { - "title": "param", - "description": "Optional `callback` to handle the response", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "function" - } - }, - "name": "callback" - }, - { - "title": "returns", - "description": null, - "type": { - "type": "NameExpression", - "name": "Operation" - } - }, - { - "title": "example", - "description": "getDataValues({\n orgUnit: 'DiszpKrYNg8',\n period: '202010',\n dataSet: 'pBOMPrpg1QX',\n limit: 2,\n});", - "caption": "- Example getting **two** `data values` associated with a specific `orgUnit`, `dataSet`, and `period `" - } - ] - }, - "valid": true - }, - { - "name": "createDataValues", - "params": [ - "data", - "options", - "params", - "callback" - ], - "docs": { - "description": "Create DHIS2 Data Values\n- This is used to send aggregated data to DHIS2\n- A data value set represents a set of data values which have a relationship, usually from being captured off the same data entry form.\n- To send a set of related data values sharing the same period and organisation unit, we need to identify the period, the data set, the org unit (facility) and the data elements for which to report.\n- You can also use this operation to send large bulks of data values which don't necessarily are logically related.\n- To send data values that are not linked to a `dataSet`, you do not need to specify the dataSet and completeDate attributes. Instead, you will specify the period and orgUnit attributes on the individual data value elements instead of on the outer data value set element. This will enable us to send data values for various periods and organisation units", - "tags": [ - { - "title": "public", - "description": null, - "type": null - }, - { - "title": "function", - "description": null, - "name": null - }, - { - "title": "param", - "description": "The `data values` to upload or create. See example shape.", - "type": { - "type": "NameExpression", - "name": "object" - }, - "name": "data" - }, - { - "title": "param", - "description": "Optional `flags` for the behavior of the `createDataVaues` operation.", - "type": { - "type": "OptionalType", - "expression": { - "type": "RecordType", - "fields": [ - { - "type": "FieldType", - "key": "apiVersion", - "value": { - "type": "NameExpression", - "name": "number" - } - }, - { - "type": "FieldType", - "key": "responseType", - "value": { - "type": "NameExpression", - "name": "string" - } - } - ] - } - }, - "name": "options" - }, - { - "title": "param", - "description": "Optional `import` parameters for `createDataValues`. E.g. `{dryRun: true, IdScheme: 'CODE'}. Defaults to DHIS2 `default params`. Run `discover` or visit {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html#data-values DHIS2 Docs API} to learn about available data values import parameters.", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "object" - } - }, - "name": "params" - }, - { - "title": "param", - "description": "Optional callback to handle the response", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "function" - } - }, - "name": "callback" - }, - { - "title": "returns", - "description": null, - "type": { - "type": "NameExpression", - "name": "Operation" - } - }, - { - "title": "example", - "description": "createDataValues({\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": "- Example `expression.js` of `createDataValues` for sending a set of related data values sharing the same period and organisation unit" - } - ] - }, - "valid": true - }, - { - "name": "generateDhis2UID", - "params": [ - "options", - "callback" - ], - "docs": { - "description": "Generate valid, random DHIS2 identifiers\n- Useful for client generated Ids compatible with DHIS2", - "tags": [ - { - "title": "public", - "description": null, - "type": null - }, - { - "title": "function", - "description": null, - "name": null - }, - { - "title": "param", - "description": "Optional `options` for `generateDhis2UID` operation. Defaults to `{apiVersion: state.configuration.apiVersion,limit: 1,responseType: 'json'}`", - "type": { - "type": "OptionalType", - "expression": { - "type": "RecordType", - "fields": [ - { - "type": "FieldType", - "key": "apiVersion", - "value": { - "type": "NameExpression", - "name": "number" - } - }, - { - "type": "FieldType", - "key": "limit", - "value": { - "type": "NameExpression", - "name": "number" - } - }, - { - "type": "FieldType", - "key": "responseType", - "value": { - "type": "NameExpression", - "name": "string" - } - } - ] - } - }, - "name": "options" - }, - { - "title": "param", - "description": "Callback to handle response", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "function" - } - }, - "name": "callback" - }, - { - "title": "returns", - "description": null, - "type": { - "type": "NameExpression", - "name": "Operation" - } - }, - { - "title": "example", - "description": "generateDhis2UID({limit: 3});", - "caption": "Example generating `three UIDs` from the DHIS2 server" - } - ] - }, - "valid": true - }, - { - "name": "discover", - "params": [ - "httpMethod", - "endpoint" - ], - "docs": { - "description": "Discover `DHIS2` `api` `endpoint` `query parameters` and allowed `operators` for a given resource's endpoint.", - "tags": [ - { - "title": "public", - "description": null, - "type": null - }, - { - "title": "function", - "description": null, - "name": null - }, - { - "title": "param", - "description": "The HTTP to inspect parameter usage for a given endpoint, e.g., `get`, `post`,`put`,`patch`,`delete`", - "type": { - "type": "NameExpression", - "name": "string" - }, - "name": "httpMethod" - }, - { - "title": "param", - "description": "The path for a given endpoint. E.g. `/trackedEntityInstances` or `/dataValueSets`", - "type": { - "type": "NameExpression", - "name": "string" - }, - "name": "endpoint" - }, - { - "title": "returns", - "description": null, - "type": { - "type": "NameExpression", - "name": "Operation" - } - }, - { - "title": "example", - "description": "discover('post', '/trackedEntityInstances')", - "caption": "Example getting a list of `parameters allowed` on a given `endpoint` for specific `http method`" - } - ] - }, - "valid": true - }, - { - "name": "getAnalytics", - "params": [ - "params", - "options", - "callback" - ], - "docs": { - "description": "Get analytical, aggregated data\n- The analytics resource is powerful as it lets you query and retrieve data aggregated along all available data dimensions.\n- For instance, you can ask the analytics resource to provide the aggregated data values for a set of data elements, periods and organisation units.\n- Also, you can retrieve the aggregated data for a combination of any number of dimensions based on data elements and organisation unit group sets.", - "tags": [ - { - "title": "public", - "description": null, - "type": null - }, - { - "title": "function", - "description": null, - "name": null - }, - { - "title": "param", - "description": "Analytics `query parameters`, e.g. `{dx: 'fbfJHSPpUQD;cYeuwXTCPkU',filters: ['pe:2014Q1;2014Q2','ou:O6uvpzGd5pu;lc3eMKXaEfw']}`. Run `discover` or visit {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html#analytics DHIS2 API docs} to get the params available.", - "type": { - "type": "NameExpression", - "name": "Object" - }, - "name": "params" - }, - { - "title": "param", - "description": "`Optional` options for `getAnalytics` operation. Defaults to `{apiVersion: state.configuration.apiVersion, responseType: 'json'}`.", - "type": { - "type": "OptionalType", - "expression": { - "type": "RecordType", - "fields": [ - { - "type": "FieldType", - "key": "apiVersion", - "value": { - "type": "NameExpression", - "name": "number" - } - }, - { - "type": "FieldType", - "key": "responseType", - "value": { - "type": "NameExpression", - "name": "string" - } - } - ] - } - }, - "name": "options" - }, - { - "title": "param", - "description": "Callback to handle response", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "function" - } - }, - "name": "callback" - }, - { - "title": "returns", - "description": null, - "type": { - "type": "NameExpression", - "name": "Operation" - } - }, - { - "title": "example", - "description": "getAnalytics({\n dimensions: [\n 'dx:fbfJHSPpUQD;cYeuwXTCPkU',\n 'pe:2014',\n 'ou:O6uvpzGd5pu;lc3eMKXaEfw',\n ],\n measureCriteria: 'GE:6500;LT:33000',\n});", - "caption": "Example getting only records where the data value is greater or equal to 6500 and less than 33000" - } - ] - }, - "valid": true - }, - { - "name": "getResources", - "params": [ - "params", - "options", - "callback" - ], - "docs": { - "description": "Get DHIS2 api resources", - "tags": [ - { - "title": "public", - "description": null, - "type": null - }, - { - "title": "function", - "description": null, - "name": null - }, - { - "title": "param", - "description": "The `optional` query parameters for this endpoint. E.g `{filter: 'singular:like:attribute'}`.", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "Object" - } - }, - "name": "params" - }, - { - "title": "param", - "description": "The `optional` options, specifiying the filter expression. E.g. `singular:eq:attribute`.", - "type": { - "type": "OptionalType", - "expression": { - "type": "RecordType", - "fields": [ - { - "type": "FieldType", - "key": "filter", - "value": { - "type": "NameExpression", - "name": "string" - } - }, - { - "type": "FieldType", - "key": "fields", - "value": { - "type": "NameExpression", - "name": "string" - } - }, - { - "type": "FieldType", - "key": "responseType", - "value": { - "type": "NameExpression", - "name": "string" - } - } - ] - } - }, - "name": "options" - }, - { - "title": "param", - "description": "The `optional callback function that will be called to handle data returned by this function.", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "function" - } - }, - "name": "callback" - }, - { - "title": "returns", - "description": null, - "type": { - "type": "NameExpression", - "name": "Operation" - } - }, - { - "title": "example", - "description": "getResources('dataElement', {\n filter: 'singular:eq:attribute',\n fields: '*',\n responseType: 'xml',\n});", - "caption": "Example getting a resource named `attribute`, in `xml` format, returning all the fields" - } - ] - }, - "valid": true - }, - { - "name": "getSchema", - "params": [ - "resourceType", - "params", - "options", - "callback" - ], - "docs": { - "description": "Get schema of a given resource type, in any data format supported by DHIS2", - "tags": [ - { - "title": "public", - "description": null, - "type": null - }, - { - "title": "function", - "description": null, - "name": null - }, - { - "title": "param", - "description": "The type of resource to be updated(`singular` version of the `resource name`). E.g. `dataElement`, `organisationUnit`, etc. Run `getResources` to see available resources and their corresponding `singular` names.", - "type": { - "type": "NameExpression", - "name": "string" - }, - "name": "resourceType" - }, - { - "title": "param", - "description": "Optional `query parameters` for the `getSchema` operation. e.g. `{ fields: 'properties' ,skipPaging: true}`. Run`discover` or See {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html#metadata-export-examples DHIS2 API Docs}", - "type": { - "type": "NameExpression", - "name": "Object" - }, - "name": "params" - }, - { - "title": "param", - "description": "Optional options for `getSchema` method. Defaults to `{apiVersion: state.configuration.apiVersion, responseType: 'json'}`", - "type": { - "type": "OptionalType", - "expression": { - "type": "RecordType", - "fields": [ - { - "type": "FieldType", - "key": "apiVersion", - "value": { - "type": "NameExpression", - "name": "number" - } - }, - { - "type": "FieldType", - "key": "resourceType", - "value": { - "type": "NameExpression", - "name": "string" - } - } - ] - } - }, - "name": "options" - }, - { - "title": "param", - "description": "Optional `callback` to handle the response", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "function" - } - }, - "name": "callback" - }, - { - "title": "returns", - "description": null, - "type": { - "type": "NameExpression", - "name": "Operation" - } - }, - { - "title": "example", - "description": "getSchema('dataElement', '{ fields: '*' }, { responseType: 'xml' });", - "caption": "Example getting the `schema` for `dataElement` in XML" - } - ] - }, - "valid": true - }, - { - "name": "getData", + "name": "create", "params": [ "resourceType", - "params", + "data", "options", "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": "Create a record", "tags": [ { "title": "public", @@ -2222,7 +23,7 @@ }, { "title": "param", - "description": "The type of resource to get(use its `plural` name). E.g. `dataElements`, `trackedEntityInstances`,`organisationUnits`, etc.", + "description": "Type of resource to create. E.g. `trackedEntityInstances`, `programs`, `events`, ...", "type": { "type": "NameExpression", "name": "string" @@ -2231,49 +32,21 @@ }, { "title": "param", - "description": "Optional `query parameters` e.g. `{ou: 'DiszpKrYNg8'}`. Run `discover` or see {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html DHIS2 docs} for more details on which params to use for a given type of resource.", + "description": "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": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "Object" - } + "type": "NameExpression", + "name": "Object" }, - "name": "params" + "name": "data" }, { "title": "param", - "description": "`Optional` options for `getData` operation. Defaults to `{operationName: 'getData', apiVersion: state.configuration.apiVersion, responseType: 'json'}`.", + "description": "Optional `options` to control the behavior of the `create` operation and to pass `import parameters` E.g. `{dryRun: true, importStrategy: CREATE}` See {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html DHIS2 API documentation} or {@link discover}..` Defaults to `{operationName: 'create', apiVersion: null, responseType: 'json'}`", "type": { "type": "OptionalType", "expression": { - "type": "RecordType", - "fields": [ - { - "type": "FieldType", - "key": "apiVersion", - "value": { - "type": "NameExpression", - "name": "number" - } - }, - { - "type": "FieldType", - "key": "operationName", - "value": { - "type": "NameExpression", - "name": "string" - } - }, - { - "type": "FieldType", - "key": "responseType", - "value": { - "type": "NameExpression", - "name": "string" - } - } - ] + "type": "NameExpression", + "name": "Object" } }, "name": "options" @@ -2292,7 +65,7 @@ }, { "title": "returns", - "description": "state", + "description": null, "type": { "type": "NameExpression", "name": "Operation" @@ -2300,23 +73,74 @@ }, { "title": "example", - "description": "getData('trackedEntityInstances', {\n fields: '*',\n ou: 'DiszpKrYNg8',\n entityType: 'nEenWmSyUEp',\n trackedEntityInstance: 'dNpxRu1mWG5',\n});", - "caption": "Example getting one `trackedEntityInstance` with `Id` 'dNpxRu1mWG5' for a given `orgUnit(DiszpKrYNg8)`" + "description": "create('programs', {\n name: 'name 20',\n shortName: 'n20',\n programType: 'WITHOUT_REGISTRATION',\n});", + "caption": "-a `program`" + }, + { + "title": "example", + "description": "create('events', {\n program: 'eBAyeGv0exc',\n orgUnit: 'DiszpKrYNg8',\n status: 'COMPLETED',\n});", + "caption": "-an `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`" + }, + { + "title": "example", + "description": "create('dataSets', { name: 'OpenFn Data Set', periodType: 'Monthly' });", + "caption": "-a `dataSet`" + }, + { + "title": "example", + "description": "create('dataSetNotificationTemplates', {\n dataSetNotificationTrigger: 'DATA_SET_COMPLETION',\n notificationRecipient: 'ORGANISATION_UNIT_CONTACT',\n name: 'Notification',\n messageTemplate: 'Hello',\n deliveryChannels: ['SMS'],\n dataSets: [],\n});", + "caption": "-a `dataSetNotification`" + }, + { + "title": "example", + "description": "create('dataElements', {\n aggregationType: 'SUM',\n domainType: 'AGGREGATE',\n valueType: 'NUMBER',\n name: 'Paracetamol',\n shortName: 'Para',\n});", + "caption": "-a `dataElement`" + }, + { + "title": "example", + "description": "create('dataElementGroups', {\n name: 'Data Element Group 1',\n dataElements: [],\n});", + "caption": "-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`" + }, + { + "title": "example", + "description": "create('dataValueSets', {\n dataElement: 'f7n9E0hX8qk',\n period: '201401',\n orgUnit: 'DiszpKrYNg8',\n value: '12',\n});", + "caption": "-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`" + }, + { + "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`" } ] }, "valid": true }, { - "name": "getMetadata", + "name": "update", "params": [ - "resources", - "params", + "resourceType", + "path", + "data", "options", "callback" ], "docs": { - "description": "Get metadata. A generic helper function to get metadata records from a given DHIS2 instance", + "description": "Update data. A generic helper function to update a resource object of any type.\nUpdating an object requires to send `all required fields` or the `full body`", "tags": [ { "title": "public", @@ -2330,37 +154,34 @@ }, { "title": "param", - "description": "Required. List of metadata resources to fetch. E.g. `['organisationUnits', 'attributes']` or like `'dataSets'` if you only want a single type of resource. See `getResources` to see the types of resources available.", + "description": "The type of resource to be updated. E.g. `dataElements`, `organisationUnits`, etc.", "type": { - "type": "TypeApplication", - "expression": { - "type": "NameExpression", - "name": "Array" - }, - "applications": [ - { - "type": "NameExpression", - "name": "string" - } - ] + "type": "NameExpression", + "name": "string" }, - "name": "resources" + "name": "resourceType" }, { "title": "param", - "description": "Optional `query parameters` e.g. `{filters: ['name:like:ANC'],fields:'*'}`. See `discover` or visit {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html#metadata-export DHIS2 API docs}", + "description": "The `id` or `path` to the `object` to be updated. E.g. `FTRrcoaog83` or `FTRrcoaog83/{collection-name}/{object-id}`", "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "Object" - } + "type": "NameExpression", + "name": "string" }, - "name": "params" + "name": "path" + }, + { + "title": "param", + "description": "Data to update. It requires to send `all required fields` or the `full body`. If you want `partial updates`, use `patch` operation.", + "type": { + "type": "NameExpression", + "name": "Object" + }, + "name": "data" }, { "title": "param", - "description": "Optional `options` for `getMetadata` operation. Defaults to `{operationName: 'getMetadata', apiVersion: state.configuration.apiVersion, responseType: 'json'}`", + "description": "Optional options for update method. Defaults to `{operationName: 'update', apiVersion: state.configuration.apiVersion, responseType: 'json'}`", "type": { "type": "OptionalType", "expression": { @@ -2397,7 +218,7 @@ }, { "title": "param", - "description": "Optional `callback` to handle the response", + "description": "Optional callback to handle the response", "type": { "type": "OptionalType", "expression": { @@ -2417,171 +238,95 @@ }, { "title": "example", - "description": "getMetadata(['dataElements', 'indicators'], {\n filters: ['name:like:ANC'],\n});", - "caption": "Example getting a list of `data elements` and `indicators` where `name` includes the word **ANC**" - } - ] - }, - "valid": true - }, - { - "name": "create", - "params": [ - "resourceType", - "data", - "options", - "params", - "callback" - ], - "docs": { - "description": "create data. A generic helper method to create a record of any kind in DHIS2", - "tags": [ + "description": "update('programs', 'qAZJCrNJK8H', {\n name: '14e1aa02c3f0a31618e096f2c6d03bed',\n shortName: '14e1aa02',\n programType: 'WITHOUT_REGISTRATION',\n});", + "caption": "-a program" + }, { - "title": "public", - "description": null, - "type": null + "title": "example", + "description": "update('events', 'PVqUD2hvU4E', {\n program: 'eBAyeGv0exc',\n orgUnit: 'Ngelehun CHC',\n status: 'COMPLETED',\n storedBy: 'admin',\n dataValues: [],\n});", + "caption": "an `event`" }, { - "title": "function", - "description": null, - "name": null + "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`" }, { - "title": "param", - "description": "Type of resource to create. E.g. `trackedEntityInstances`", - "type": { - "type": "NameExpression", - "name": "string" - }, - "name": "resourceType" + "title": "example", + "description": "update('dataSets', 'lyLU2wR22tC', { name: 'OpenFN Data Set', periodType: 'Weekly' });", + "caption": "-a `dataSet`" }, { - "title": "param", - "description": "Data that will be used to create a given instance of resource", - "type": { - "type": "NameExpression", - "name": "Object" - }, - "name": "data" + "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});", + "caption": "-a `dataSetNotification`" }, { - "title": "param", - "description": "Optional `options` to control the behavior of the `create` operation.` Defaults to `{operationName: 'create', apiVersion: null, responseType: 'json'}`", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "Object" - } - }, - "name": "options" + "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`" }, { - "title": "param", - "description": "Optional `import parameters` for a given a resource. E.g. `{dryRun: true, importStrategy: CREATE}` See {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html DHIS2 API documentation} or {@link discover}. Defauls to `DHIS2 default params` for a given resource type.", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "Object" - } - }, - "name": "params" + "title": "example", + "description": "update('dataElementGroups', 'QrprHT61XFk', {\n name: 'Data Element Group 1',\n dataElements: [],\n});", + "caption": "-a `dataElementGroup`" }, { - "title": "param", - "description": "Optional callback to handle the response", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "function" - } - }, - "name": "callback" + "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`" }, { - "title": "returns", - "description": null, - "type": { - "type": "NameExpression", - "name": "Operation" - } + "title": "example", + "description": "update('dataValueSets', 'AsQj6cDsUq4', {\n dataElement: 'f7n9E0hX8qk',\n period: '201401',\n orgUnit: 'DiszpKrYNg8',\n value: '12',\n});", + "caption": "-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`" }, { "title": "example", - "description": "create('events', {\n program: 'eBAyeGv0exc',\n orgUnit: 'DiszpKrYNg8',\n eventDate: date,\n status: 'COMPLETED',\n completedDate: date,\n storedBy: 'admin',\n coordinate: {\n latitude: 59.8,\n longitude: 10.9,\n },\n dataValues: [\n {\n dataElement: 'qrur9Dvnyt5',\n value: '33',\n },\n {\n dataElement: 'oZg33kd9taw',\n value: 'Male',\n },\n {\n dataElement: 'msodh3rEMJa',\n value: date,\n },\n ],\n});", - "caption": "- Example `expression.js` of `create`" + "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" } ] }, "valid": true }, { - "name": "update", + "name": "get", "params": [ "resourceType", - "path", - "data", - "params", "options", "callback" ], "docs": { - "description": "Update data. A generic helper function to update a resource object of any type.\n- It requires to send `all required fields` or the `full body`", + "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.`", "tags": [ { - "title": "public", - "description": null, - "type": null - }, - { - "title": "function", - "description": null, - "name": null - }, - { - "title": "param", - "description": "The type of resource to be updated. E.g. `dataElements`, `organisationUnits`, etc.", - "type": { - "type": "NameExpression", - "name": "string" - }, - "name": "resourceType" - }, - { - "title": "param", - "description": "The `id` or `path` to the `object` to be updated. E.g. `FTRrcoaog83` or `FTRrcoaog83/{collection-name}/{object-id}`", - "type": { - "type": "NameExpression", - "name": "string" - }, - "name": "path" + "title": "public", + "description": null, + "type": null }, { - "title": "param", - "description": "Data to update. It requires to send `all required fields` or the `full body`. If you want `partial updates`, use `patch` operation.", - "type": { - "type": "NameExpression", - "name": "Object" - }, - "name": "data" + "title": "function", + "description": null, + "name": null }, { "title": "param", - "description": "Optional `update` parameters 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}", + "description": "The type of resource to get(use its `plural` name). E.g. `dataElements`, `trackedEntityInstances`,`organisationUnits`, etc.", "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "Object" - } + "type": "NameExpression", + "name": "string" }, - "name": "params" + "name": "resourceType" }, { "title": "param", - "description": "Optional options for update method. Defaults to `{operationName: 'update', apiVersion: state.configuration.apiVersion, responseType: 'json'}`", + "description": "`Optional` options for `getData` operation. Defaults to `{operationName: 'getData', apiVersion: state.configuration.apiVersion, responseType: 'json'}`.", "type": { "type": "OptionalType", "expression": { @@ -2605,7 +350,7 @@ }, { "type": "FieldType", - "key": "resourceType", + "key": "responseType", "value": { "type": "NameExpression", "name": "string" @@ -2630,7 +375,7 @@ }, { "title": "returns", - "description": null, + "description": "state", "type": { "type": "NameExpression", "name": "Operation" @@ -2638,25 +383,23 @@ }, { "title": "example", - "description": "update('dataElements', 'FTRrcoaog83',\n{\n displayName: 'New display name',\n aggregationType: 'SUM',\n domainType: 'AGGREGATE',\n valueType: 'NUMBER',\n name: 'Accute Flaccid Paralysis (Deaths < 5 yrs)',\n shortName: 'Accute Flaccid Paral (Deaths < 5 yrs)',\n});", - "caption": "Example `updating` a `data element`" + "description": "getData('trackedEntityInstances', {\n fields: '*',\n ou: 'DiszpKrYNg8',\n entityType: 'nEenWmSyUEp',\n trackedEntityInstance: 'dNpxRu1mWG5',\n});", + "caption": "Example getting one `trackedEntityInstance` with `Id` 'dNpxRu1mWG5' for a given `orgUnit(DiszpKrYNg8)`" } ] }, "valid": true }, { - "name": "patch", + "name": "upsert", "params": [ "resourceType", - "path", "data", - "params", "options", "callback" ], "docs": { - "description": "Patch a record. A generic helper function to send partial updates on one or more object properties.\n- You are not required to send the full body of object properties.\n- This is useful for cases where you don't want or need to update all properties on a object.", + "description": "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.", "tags": [ { "title": "public", @@ -2670,7 +413,7 @@ }, { "title": "param", - "description": "The type of resource to be updated. E.g. `dataElements`, `organisationUnits`, etc.", + "description": "The type of a resource to `insert` or `update`. E.g. `trackedEntityInstances`", "type": { "type": "NameExpression", "name": "string" @@ -2679,16 +422,7 @@ }, { "title": "param", - "description": "The `id` or `path` to the `object` to be updated. E.g. `FTRrcoaog83` or `FTRrcoaog83/{collection-name}/{object-id}`", - "type": { - "type": "NameExpression", - "name": "string" - }, - "name": "path" - }, - { - "title": "param", - "description": "Data to update. Include only the fields you want to update. E.g. `{name: \"New Name\"}`", + "description": "The update data containing new values", "type": { "type": "NameExpression", "name": "Object" @@ -2697,24 +431,20 @@ }, { "title": "param", - "description": "Optional `update` parameters 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}", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "Object" - } - }, - "name": "params" - }, - { - "title": "param", - "description": "Optional options for update method. Defaults to `{operationName: 'patch', apiVersion: state.configuration.apiVersion, responseType: 'json'}`", + "description": "`Optional` options for `upsertTEI` operation. Defaults to `{replace: false, apiVersion: state.configuration.apiVersion,strict: true,responseType: 'json'}`.", "type": { "type": "OptionalType", "expression": { "type": "RecordType", "fields": [ + { + "type": "FieldType", + "key": "replace", + "value": { + "type": "NameExpression", + "name": "boolean" + } + }, { "type": "FieldType", "key": "apiVersion", @@ -2725,10 +455,10 @@ }, { "type": "FieldType", - "key": "operationName", + "key": "strict", "value": { "type": "NameExpression", - "name": "string" + "name": "boolean" } }, { @@ -2756,6 +486,14 @@ }, "name": "callback" }, + { + "title": "throws", + "description": "Throws range error", + "type": { + "type": "NameExpression", + "name": "RangeError" + } + }, { "title": "returns", "description": null, @@ -2766,15 +504,85 @@ }, { "title": "example", - "description": "patch('dataElements', 'FTRrcoaog83',\n{\n name: 'New Name',\n});", - "caption": "Example `patching` a `data element`" + "description": "upsert(\n 'trackedEntityInstances',\n {\n attributeId: 'lZGmxYbs97q',\n attributeValue: state =>\n state.data.attributes.find(obj => obj.attribute === 'lZGmxYbs97q')\n .value,\n },\n state.data,\n { ou: 'TSyzvBiovKh' }\n);", + "caption": "Example `expression.js` of upsert" + }, + { + "title": "todo", + "description": "Tweak/refine to mimic implementation based on the following inspiration: {@link https://sqlite.org/lang_upsert.html sqlite upsert} and {@link https://wiki.postgresql.org/wiki/UPSERT postgresql upsert}" + }, + { + "title": "todo", + "description": "Test implementation for upserting metadata" + }, + { + "title": "todo", + "description": "Test implementation for upserting data values" + }, + { + "title": "todo", + "description": "Implement the updateCondition" } ] }, "valid": true }, { - "name": "del", + "name": "discover", + "params": [ + "httpMethod", + "endpoint" + ], + "docs": { + "description": "Discover `DHIS2` `api` `endpoint` `query parameters` and allowed `operators` for a given resource's endpoint.", + "tags": [ + { + "title": "public", + "description": null, + "type": null + }, + { + "title": "function", + "description": null, + "name": null + }, + { + "title": "param", + "description": "The HTTP to inspect parameter usage for a given endpoint, e.g., `get`, `post`,`put`,`patch`,`delete`", + "type": { + "type": "NameExpression", + "name": "string" + }, + "name": "httpMethod" + }, + { + "title": "param", + "description": "The path for a given endpoint. E.g. `/trackedEntityInstances` or `/dataValueSets`", + "type": { + "type": "NameExpression", + "name": "string" + }, + "name": "endpoint" + }, + { + "title": "returns", + "description": null, + "type": { + "type": "NameExpression", + "name": "Operation" + } + }, + { + "title": "example", + "description": "discover('post', '/trackedEntityInstances')", + "caption": "Example getting a list of `parameters allowed` on a given `endpoint` for specific `http method`" + } + ] + }, + "valid": true + }, + { + "name": "patch", "params": [ "resourceType", "path", @@ -2784,7 +592,7 @@ "callback" ], "docs": { - "description": "Delete a record. A generic helper function to delete an object", + "description": "Patch a record. A generic helper function to send partial updates on one or more object properties.\n- You are not required to send the full body of object properties.\n- This is useful for cases where you don't want or need to update all properties on a object.", "tags": [ { "title": "public", @@ -2798,7 +606,7 @@ }, { "title": "param", - "description": "The type of resource to be deleted. E.g. `trackedEntityInstances`, `organisationUnits`, etc.", + "description": "The type of resource to be updated. E.g. `dataElements`, `organisationUnits`, etc.", "type": { "type": "NameExpression", "name": "string" @@ -2807,7 +615,7 @@ }, { "title": "param", - "description": "Can be an `id` of an `object` or `path` to the `nested object` to `delete`.", + "description": "The `id` or `path` to the `object` to be updated. E.g. `FTRrcoaog83` or `FTRrcoaog83/{collection-name}/{object-id}`", "type": { "type": "NameExpression", "name": "string" @@ -2816,13 +624,10 @@ }, { "title": "param", - "description": "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}", + "description": "Data to update. Include only the fields you want to update. E.g. `{name: \"New Name\"}`", "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "Object" - } + "type": "NameExpression", + "name": "Object" }, "name": "data" }, @@ -2840,7 +645,7 @@ }, { "title": "param", - "description": "Optional `options` for `del` operation. Defaults to `{operationName: 'delete', apiVersion: state.configuration.apiVersion, responseType: 'json'}`", + "description": "Optional options for update method. Defaults to `{operationName: 'patch', apiVersion: state.configuration.apiVersion, responseType: 'json'}`", "type": { "type": "OptionalType", "expression": { @@ -2864,7 +669,7 @@ }, { "type": "FieldType", - "key": "resourceType", + "key": "responseType", "value": { "type": "NameExpression", "name": "string" @@ -2897,25 +702,25 @@ }, { "title": "example", - "description": "del('trackedEntityInstances', 'LcRd6Nyaq7T');", - "caption": "Example`deleting` a `tracked entity instance`" + "description": "patch('dataElements', 'FTRrcoaog83',\n{\n name: 'New Name',\n});", + "caption": "Example `patching` a `data element`" } ] }, "valid": true }, { - "name": "upsert", + "name": "del", "params": [ "resourceType", - "uniqueAttribute", + "path", "data", "params", "options", "callback" ], "docs": { - "description": "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.", + "description": "Delete a record. A generic helper function to delete an object", "tags": [ { "title": "public", @@ -2929,7 +734,7 @@ }, { "title": "param", - "description": "The type of a resource to `insert` or `update`. E.g. `trackedEntityInstances`", + "description": "The type of resource to be deleted. E.g. `trackedEntityInstances`, `organisationUnits`, etc.", "type": { "type": "NameExpression", "name": "string" @@ -2938,42 +743,28 @@ }, { "title": "param", - "description": "An object containing a `attributeId` and `attributeValue` which will be used to uniquely identify the record", + "description": "Can be an `id` of an `object` or `path` to the `nested object` to `delete`.", "type": { - "type": "RecordType", - "fields": [ - { - "type": "FieldType", - "key": "attributeId", - "value": { - "type": "NameExpression", - "name": "string" - } - }, - { - "type": "FieldType", - "key": "attributeValue", - "value": { - "type": "NameExpression", - "name": "any" - } - } - ] + "type": "NameExpression", + "name": "string" }, - "name": "uniqueAttribute" + "name": "path" }, { "title": "param", - "description": "The update data containing new values", + "description": "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}", "type": { - "type": "NameExpression", - "name": "Object" + "type": "OptionalType", + "expression": { + "type": "NameExpression", + "name": "Object" + } }, "name": "data" }, { "title": "param", - "description": "Optional `import` parameters e.g. `{ou: 'lZGmxYbs97q', filters: ['w75KJ2mc4zz:EQ:Jane']}`", + "description": "Optional `update` parameters 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}", "type": { "type": "OptionalType", "expression": { @@ -2985,20 +776,12 @@ }, { "title": "param", - "description": "`Optional` options for `upsertTEI` operation. Defaults to `{replace: false, apiVersion: state.configuration.apiVersion,strict: true,responseType: 'json'}`.", + "description": "Optional `options` for `del` operation. Defaults to `{operationName: 'delete', apiVersion: state.configuration.apiVersion, responseType: 'json'}`", "type": { "type": "OptionalType", "expression": { "type": "RecordType", "fields": [ - { - "type": "FieldType", - "key": "replace", - "value": { - "type": "NameExpression", - "name": "boolean" - } - }, { "type": "FieldType", "key": "apiVersion", @@ -3009,15 +792,15 @@ }, { "type": "FieldType", - "key": "strict", + "key": "operationName", "value": { "type": "NameExpression", - "name": "boolean" + "name": "string" } }, { "type": "FieldType", - "key": "responseType", + "key": "resourceType", "value": { "type": "NameExpression", "name": "string" @@ -3040,14 +823,6 @@ }, "name": "callback" }, - { - "title": "throws", - "description": "Throws range error", - "type": { - "type": "NameExpression", - "name": "RangeError" - } - }, { "title": "returns", "description": null, @@ -3058,24 +833,8 @@ }, { "title": "example", - "description": "upsert(\n 'trackedEntityInstances',\n {\n attributeId: 'lZGmxYbs97q',\n attributeValue: state =>\n state.data.attributes.find(obj => obj.attribute === 'lZGmxYbs97q')\n .value,\n },\n state.data,\n { ou: 'TSyzvBiovKh' }\n);", - "caption": "- Example `expression.js` of upsert" - }, - { - "title": "todo", - "description": "Tweak/refine to mimic implementation based on the following inspiration: {@link https://sqlite.org/lang_upsert.html sqlite upsert} and {@link https://wiki.postgresql.org/wiki/UPSERT postgresql upsert}" - }, - { - "title": "todo", - "description": "Test implementation for upserting metadata" - }, - { - "title": "todo", - "description": "Test implementation for upserting data values" - }, - { - "title": "todo", - "description": "Implement the updateCondition" + "description": "del('trackedEntityInstances', 'LcRd6Nyaq7T');", + "caption": "Example`deleting` a `tracked entity instance`" } ] }, diff --git a/lib/Adaptor.js b/lib/Adaptor.js index 6cbb0ee..f668843 100644 --- a/lib/Adaptor.js +++ b/lib/Adaptor.js @@ -4,106 +4,83 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.execute = execute; -exports.getTEIs = getTEIs; -exports.upsertTEI = upsertTEI; -exports.createTEI = createTEI; -exports.updateTEI = updateTEI; -exports.getEvents = getEvents; -exports.createEvents = createEvents; -exports.updateEvents = updateEvents; -exports.getPrograms = getPrograms; -exports.createPrograms = createPrograms; -exports.updatePrograms = updatePrograms; -exports.getEnrollments = getEnrollments; -exports.enrollTEI = enrollTEI; -exports.updateEnrollments = updateEnrollments; -exports.cancelEnrollment = cancelEnrollment; -exports.completeEnrollment = completeEnrollment; -exports.getRelationships = getRelationships; -exports.getDataValues = getDataValues; -exports.createDataValues = createDataValues; -exports.generateDhis2UID = generateDhis2UID; -exports.discover = discover; -exports.getAnalytics = getAnalytics; -exports.getResources = getResources; -exports.getSchema = getSchema; -exports.getData = getData; -exports.getMetadata = getMetadata; exports.create = create; exports.update = update; +exports.get = get; +exports.upsert = upsert; +exports.discover = discover; exports.patch = patch; exports.del = del; -exports.upsert = upsert; exports.attrVal = attrVal; Object.defineProperty(exports, "field", { enumerable: true, - get: function get() { + get: function () { return _languageCommon.field; } }); Object.defineProperty(exports, "fields", { enumerable: true, - get: function get() { + get: function () { return _languageCommon.fields; } }); Object.defineProperty(exports, "sourceValue", { enumerable: true, - get: function get() { + get: function () { return _languageCommon.sourceValue; } }); Object.defineProperty(exports, "merge", { enumerable: true, - get: function get() { + get: function () { return _languageCommon.merge; } }); Object.defineProperty(exports, "each", { enumerable: true, - get: function get() { + get: function () { return _languageCommon.each; } }); Object.defineProperty(exports, "dataPath", { enumerable: true, - get: function get() { + get: function () { return _languageCommon.dataPath; } }); Object.defineProperty(exports, "dataValue", { enumerable: true, - get: function get() { + get: function () { return _languageCommon.dataValue; } }); Object.defineProperty(exports, "lastReferenceValue", { enumerable: true, - get: function get() { + get: function () { return _languageCommon.lastReferenceValue; } }); Object.defineProperty(exports, "alterState", { enumerable: true, - get: function get() { + get: function () { return _languageCommon.alterState; } }); Object.defineProperty(exports, "fn", { enumerable: true, - get: function get() { + get: function () { return _languageCommon.fn; } }); Object.defineProperty(exports, "http", { enumerable: true, - get: function get() { + get: function () { return _languageCommon.http; } }); Object.defineProperty(exports, "attribute", { enumerable: true, - get: function get() { + get: function () { return _Utils.attribute; } }); @@ -116,33 +93,11 @@ var _lodash = require("lodash"); var _Utils = require("./Utils"); -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } - -function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); } - -function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } - -function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); } - -function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); } +var _Client = require("./Client"); -function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } -function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } - -function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } - -function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } - -function _iterableToArrayLimit(arr, i) { var _i = arr && (typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]); if (_i == null) return; var _arr = []; var _n = true; var _d = false; var _s, _e; try { for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } - -function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } - -function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; } - -function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +/** @module Adaptor */ /** * Execute a sequence of operations. @@ -156,17 +111,15 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope * @param {Operations} operations - Operations to be performed. * @returns {Operation} */ -function execute() { - for (var _len = arguments.length, operations = new Array(_len), _key = 0; _key < _len; _key++) { - operations[_key] = arguments[_key]; - } - - var initialState = { +function execute(...operations) { + const initialState = { references: [], data: null }; - return function (state) { - return _languageCommon.execute.apply(void 0, [configMigrationHelper].concat(operations))(_objectSpread({}, initialState, {}, state)); + return state => { + return (0, _languageCommon.execute)(configMigrationHelper, ...operations)({ ...initialState, + ...state + }); }; } /** @@ -181,9 +134,10 @@ function execute() { function configMigrationHelper(state) { - var _state$configuration = state.configuration, - hostUrl = _state$configuration.hostUrl, - apiUrl = _state$configuration.apiUrl; + const { + hostUrl, + apiUrl + } = state.configuration; if (!hostUrl) { _Utils.Log.warn('DEPRECATION WARNING: Please migrate instance address from `apiUrl` to `hostUrl`.'); @@ -195,15 +149,15 @@ function configMigrationHelper(state) { return state; } -_axios["default"].interceptors.response.use(function (response) { +_axios.default.interceptors.response.use(function (response) { var _response$headers$con, _response; - var contentType = (_response$headers$con = response.headers['content-type']) === null || _response$headers$con === void 0 ? void 0 : _response$headers$con.split(';')[0]; - var acceptHeaders = response.config.headers['Accept'].split(';')[0].split(','); + const contentType = (_response$headers$con = response.headers['content-type']) === null || _response$headers$con === void 0 ? void 0 : _response$headers$con.split(';')[0]; + const acceptHeaders = response.config.headers['Accept'].split(';')[0].split(','); if (response.config.method === 'get') { if ((0, _lodash.indexOf)(acceptHeaders, contentType) === -1) { - var newError = { + const newError = { status: 404, message: 'Unexpected content,returned', responseData: response.data @@ -217,9 +171,9 @@ _axios["default"].interceptors.response.use(function (response) { if (typeof ((_response = response) === null || _response === void 0 ? void 0 : _response.data) === 'string' && contentType === (_Utils.CONTENT_TYPES === null || _Utils.CONTENT_TYPES === void 0 ? void 0 : _Utils.CONTENT_TYPES.json)) { try { - response = _objectSpread({}, response, { + response = { ...response, data: JSON.parse(response.data) - }); + }; } catch (error) { /* Keep quiet */ } @@ -227,711 +181,472 @@ _axios["default"].interceptors.response.use(function (response) { return response; }, function (error) { - _Utils.Log.error("".concat(error === null || error === void 0 ? void 0 : error.message)); + console.log(error); - return Promise.reject(JSON.stringify(error.response.data, null, 2)); -}); + _Utils.Log.error(`${error === null || error === void 0 ? void 0 : error.message}`); -function expandAndSetOperation(options, state, operationName) { - return _objectSpread({ - operationName: operationName - }, (0, _languageCommon.expandReferences)(options)(state)); -} + return Promise.reject(error); +}); /** - * Get Tracked Entity Instance(s). + * Create a record * @public * @function - * @param {Object} [params] - Optional `query parameters` e.g. `{ou: 'DiszpKrYNg8', filters: ['lZGmxYbs97q':GT:5']}`. Run `discover` or see {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html DHIS2 docs} for more details on which params to use when querying tracked entities instances. - * @param {{apiVersion: number,responseType: string}} [options] - `Optional` options for `getTEIs` operation. Defaults to `{apiVersion: state.configuration.apiVersion, responseType: 'json'}`. - * @param {function} [callback] - Optional callback to handle the response. + * @param {string} resourceType - Type of resource to create. E.g. `trackedEntityInstances`, `programs`, `events`, ... + * @param {Object} data - 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 control the behavior of the `create` operation and to pass `import parameters` E.g. `{dryRun: true, importStrategy: CREATE}` See {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html DHIS2 API documentation} or {@link discover}..` Defaults to `{operationName: 'create', apiVersion: null, responseType: 'json'}` + * @param {function} [callback] - Optional callback to handle the response * @returns {Operation} - * @example - Example `getTEIs` `expression.js` for fetching a `single` `Tracked Entity Instance` with all the fields included. - * getTEIs({ - * fields: '*', - * ou: 'CMqUILyVnBL', - * trackedEntityInstance: 'HNTA9qD6EEG', - * skipPaging: true, + * + * @example -a `program` + * create('programs', { + * name: 'name 20', + * shortName: 'n20', + * programType: 'WITHOUT_REGISTRATION', * }); - */ - - -function getTEIs(params, options, callback) { - return function (state) { - var expandedOptions = expandAndSetOperation(options, state, 'getTEIs'); - return getData('trackedEntityInstances', params, expandedOptions, callback)(state); - }; -} -/** - * Update TEI if exists otherwise create. - * - Update if the record exists otherwise insert a new record. - * - This is useful for idempotency and duplicate record management. - * @public - * @function - * @param {string} uniqueAttributeId - Tracked Entity Instance unique identifier attribute used during matching. - * @param {Object} data - Payload data for new tracked entity instance or updated data for an existing tracked entity instance. - * @param {{apiVersion: number,strict: boolean,responseType: string}} [options] - `Optional` options for `upsertTEI` operation. Defaults to `{apiVersion: state.configuration.apiVersion,strict: true,responseType: 'json'}`. - * @param {function} [callback] - Optional `callback` to handle the response. - * @throws {RangeError} - Throws `RangeError` when `uniqueAttributeId` is `invalid` or `not unique`. - * @returns {Operation} - * @example - Example `expression.js` for upserting a tracked entity instance on attribute with Id `lZGmxYbs97q`. - * upsertTEI('lZGmxYbs97q', { + * + * @example -an `event` + * create('events', { + * program: 'eBAyeGv0exc', + * orgUnit: 'DiszpKrYNg8', + * status: 'COMPLETED', + * }); + * + * @example -a `trackedEntityInstance` + * create('trackedEntityInstances', { * orgUnit: 'TSyzvBiovKh', * trackedEntityType: 'nEenWmSyUEp', * attributes: [ * { - * attribute: 'lZGmxYbs97q', - * value: '77790012', - * }, - * { * attribute: 'w75KJ2mc4zz', * value: 'Gigiwe', * }, + * ] + * }); + * + * @example -a `dataSet` + * create('dataSets', { name: 'OpenFn Data Set', periodType: 'Monthly' }); + * + * @example -a `dataSetNotification` + * create('dataSetNotificationTemplates', { + * dataSetNotificationTrigger: 'DATA_SET_COMPLETION', + * notificationRecipient: 'ORGANISATION_UNIT_CONTACT', + * name: 'Notification', + * messageTemplate: 'Hello', + * deliveryChannels: ['SMS'], + * dataSets: [], + * }); + * + * @example -a `dataElement` + * create('dataElements', { + * aggregationType: 'SUM', + * domainType: 'AGGREGATE', + * valueType: 'NUMBER', + * name: 'Paracetamol', + * shortName: 'Para', + * }); + * + * @example -a `dataElementGroup` + * create('dataElementGroups', { + * name: 'Data Element Group 1', + * dataElements: [], + * }); + * + * @example -a `dataElementGroupSet` + * create('dataElementGroupSets', { + * name: 'Data Element Group Set 4', + * dataDimension: true, + * shortName: 'DEGS4', + * dataElementGroups: [], + * }); + * + * @example -a `dataValueSet` + * create('dataValueSets', { + * dataElement: 'f7n9E0hX8qk', + * period: '201401', + * orgUnit: 'DiszpKrYNg8', + * value: '12', + * }); + * + * @example -a `dataValueSet` with related `dataValues` + * create('dataValueSets', { + * dataSet: 'pBOMPrpg1QX', + * completeDate: '2014-02-03', + * period: '201401', + * orgUnit: 'DiszpKrYNg8', + * dataValues: [ + * { + * dataElement: 'f7n9E0hX8qk', + * value: '1', + * }, * { - * attribute: 'zDhUuAYrxNC', - * value: 'Mwanza', + * dataElement: 'Ix2HsbDMLea', + * value: '2', + * }, + * { + * dataElement: 'eY5ehpbEsB7', + * value: '3', * }, * ], * }); + * + * @example -an `enrollment` + * create('enrollments', { + * trackedEntityInstance: 'bmshzEacgxa', + * orgUnit: 'TSyzvBiovKh', + * program: 'gZBxv9Ujxg0', + * enrollmentDate: '2013-09-17', + * incidentDate: '2013-09-17', + * }); */ -function upsertTEI(uniqueAttributeId, data, options, callback) { - return function (state) { - var _expandedOptions$apiV, _expandedOptions$stri, _body$attributes, _body$attributes$find; - - uniqueAttributeId = (0, _languageCommon.expandReferences)(uniqueAttributeId)(state); - var body = (0, _languageCommon.expandReferences)(data)(state); - var expandedOptions = expandAndSetOperation(options, state, 'upsertTEI'); - var _state$configuration2 = state.configuration, - password = _state$configuration2.password, - username = _state$configuration2.username, - hostUrl = _state$configuration2.hostUrl; - var apiVersion = (_expandedOptions$apiV = expandedOptions === null || expandedOptions === void 0 ? void 0 : expandedOptions.apiVersion) !== null && _expandedOptions$apiV !== void 0 ? _expandedOptions$apiV : state.configuration.apiVersion; - var strict = (_expandedOptions$stri = expandedOptions === null || expandedOptions === void 0 ? void 0 : expandedOptions.strict) !== null && _expandedOptions$stri !== void 0 ? _expandedOptions$stri : true; - var params = { - ou: body.orgUnit - }; - var uniqueAttributeValue = (_body$attributes = body.attributes) === null || _body$attributes === void 0 ? void 0 : (_body$attributes$find = _body$attributes.find(function (obj) { - return (obj === null || obj === void 0 ? void 0 : obj.attribute) === uniqueAttributeId; - })) === null || _body$attributes$find === void 0 ? void 0 : _body$attributes$find.value; - var trackedEntityType = body.trackedEntityType; - var uniqueAttributeUrl = (0, _Utils.buildUrl)("/trackedEntityAttributes/".concat(uniqueAttributeId), hostUrl, apiVersion); - var trackedEntityTypeUrl = (0, _Utils.buildUrl)("/trackedEntityTypes/".concat(trackedEntityType, "?fields=*"), hostUrl, apiVersion); - - var findTrackedEntityType = function findTrackedEntityType() { - return _axios["default"].get(trackedEntityTypeUrl, { - auth: { - username: username, - password: password - } - }).then(function (result) { - var _result$data, _result$data$trackedE; - - var attribute = (_result$data = result.data) === null || _result$data === void 0 ? void 0 : (_result$data$trackedE = _result$data.trackedEntityTypeAttributes) === null || _result$data$trackedE === void 0 ? void 0 : _result$data$trackedE.find(function (obj) { - var _obj$trackedEntityAtt; - - return (obj === null || obj === void 0 ? void 0 : (_obj$trackedEntityAtt = obj.trackedEntityAttribute) === null || _obj$trackedEntityAtt === void 0 ? void 0 : _obj$trackedEntityAtt.id) === uniqueAttributeId; - }); - return _objectSpread({}, result.data, { - upsertAttributeAssigned: attribute ? true : false - }); - }); - }; - - var isAttributeUnique = function isAttributeUnique() { - return _axios["default"].get(uniqueAttributeUrl, { - auth: { - username: username, - password: password - } - }).then(function (result) { - var foundAttribute = result.data; - return { - unique: foundAttribute.unique, - name: foundAttribute.name - }; - }); - }; - - return Promise.all([findTrackedEntityType(), strict === true ? isAttributeUnique() : Promise.resolve({ - unique: true - })]).then(function (_ref) { - var _ref2 = _slicedToArray(_ref, 2), - entityType = _ref2[0], - attribute = _ref2[1]; - - if (!entityType.upsertAttributeAssigned) { - _Utils.Log.error(''); - - throw new RangeError("Tracked Entity Attribute ".concat(uniqueAttributeId, " is not assigned to the ").concat(entityType.name, " Entity Type.")); - } - - if (!attribute.unique) { - var _attribute$name; - - _Utils.Log.error(''); - - throw new RangeError("Attribute ".concat((_attribute$name = attribute.name) !== null && _attribute$name !== void 0 ? _attribute$name : '', "(").concat(uniqueAttributeId, ") is not marked as unique.")); +function create(resourceType, data, options, callback) { + const initialParams = { + resourceType, + data, + options, + callback + }; + return state => { + const { + url, + data, + resourceType, + auth, + urlParams, + callback + } = (0, _Utils.expandExtractAndLog)('create', initialParams)(state); + return (0, _Client.request)({ + method: 'post', + url, + data: (0, _Utils.nestArray)(data, resourceType), + options: { + auth, + params: urlParams } + }).then(result => { + _Utils.Log.info(`\nOperation succeeded. Created ${resourceType}: ${result.headers.location}.\n`); - return upsert('trackedEntityInstances', { - attributeId: uniqueAttributeId, - attributeValue: uniqueAttributeValue - }, body, params, expandedOptions, callback)(state); + if (callback) return callback((0, _languageCommon.composeNextState)(state, result.data)); + return (0, _languageCommon.composeNextState)(state, result.data); + }).catch(error => { + throw error; }); }; } /** - * Create Tracked Entity Instance. - * @public - * @function - * @param {Object} data - The update data containing new values. - * @param {Object} [params] - Optional `import parameters` for a given a resource. E.g. `{dryRun: true, importStrategy: CREATE}` See {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html#import-parameters_1 DHIS2 Import parameters documentation} or run `discover`. Defauls to `DHIS2 default import parameters`. - * @param {{apiVersion: number,responseType: string}} [options] - `Optional` options for `createTEI` operation. Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}`. - * @param {function} [callback] - Optional callback to handle the response. - * @returns {Operation} - * @example - Example `expression.js` of `createTEI`. - * createTEI({ - * orgUnit: 'TSyzvBiovKh', - * trackedEntityType: 'nEenWmSyUEp', - * attributes: [ - * { - * attribute: 'lZGmxYbs97q', - * value: valUpsertTEI, - * }, - * { - * attribute: 'w75KJ2mc4zz', - * value: 'Gigiwe', - * }, - * ], - * enrollments: [ - * { - * orgUnit: 'TSyzvBiovKh', - * program: 'fDd25txQckK', - * programState: 'lST1OZ5BDJ2', - * enrollmentDate: '2021-01-04', - * incidentDate: '2021-01-04', - * }, - * ], - * }); - */ - - -function createTEI(data, params, options, callback) { - return function (state) { - var expandedOptions = expandAndSetOperation(options, state, 'createTEI'); - return create('trackedEntityInstances', data, params, expandedOptions, callback)(state); - }; -} -/** - * Update a Tracked Entity Instance. + * Update data. A generic helper function to update a resource object of any type. + * Updating an object requires to send `all required fields` or the `full body` * @public * @function - * @param {string} path - Path to the object being updated. This can be an `id` or path to an `object` in a `nested collection` on the object(E.g. `/api/{collection-object}/{collection-object-id}/{collection-name}/{object-id}`). - * @param {Object} data - The update data containing new values. - * @param {Object} [params] - Optional `import parameters` for a given a resource. E.g. `{dryRun: true, importStrategy: CREATE, filters:[]}` See {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html#import-parameters_1 DHIS2 Import parameters documentation} or run `discover`. Defauls to `DHIS2 default import parameters`. - * @param {{apiVersion: number,responseType: string}} [options] - `Optional` options for `updateTEI` operation. Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}`. - * @param {function} [callback] - Optional callback to handle the response. + * @param {string} resourceType - The type of resource to be updated. E.g. `dataElements`, `organisationUnits`, etc. + * @param {string} path - The `id` or `path` to the `object` to be updated. E.g. `FTRrcoaog83` or `FTRrcoaog83/{collection-name}/{object-id}` + * @param {Object} data - Data to update. It requires to send `all required fields` or the `full body`. If you want `partial updates`, use `patch` operation. + * @param {{apiVersion: number,operationName: string,resourceType: string}} [options] - Optional options for update method. Defaults to `{operationName: 'update', apiVersion: state.configuration.apiVersion, responseType: 'json'}` + * @param {function} [callback] - Optional callback to handle the response * @returns {Operation} - * @example - Example `expression.js` of `updateTEI`. - * updateTEI('PVqUD2hvU4E', { - * orgUnit: 'TSyzvBiovKh', - * trackedEntityType: 'nEenWmSyUEp', - * attributes: [ - * { - * attribute: 'lZGmxYbs97q', - * value: valUpsertTEI, - * }, - * { - * attribute: 'w75KJ2mc4zz', - * value: 'Gigiwe', - * }, - * ], - * enrollments: [ - * { - * orgUnit: 'TSyzvBiovKh', - * program: 'fDd25txQckK', - * programState: 'lST1OZ5BDJ2', - * enrollmentDate: '2021-01-04', - * incidentDate: '2021-01-04', - * }, - * ], + * @example -a program + * update('programs', 'qAZJCrNJK8H', { + * name: '14e1aa02c3f0a31618e096f2c6d03bed', + * shortName: '14e1aa02', + * programType: 'WITHOUT_REGISTRATION', * }); - */ - - -function updateTEI(path, data, params, options, callback) { - return function (state) { - var expandedOptions = expandAndSetOperation(options, state, 'updateTEI'); - return update('trackedEntityInstances', path, data, params, expandedOptions, callback)(state); - }; -} -/** - * Get annonymous events or tracker events. - * @public - * @function - * @param {Object} params - `import` parameters for `getEvents`. See examples here - * @param {{apiVersion: number,responseType: string}} [options] - `Optional` options for `getEvents` operation. Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}`. - * @param {function} [callback] - Optional callback to handle the response. - * @returns {Operation} - * @example - Query for `all events` with `children` of a certain `organisation unit` - * getEvents({ orgUnit: 'YuQRtpLP10I', ouMode: 'CHILDREN' }); - */ - - -function getEvents(params, options, callback) { - return function (state) { - var expandedOptions = expandAndSetOperation(options, state, 'getEvents'); - return getData('events', params, expandedOptions, callback)(state); - }; -} -/** - * Create DHIS2 Events - * - You will need a `program` which can be looked up using the `getPrograms` operation, an `orgUnit` which can be looked up using the `getMetadata` operation and passing `{organisationUnits: true}` as `resources` param, and a list of `valid data element identifiers` which can be looked up using the `getMetadata` passing `{dataElements: true}` as `resources` param. - * - For events with registration, a `tracked entity instance identifier is required` - * - For sending `events` to `programs with multiple stages`, you will need to also include the `programStage` identifier, the identifiers for `programStages` can be found in the `programStages` resource via a call to `getMetadata` operation. - * @public - * @function - * @param {Object} data - The payload containing new values - * @param {Object} [params] - Optional `import parameters` for events. E.g. `{dryRun: true, importStrategy: CREATE, filters:[]}` See {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html#events DHIS2 Event Import parameters documentation} or run `discover`. Defauls to `DHIS2 default import parameters`. - * @param {{apiVersion: number,responseType: string}} [options] - Optional `flags` for the behavior of the `createEvents` operation.Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}` - * @param {function} [callback] - Optional callback to handle the response - * @returns {Operation} state - * @example - Example `expression.js` of `createEvents` for a `single event` can look like this: - * createEvents({ + * + * @example an `event` + * update('events', 'PVqUD2hvU4E', { * program: 'eBAyeGv0exc', - * orgUnit: 'DiszpKrYNg8', - * eventDate: date, + * orgUnit: 'Ngelehun CHC', * status: 'COMPLETED', - * completedDate: date, * storedBy: 'admin', - * coordinate: { - * latitude: 59.8, - * longitude: 10.9, - * }, + * dataValues: [], + * }); + * + * @example a `trackedEntityInstance` + * update('trackedEntityInstances', 'IeQfgUtGPq2', { + * created: '2015-08-06T21:12:37.256', + * orgUnit: 'TSyzvBiovKh', + * createdAtClient: '2015-08-06T21:12:37.256', + * trackedEntityInstance: 'IeQfgUtGPq2', + * lastUpdated: '2015-08-06T21:12:37.257', + * trackedEntityType: 'nEenWmSyUEp', + * inactive: false, + * deleted: false, + * featureType: 'NONE', + * programOwners: [ + * { + * ownerOrgUnit: 'TSyzvBiovKh', + * program: 'IpHINAT79UW', + * trackedEntityInstance: 'IeQfgUtGPq2', + * }, + * ], + * enrollments: [], + * relationships: [], + * attributes: [ + * { + * lastUpdated: '2016-01-12T00:00:00.000', + * displayName: 'Last name', + * created: '2016-01-12T00:00:00.000', + * valueType: 'TEXT', + * attribute: 'zDhUuAYrxNC', + * value: 'Russell', + * }, + * { + * 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: 'Catherine', + * }, + * ], + * }); + * + * @example -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, + * deliveryChannels: ['SMS'], + * dataSets: [], + * }); + * + * @example -a `dataElement` + * update('dataElements', 'FTRrcoaog83', { + * aggregationType: 'SUM', + * domainType: 'AGGREGATE', + * valueType: 'NUMBER', + * name: 'Paracetamol', + * shortName: 'Para', + * }); + * + * @example -a `dataElementGroup` + * update('dataElementGroups', 'QrprHT61XFk', { + * name: 'Data Element Group 1', + * dataElements: [], + * }); + * + * @example -a `dataElementGroupSet` + * update('dataElementGroupSets', 'VxWloRvAze8', { + * name: 'Data Element Group Set 4', + * dataDimension: true, + * shortName: 'DEGS4', + * dataElementGroups: [], + * }); + * + * @example -a `dataValueSet` + * update('dataValueSets', 'AsQj6cDsUq4', { + * dataElement: 'f7n9E0hX8qk', + * period: '201401', + * orgUnit: 'DiszpKrYNg8', + * value: '12', + * }); + * + * @example -a `dataValueSet` with related `dataValues` + * update('dataValueSets', 'Ix2HsbDMLea', { + * dataSet: 'pBOMPrpg1QX', + * completeDate: '2014-02-03', + * period: '201401', + * orgUnit: 'DiszpKrYNg8', * dataValues: [ * { - * dataElement: 'qrur9Dvnyt5', - * value: '33', + * dataElement: 'f7n9E0hX8qk', + * value: '1', * }, * { - * dataElement: 'oZg33kd9taw', - * value: 'Male', + * dataElement: 'Ix2HsbDMLea', + * value: '2', * }, * { - * dataElement: 'msodh3rEMJa', - * value: date, + * dataElement: 'eY5ehpbEsB7', + * value: '3', * }, * ], * }); + * + * @example a single enrollment + * update('enrollments', 'CmsHzercTBa' { + * trackedEntityInstance: 'bmshzEacgxa', + * orgUnit: 'TSyzvBiovKh', + * program: 'gZBxv9Ujxg0', + * enrollmentDate: '2013-10-17', + * incidentDate: '2013-10-17', + * }); */ -function createEvents(data, params, options, callback) { - return function (state) { - var expandedOptions = expandAndSetOperation(options, state, 'createEvents'); - return create('events', data, params, expandedOptions, callback)(state); +function update(resourceType, path, data, options, callback) { + const initialParams = { + resourceType, + path, + data, + options, + callback + }; + return state => { + const { + url, + data, + resourceType, + auth, + _, + callback + } = (0, _Utils.expandExtractAndLog)('update', initialParams)(state); + return (0, _Client.request)({ + method: 'put', + url, + data, + options: { + auth + } + }).then(result => { + _Utils.Log.info(`\nOperation succeeded. Updated ${resourceType} ${path}.\n`); + + if (callback) return callback((0, _languageCommon.composeNextState)(state, result.data)); + return (0, _languageCommon.composeNextState)(state, result.data); + }).catch(error => { + throw error; + }); }; } /** - * Update DHIS2 Event. - * - To update an existing event, the format of the payload is the same as that of `creating an event` via `createEvents` operations - * - But you should supply the `identifier` of the object you are updating - * - The payload has to contain `all`, even `non-modified`, `attributes`. - * - Attributes that were present before and are not present in the current payload any more will be removed by DHIS2. - * - If you do not want this behavior, please use `upsert` operation to upsert your events. + * Get data. Generic helper method for getting data of any kind from DHIS2. + * - This can be used to get `DataValueSets`,`events`,`trackedEntityInstances`,`etc.` * @public * @function - * @param {string} path - Path to the object being updated. This can be an `id` or path to an `object` in a `nested collection` on the object(E.g. `/api/{collection-object}/{collection-object-id}/{collection-name}/{object-id}`) - * @param {Object} data - The update data containing new values - * @param {Object} [params] - Optional `import` parameters for `updateEvents`. E.g. `{dryRun: true, IdScheme: 'CODE'}. Defaults to DHIS2 `default params` - * @param {{apiVersion: number,responseType: string}} [options] - Optional `flags` for the behavior of the `updateEvents` operation.Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}` - * @param {function} [callback] - Optional callback to handle the response - * @returns {Operation} - * @example - Example `expression.js` of `updateEvents` - * updateEvents('PVqUD2hvU4E', { events: [ - * { - * program: 'eBAyeGv0exc', - * orgUnit: 'DiszpKrYNg8', - * eventDate: date, - * status: 'COMPLETED', - * storedBy: 'admin', - * coordinate: { - * latitude: '59.8', - * longitude: '10.9', - * }, - * dataValues: [ - * { - * dataElement: 'qrur9Dvnyt5', - * value: '22', - * }, - * { - * dataElement: 'oZg33kd9taw', - * value: 'Male', - * }, - * ], - * }] + * @param {string} resourceType - The type of resource to get(use its `plural` name). E.g. `dataElements`, `trackedEntityInstances`,`organisationUnits`, etc. + * @param {{apiVersion: number,operationName: string,responseType: string}}[options] - `Optional` options for `getData` operation. Defaults to `{operationName: 'getData', apiVersion: state.configuration.apiVersion, responseType: 'json'}`. + * @param {function} [callback] - Optional callback to handle the response + * @returns {Operation} state + * @example Example getting one `trackedEntityInstance` with `Id` 'dNpxRu1mWG5' for a given `orgUnit(DiszpKrYNg8)` + * getData('trackedEntityInstances', { + * fields: '*', + * ou: 'DiszpKrYNg8', + * entityType: 'nEenWmSyUEp', + * trackedEntityInstance: 'dNpxRu1mWG5', * }); */ -function updateEvents(path, data, params, options, callback) { - return function (state) { - var expandedOptions = expandAndSetOperation(options, state, 'updateEvents'); - return update('events', path, data, params, expandedOptions, callback)(state); +function get(resourceType, options, callback) { + const initialParams = { + resourceType, + options, + callback }; -} -/** - * Get DHIS2 Tracker Programs. - * @public - * @function - * @param {Object} params - `import` parameters for `getPrograms`. See {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html#tracker-web-api DHIS2 api documentation for allowed query parameters } - * @param {{apiVersion: number,responseType: string}} [options] - Optional `flags` for the behavior of the `getPrograms` operation.Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}` - * @param {function} [callback] - Optional callback to handle the response - * @returns {Operation} - * @example - Query for `all programs` with a certain `organisation unit` - * getPrograms({ orgUnit: 'DiszpKrYNg8' , fields: '*' }); - */ - + return state => { + const { + url: url, + _data, + _resourceType, + auth, + urlParams, + callback + } = (0, _Utils.expandExtractAndLog)('get', initialParams)(state); + return (0, _Client.request)({ + method: 'get', + url, + options: { + auth, + params: urlParams, + responseType: 'json' + } + }).then(result => { + _Utils.Log.info(`\nOperation succeeded. Retrieved ${result.data[resourceType].length} ${resourceType}.\n`); -function getPrograms(params, options, callback) { - return function (state) { - var expandedOptions = expandAndSetOperation(options, state, 'getPrograms'); - return getData('programs', params, expandedOptions, callback)(state); + if (callback) return callback((0, _languageCommon.composeNextState)(state, result.data)); + return (0, _languageCommon.composeNextState)(state, result.data); + }).catch(error => { + throw error; + }); }; } /** - * Create a DHIS2 Tracker Program + * 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 `insert` or `update`. E.g. `trackedEntityInstances` * @param {Object} data - The update data containing new values - * @param {Object} [params] - Optional `import` parameters for `createPrograms`. E.g. `{dryRun: true, IdScheme: 'CODE'}. Defaults to DHIS2 `default params` - * @param {{apiVersion: number,responseType: string}} [options] - Optional `flags` for the behavior of the `getPrograms` operation.Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}` + * @param {{replace:boolean, apiVersion: number,strict: boolean,responseType: string}} [options] - `Optional` options for `upsertTEI` operation. Defaults to `{replace: false, apiVersion: state.configuration.apiVersion,strict: true,responseType: 'json'}`. * @param {function} [callback] - Optional callback to handle the response + * @throws {RangeError} - Throws range error * @returns {Operation} - * @example - Example `expression.js` of `createPrograms` for a `single program` can look like this: - * createPrograms(state.data); + * @example Example `expression.js` of upsert + * upsert( + * 'trackedEntityInstances', + * { + * attributeId: 'lZGmxYbs97q', + * attributeValue: state => + * state.data.attributes.find(obj => obj.attribute === 'lZGmxYbs97q') + * .value, + * }, + * state.data, + * { ou: 'TSyzvBiovKh' } + * ); + * @todo Tweak/refine to mimic implementation based on the following inspiration: {@link https://sqlite.org/lang_upsert.html sqlite upsert} and {@link https://wiki.postgresql.org/wiki/UPSERT postgresql upsert} + * @todo Test implementation for upserting metadata + * @todo Test implementation for upserting data values + * @todo Implement the updateCondition */ -function createPrograms(data, params, options, callback) { - return function (state) { - var expandedOptions = expandAndSetOperation(options, state, 'createPrograms'); - return create('programs', data, expandedOptions, params, callback)(state); +function upsert(resourceType, data, options, callback) { + return state => { + return get(resourceType, options)(state).then(res => { + const resources = res.data[resourceType]; + + if (resources.length > 1) { + throw new RangeError(`Cannot upsert on Non-unique attribute. The operation found more than one records for your request.`); + } else if (resources.length <= 0) { + return create(resourceType, data, options, callback)(state); + } else { + const pathName = resourceType === 'trackedEntityInstances' ? 'trackedEntityInstance' : 'id'; + const path = resources[0][pathName]; + return update(resourceType, path, data, options, callback)(state); + } + }).catch(err => { + throw err; + }); }; } /** - * Update DHIS2 Tracker Programs - * - To update an existing program, the format of the payload is the same as that of `creating an event` via `createEvents` operations - * - But you should supply the `identifier` of the object you are updating - * - The payload has to contain `all`, even `non-modified`, `attributes`. - * - Attributes that were present before and are not present in the current payload any more will be removed by DHIS2. - * - If you do not want this behavior, please use `upsert` operation to upsert your events. + * Discover `DHIS2` `api` `endpoint` `query parameters` and allowed `operators` for a given resource's endpoint. * @public * @function - * @param {string} path - Path to the object being updated. This can be an `id` or path to an `object` in a `nested collection` on the object(E.g. `/api/{collection-object}/{collection-object-id}/{collection-name}/{object-id}`) - * @param {Object} data - The update data containing new values - * @param {Object} [params] - Optional `import` parameters for `updatePrograms`. E.g. `{dryRun: true, IdScheme: 'CODE'}. Defaults to DHIS2 `default params` - * @param {{apiVersion: number,responseType: string}} [options] - Optional `flags` for the behavior of the `getPrograms` operation.Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}` - * @param {function} [callback] - Optional callback to handle the response + * @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` * @returns {Operation} - * @example - Example `expression.js` of `updatePrograms` - * updatePrograms('PVqUD2hvU4E', state.data); + * @example Example getting a list of `parameters allowed` on a given `endpoint` for specific `http method` + * discover('post', '/trackedEntityInstances') */ -function updatePrograms(path, data, params, options, callback) { - return function (state) { - var expandedOptions = expandAndSetOperation(options, state, 'updatePrograms'); - return update('programs', path, data, params, expandedOptions, callback)(state); - }; -} -/** - * Get DHIS2 Enrollments - * @public - * @function - * @param {Object} params - `Query` parameters for `getEnrollments`. See {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html#enrollment-management here} - * @param {{apiVersion: number,responseType: string}} [options] - Optional `flags` for the behavior of the `getEnrollments` operation.Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}` - * @param {function} [callback] - Optional callback to handle the response - * @returns {Operation} - * @example - To constrain the response to `enrollments` which are part of a `specific program` you can include a `program query parameter` - * getEnrollments({ - * ou: 'O6uvpzGd5pu', - * ouMode: 'DESCENDANTS', - * program: 'ur1Edk5Oe2n', - * fields: '*', - * }); - */ +function discover(httpMethod, endpoint) { + return state => { + _Utils.Log.info(`Discovering query/import parameters for ${httpMethod} on ${endpoint}`); + + return _axios.default.get('https://dhis2.github.io/dhis2-api-specification/spec/metadata_openapi.json', { + transformResponse: [data => { + let tempData = JSON.parse(data); + let filteredData = tempData.paths[endpoint][httpMethod]; + return { ...filteredData, + parameters: filteredData.parameters.reduce((acc, currentValue) => { + let index = currentValue['$ref'].lastIndexOf('/') + 1; + let paramRef = currentValue['$ref'].slice(index); + let param = tempData.components.parameters[paramRef]; - -function getEnrollments(params, options, callback) { - return function (state) { - var expandedOptions = expandAndSetOperation(options, state, 'getEnrollments'); - return getData('enrollments', params, expandedOptions, callback)(state); - }; -} -/** - * Enroll a TEI into a program - * - Enrolling a tracked entity instance into a program - * - For enrolling `persons` into a `program`, you will need to first get the `identifier of the person` from the `trackedEntityInstances resource` via the `getTEIs` operation. - * - Then, you will need to get the `program identifier` from the `programs` resource via the `getPrograms` operation. - * @public - * @function - * @param {Object} data - The enrollment data. See example {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html#enrollment-management here } - * @param {Object} [params] - Optional `import` parameters for `createEnrollment`. E.g. `{dryRun: true, IdScheme: 'CODE'}. Defaults to DHIS2 `default params` - * @param {{apiVersion: number,responseType: string}} [options] - Optional `flags` for the behavior of the `enrollTEI` operation.Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}` - * @param {function} [callback] - Optional callback to handle the response - * @returns {Operation} - * @example - Example `expression.js` of `createEnrollment` of a `person` into a `program` can look like this: - * enrollTEI({ - * trackedEntity: 'tracked-entity-id', - * orgUnit: 'org-unit-id', - * attributes: [ - * { - * attribute: 'attribute-id', - * value: 'attribute-value', - * }, - * ], - * enrollments: [ - * { - * orgUnit: 'org-unit-id', - * program: 'program-id', - * enrollmentDate: '2013-09-17', - * incidentDate: '2013-09-17', - * }, - * ], - *}); - */ - - -function enrollTEI(data, params, options, callback) { - return function (state) { - var expandedOptions = expandAndSetOperation(options, state, 'enrollTEI'); - return create('enrollments', data, expandedOptions, params, callback)(state); - }; -} -/** - * Update a DHIS2 Enrollemts - * - To update an existing enrollment, the format of the payload is the same as that of `creating an event` via `createEvents` operations - * - But you should supply the `identifier` of the object you are updating - * - The payload has to contain `all`, even `non-modified`, `attributes`. - * - Attributes that were present before and are not present in the current payload any more will be removed by DHIS2. - * - If you do not want this behavior, please use `upsert` operation to upsert your events. - * @public - * @function - * @param {string} path - Path to the object being updated. This can be an `id` or path to an `object` in a `nested collection` on the object(E.g. `/api/{collection-object}/{collection-object-id}/{collection-name}/{object-id}`) - * @param {Object} data - The update data containing new values - * @param {Object} [params] - Optional `import` parameters for `updateEnrollments`. E.g. `{dryRun: true, IdScheme: 'CODE'}. Defaults to DHIS2 `default params` - * @param {{apiVersion: number,responseType: string}} [options] - Optional `flags` for the behavior of the `updateEnrollments` operation.Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}` - * @param {function} [callback] - Optional callback to handle the response - * @returns {Operation} - * @example - Example `expression.js` of `updateEnromments` - * updateEnrollments('PVqUD2hvU4E', state.data); - */ - - -function updateEnrollments(path, data, params, options, callback) { - return function (state) { - var expandedOptions = expandAndSetOperation(options, state, 'updateEnrollments'); - return update('enrollments', path, data, params, expandedOptions, callback)(state); - }; -} -/** - * Cancel a DHIS2 Enrollment - * - To cancel an existing enrollment, you should supply the `enrollment identifier`(`enrollemt-id`) - * @public - * @function - * @param {string} enrollmentId - The `enrollment-id` of the enrollment you wish to cancel - * @param {Object} [params] - Optional `import` parameters for `cancelEnrollment`. E.g. `{dryRun: true, IdScheme: 'CODE'}. Defaults to DHIS2 `default params` - * @param {{apiVersion: number,responseType: string}} [options] - Optional `flags` for the behavior of the `cancelEnrollment` operation.Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}` - * @param {function} [callback] - Optional callback to handle the response - * @returns {Operation} - * @example - Example `expression.js` of `cancelEnrollment` - * cancelEnrollments('PVqUD2hvU4E'); - */ - - -function cancelEnrollment(enrollmentId, params, options, callback) { - return function (state) { - enrollmentId = (0, _languageCommon.expandReferences)(enrollmentId)(state); - var path = "".concat(enrollmentId, "/cancelled"); - var expandedOptions = expandAndSetOperation(options, state, 'cancelEnrollment'); - return update('enrollments', path, null, params, expandedOptions, callback)(state); - }; -} -/** - * Complete a DHIS2 Enrollment - * - To complete an existing enrollment, you should supply the `enrollment identifier`(`enrollemt-id`) - * @public - * @function - * @param {string} enrollmentId - The `enrollment-id` of the enrollment you wish to cancel - * @param {Object} [params] - Optional `import` parameters for `completeEnrollment`. E.g. `{dryRun: true, IdScheme: 'CODE'}. Defaults to DHIS2 `default params` - * @param {{apiVersion: number,responseType: string}} [options] - Optional `flags` for the behavior of the `completeEnrollment` operation.Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}` - * @param {function} [callback] - Optional callback to handle the response - * @returns {Operation} - * @example - Example `expression.js` of `completeEnrollment` - * completeEnrollment('PVqUD2hvU4E'); - */ - - -function completeEnrollment(enrollmentId, params, options, callback) { - return function (state) { - enrollmentId = (0, _languageCommon.expandReferences)(enrollmentId)(state); - var path = "".concat(enrollmentId, "/completed"); - var expandedOptions = expandAndSetOperation(options, state, 'enrollments'); - return update('enrollments', path, null, params, expandedOptions, callback)(state); - }; -} -/** - * Get DHIS2 Relationships(links) between two entities in tracker. These entities can be tracked entity instances, enrollments and events. - * - All the tracker operations, `getTEIs`, `getEnrollments` and `getEvents` also list their relationships if requested in the `field` filter. - * - To list all relationships, this requires you to provide the UID of the trackedEntityInstance, Enrollment or event that you want to list all the relationships for. - * @public - * @function - * @param {Object} params - `Query` parameters for `getRelationships`. See examples {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html#relationships here} - * @param {{apiVersion: number,responseType: string}} [options] - Optional `flags` for the behavior of the `getRelationships` operation.Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}` - * @param {function} [callback] - Optional callback to handle the response - * @returns {Operation} - * @example - A query for `all relationships` associated with a `specific tracked entity instance` can look like this: - * getRelationships({ tei: 'F8yKM85NbxW', fields: '*' }); - */ - - -function getRelationships(params, options, callback) { - return function (state) { - var expandedOptions = expandAndSetOperation(options, state, 'getRelationships'); - return getData('relationships', params, expandedOptions, callback)(state); - }; -} -/** - * Get DHIS2 Data Values. - * - This operation retrives data values from DHIS2 Web API by interacting with the `dataValueSets` resource - * - Data values can be retrieved in XML, JSON and CSV format. - * @public - * @function - * @param {Object} params - `Query` parameters for `getDataValues`. E.g. `{dataset: 'pBOMPrpg1QX', limit: 3, period: 2021, orgUnit: 'DiszpKrYNg8'} Run `discover` or see {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html#data-values DHIS2 API docs} for available `Data Value Set Query Parameters`. - * @param {{apiVersion: number,responseType: string}} [options] - Optional `options` for `getDataValues` operation. Defaults to `{apiVersion: state.configuration.apiVersion, responseType: 'json'}` - * @param {function} [callback] - Optional `callback` to handle the response - * @returns {Operation} - * @example - Example getting **two** `data values` associated with a specific `orgUnit`, `dataSet`, and `period ` - * getDataValues({ - * orgUnit: 'DiszpKrYNg8', - * period: '202010', - * dataSet: 'pBOMPrpg1QX', - * limit: 2, - * }); - */ - - -function getDataValues(params, options, callback) { - return function (state) { - var expandedOptions = expandAndSetOperation(options, state, 'getDataValues'); - return getData('dataValueSets', params, expandedOptions, callback)(state); - }; -} -/** - * Create DHIS2 Data Values - * - This is used to send aggregated data to DHIS2 - * - A data value set represents a set of data values which have a relationship, usually from being captured off the same data entry form. - * - To send a set of related data values sharing the same period and organisation unit, we need to identify the period, the data set, the org unit (facility) and the data elements for which to report. - * - You can also use this operation to send large bulks of data values which don't necessarily are logically related. - * - To send data values that are not linked to a `dataSet`, you do not need to specify the dataSet and completeDate attributes. Instead, you will specify the period and orgUnit attributes on the individual data value elements instead of on the outer data value set element. This will enable us to send data values for various periods and organisation units - * @public - * @function - * @param {object} data - The `data values` to upload or create. See example shape. - * @param {{apiVersion: number,responseType: string}} [options] - Optional `flags` for the behavior of the `createDataVaues` operation. - * @param {object} [params] - Optional `import` parameters for `createDataValues`. E.g. `{dryRun: true, IdScheme: 'CODE'}. Defaults to DHIS2 `default params`. Run `discover` or visit {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html#data-values DHIS2 Docs API} to learn about available data values import parameters. - * @param {function} [callback] - Optional callback to handle the response - * @returns {Operation} - * @example - Example `expression.js` of `createDataValues` for sending a set of related data values sharing the same period and organisation unit - * createDataValues({ - * dataSet: 'pBOMPrpg1QX', - * completeDate: '2014-02-03', - * period: '201401', - * orgUnit: 'DiszpKrYNg8', - * dataValues: [ - * { - * dataElement: 'f7n9E0hX8qk', - * value: '1', - * }, - * { - * dataElement: 'Ix2HsbDMLea', - * value: '2', - * }, - * { - * dataElement: 'eY5ehpbEsB7', - * value: '3', - * }, - * ], - * }); - */ - - -function createDataValues(data, options, params, callback) { - return function (state) { - var expandedOptions = expandAndSetOperation(options, state, 'createDataValues'); - return create('dataValueSets', data, expandedOptions, params, callback)(state); - }; -} -/** - * Generate valid, random DHIS2 identifiers - * - Useful for client generated Ids compatible with DHIS2 - * @public - * @function - * @param {{apiVersion: number,limit: number,responseType: string}} [options] - Optional `options` for `generateDhis2UID` operation. Defaults to `{apiVersion: state.configuration.apiVersion,limit: 1,responseType: 'json'}` - * @param {function} [callback] - Callback to handle response - * @returns {Operation} - * @example Example generating `three UIDs` from the DHIS2 server - * generateDhis2UID({limit: 3}); - */ - - -function generateDhis2UID(options, callback) { - return function (state) { - var _options$limit; - - var expandedOptions = expandAndSetOperation(options, state, 'generateDhis2UID'); - var limit = { - limit: (_options$limit = options === null || options === void 0 ? void 0 : options.limit) !== null && _options$limit !== void 0 ? _options$limit : 1 - }; - options === null || options === void 0 ? true : delete options.limit; - return getData('system/id', limit, expandedOptions, callback)(state); - }; -} -/** - * Discover `DHIS2` `api` `endpoint` `query parameters` and allowed `operators` for a given resource's endpoint. - * @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` - * @returns {Operation} - * @example Example getting a list of `parameters allowed` on a given `endpoint` for specific `http method` - * discover('post', '/trackedEntityInstances') - */ - - -function discover(httpMethod, endpoint) { - return function (state) { - _Utils.Log.info("Discovering query/import parameters for ".concat(httpMethod, " on ").concat(endpoint)); - - return _axios["default"].get('https://dhis2.github.io/dhis2-api-specification/spec/metadata_openapi.json', { - transformResponse: [function (data) { - var tempData = JSON.parse(data); - var filteredData = tempData.paths[endpoint][httpMethod]; - return _objectSpread({}, filteredData, { - parameters: filteredData.parameters.reduce(function (acc, currentValue) { - var index = currentValue['$ref'].lastIndexOf('/') + 1; - var paramRef = currentValue['$ref'].slice(index); - var param = tempData.components.parameters[paramRef]; - - if (param.schema['$ref']) { - var schemaRefIndex = param.schema['$ref'].lastIndexOf('/') + 1; - var schemaRef = param.schema['$ref'].slice(schemaRefIndex); - param.schema = tempData.components.schemas[schemaRef]; - } + if (param.schema['$ref']) { + let schemaRefIndex = param.schema['$ref'].lastIndexOf('/') + 1; + let schemaRef = param.schema['$ref'].slice(schemaRefIndex); + param.schema = tempData.components.schemas[schemaRef]; + } param.schema = JSON.stringify(param.schema); - var descIndex; + let descIndex; if ((0, _lodash.indexOf)(param.description, ',') === -1 && (0, _lodash.indexOf)(param.description, '.') > -1) descIndex = (0, _lodash.indexOf)(param.description, '.');else if ((0, _lodash.indexOf)(param.description, ',') > -1 && (0, _lodash.indexOf)(param.description, '.') > -1) { descIndex = (0, _lodash.indexOf)(param.description, '.') < (0, _lodash.indexOf)(param.description, ',') ? (0, _lodash.indexOf)(param.description, '.') : (0, _lodash.indexOf)(param.description, ','); } else { @@ -941,491 +656,19 @@ function discover(httpMethod, endpoint) { acc[paramRef] = param; return acc; }, {}) - }); + }; }] - }).then(function (result) { + }).then(result => { var _result$data$descript; - _Utils.Log.info("\t=======================================================================================\n\tQuery Parameters for ".concat(httpMethod, " on ").concat(endpoint, " [").concat((_result$data$descript = result.data.description) !== null && _result$data$descript !== void 0 ? _result$data$descript : '', "]\n\t=======================================================================================")); + _Utils.Log.info(`\t=======================================================================================\n\tQuery Parameters for ${httpMethod} on ${endpoint} [${(_result$data$descript = result.data.description) !== null && _result$data$descript !== void 0 ? _result$data$descript : ''}]\n\t=======================================================================================`); console.table(result.data.parameters, ['in', 'required', 'description']); console.table(result.data.parameters, ['schema']); - console.log("=========================================Responses===============================\n".concat((0, _Utils.prettyJson)(result.data.responses), "\n=======================================================================================")); - return _objectSpread({}, state, { + console.log(`=========================================Responses===============================\n${(0, _Utils.prettyJson)(result.data.responses)}\n=======================================================================================`); + return { ...state, data: result.data - }); - }); - }; -} -/** - * Get analytical, aggregated data - * - The analytics resource is powerful as it lets you query and retrieve data aggregated along all available data dimensions. - * - For instance, you can ask the analytics resource to provide the aggregated data values for a set of data elements, periods and organisation units. - * - Also, you can retrieve the aggregated data for a combination of any number of dimensions based on data elements and organisation unit group sets. - * @public - * @function - * @param {Object} params - Analytics `query parameters`, e.g. `{dx: 'fbfJHSPpUQD;cYeuwXTCPkU',filters: ['pe:2014Q1;2014Q2','ou:O6uvpzGd5pu;lc3eMKXaEfw']}`. Run `discover` or visit {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html#analytics DHIS2 API docs} to get the params available. - * @param {{apiVersion: number,responseType: string}}[options] - `Optional` options for `getAnalytics` operation. Defaults to `{apiVersion: state.configuration.apiVersion, responseType: 'json'}`. - * @param {function} [callback] - Callback to handle response - * @returns {Operation} - * @example Example getting only records where the data value is greater or equal to 6500 and less than 33000 - * getAnalytics({ - * dimensions: [ - * 'dx:fbfJHSPpUQD;cYeuwXTCPkU', - * 'pe:2014', - * 'ou:O6uvpzGd5pu;lc3eMKXaEfw', - * ], - * measureCriteria: 'GE:6500;LT:33000', - * }); - */ - - -function getAnalytics(params, options, callback) { - return function (state) { - var expandedOptions = expandAndSetOperation(options, state, 'getAnalytics'); - return getData("analytics", params, expandedOptions, callback)(state); - }; -} -/** - * Get DHIS2 api resources - * @public - * @function - * @param {Object} [params] - The `optional` query parameters for this endpoint. E.g `{filter: 'singular:like:attribute'}`. - * @param {{filter: string, fields: string, responseType: string}} [options] - The `optional` options, specifiying the filter expression. E.g. `singular:eq:attribute`. - * @param {function} [callback] - The `optional callback function that will be called to handle data returned by this function. - * @returns {Operation} - * @example Example getting a resource named `attribute`, in `xml` format, returning all the fields - * getResources('dataElement', { - * filter: 'singular:eq:attribute', - * fields: '*', - * responseType: 'xml', - * }); - */ - - -function getResources(params, options, callback) { - return function (state) { - var _options$responseType, _options, _params, _CONTENT_TYPES$respon; - - params = (0, _languageCommon.expandReferences)(params)(state); - options = (0, _languageCommon.expandReferences)(options)(state); - var operationName = 'getResources'; - var _state$configuration3 = state.configuration, - username = _state$configuration3.username, - password = _state$configuration3.password, - hostUrl = _state$configuration3.hostUrl; - var responseType = (_options$responseType = (_options = options) === null || _options === void 0 ? void 0 : _options.responseType) !== null && _options$responseType !== void 0 ? _options$responseType : 'json'; - var filter = (_params = params) === null || _params === void 0 ? void 0 : _params.filter; - var queryParams = params; - var headers = { - Accept: (_CONTENT_TYPES$respon = _Utils.CONTENT_TYPES[responseType]) !== null && _CONTENT_TYPES$respon !== void 0 ? _CONTENT_TYPES$respon : 'application/json' - }; - var path = '/resources'; - var url = (0, _Utils.buildUrl)(path, hostUrl, null, false); - - var transformResponse = function transformResponse(data, headers) { - if (filter) { - var _headers$contentType, _headers$contentType2; - - if (((_headers$contentType = (_headers$contentType2 = headers['content-type']) === null || _headers$contentType2 === void 0 ? void 0 : _headers$contentType2.split(';')[0]) !== null && _headers$contentType !== void 0 ? _headers$contentType : null) === _Utils.CONTENT_TYPES.json) { - var tempData = JSON.parse(data); - return _objectSpread({}, tempData, { - resources: _Utils.applyFilter.apply(void 0, [tempData.resources].concat(_toConsumableArray((0, _Utils.parseFilter)(filter)))) - }); - } else { - _Utils.Log.warn('Filters on this resource are only supported for json content types. Skipping filtering ...'); - } - } - - return data; - }; - - (0, _Utils.logOperation)(operationName); - (0, _Utils.logWaitingForServer)(url, queryParams); - (0, _Utils.warnExpectLargeResult)(queryParams, url); - return _axios["default"].request({ - url: url, - method: 'GET', - auth: { - username: username, - password: password - }, - responseType: responseType, - headers: headers, - params: queryParams, - transformResponse: transformResponse - }).then(function (result) { - _Utils.Log.info("".concat(operationName, " succeeded. The result of this operation will be in ").concat(operationName, " state.data or in your ").concat(operationName, " callback.")); - - if (callback) return callback((0, _languageCommon.composeNextState)(state, result.data)); - return (0, _languageCommon.composeNextState)(state, result.data); - }); - }; -} -/** - * Get schema of a given resource type, in any data format supported by DHIS2 - * @public - * @function - * @param {string} resourceType - The type of resource to be updated(`singular` version of the `resource name`). E.g. `dataElement`, `organisationUnit`, etc. Run `getResources` to see available resources and their corresponding `singular` names. - * @param {Object} params - Optional `query parameters` for the `getSchema` operation. e.g. `{ fields: 'properties' ,skipPaging: true}`. Run`discover` or See {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html#metadata-export-examples DHIS2 API Docs} - * @param {{apiVersion: number,resourceType: string}} [options] - Optional options for `getSchema` method. Defaults to `{apiVersion: state.configuration.apiVersion, responseType: 'json'}` - * @param {function} [callback] - Optional `callback` to handle the response - * @returns {Operation} - * @example Example getting the `schema` for `dataElement` in XML - * getSchema('dataElement', '{ fields: '*' }, { responseType: 'xml' }); - */ - - -function getSchema(resourceType, params, options, callback) { - return function (state) { - var _options$responseType2, _options2, _params2, _params3, _options$apiVersion, _options3, _CONTENT_TYPES$respon2, _resourceType; - - resourceType = (0, _languageCommon.expandReferences)(resourceType)(state); - params = (0, _languageCommon.expandReferences)(params)(state); - options = (0, _languageCommon.expandReferences)(options)(state); - var operationName = 'getSchema'; - var _state$configuration4 = state.configuration, - username = _state$configuration4.username, - password = _state$configuration4.password, - hostUrl = _state$configuration4.hostUrl; - var responseType = (_options$responseType2 = (_options2 = options) === null || _options2 === void 0 ? void 0 : _options2.responseType) !== null && _options$responseType2 !== void 0 ? _options$responseType2 : 'json'; - var filters = (_params2 = params) === null || _params2 === void 0 ? void 0 : _params2.filters; - (_params3 = params) === null || _params3 === void 0 ? true : delete _params3.filters; - var queryParams = new URLSearchParams(params); - filters === null || filters === void 0 ? void 0 : filters.map(function (f) { - return queryParams.append('filter', f); - }); - var apiVersion = (_options$apiVersion = (_options3 = options) === null || _options3 === void 0 ? void 0 : _options3.apiVersion) !== null && _options$apiVersion !== void 0 ? _options$apiVersion : state.configuration.apiVersion; - var headers = { - Accept: (_CONTENT_TYPES$respon2 = _Utils.CONTENT_TYPES[responseType]) !== null && _CONTENT_TYPES$respon2 !== void 0 ? _CONTENT_TYPES$respon2 : 'application/json' - }; - var url = (0, _Utils.buildUrl)("/schemas/".concat((_resourceType = resourceType) !== null && _resourceType !== void 0 ? _resourceType : ''), hostUrl, apiVersion); - (0, _Utils.logOperation)(operationName); - (0, _Utils.logApiVersion)(apiVersion); - (0, _Utils.logWaitingForServer)(url, queryParams); - (0, _Utils.warnExpectLargeResult)(resourceType, url); - return _axios["default"].request({ - method: 'GET', - url: url, - auth: { - username: username, - password: password - }, - responseType: responseType, - params: queryParams, - headers: headers - }).then(function (result) { - _Utils.Log.info("".concat(operationName, " succeeded. The result of this operation will be in ").concat(operationName, " state.data or in your ").concat(operationName, " callback.")); - - if (callback) return callback((0, _languageCommon.composeNextState)(state, result.data)); - return (0, _languageCommon.composeNextState)(state, result.data); - }); - }; -} -/** - * Get data. Generic helper method for getting data of any kind from DHIS2. - * - This can be used to get `DataValueSets`,`events`,`trackedEntityInstances`,`etc.` - * @public - * @function - * @param {string} resourceType - The type of resource to get(use its `plural` name). E.g. `dataElements`, `trackedEntityInstances`,`organisationUnits`, etc. - * @param {Object} [params] - Optional `query parameters` e.g. `{ou: 'DiszpKrYNg8'}`. Run `discover` or see {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html DHIS2 docs} for more details on which params to use for a given type of resource. - * @param {{apiVersion: number,operationName: string,responseType: string}}[options] - `Optional` options for `getData` operation. Defaults to `{operationName: 'getData', apiVersion: state.configuration.apiVersion, responseType: 'json'}`. - * @param {function} [callback] - Optional callback to handle the response - * @returns {Operation} state - * @example Example getting one `trackedEntityInstance` with `Id` 'dNpxRu1mWG5' for a given `orgUnit(DiszpKrYNg8)` - * getData('trackedEntityInstances', { - * fields: '*', - * ou: 'DiszpKrYNg8', - * entityType: 'nEenWmSyUEp', - * trackedEntityInstance: 'dNpxRu1mWG5', - * }); - */ - - -function getData(resourceType, params, options, callback) { - return function (state) { - var _options$operationNam, _options4, _options$responseType3, _options5, _params4, _params5, _params6, _options$apiVersion2, _options6, _CONTENT_TYPES$respon3; - - resourceType = (0, _languageCommon.expandReferences)(resourceType)(state); - params = (0, _languageCommon.expandReferences)(params)(state); - options = (0, _languageCommon.expandReferences)(options)(state); - var operationName = (_options$operationNam = (_options4 = options) === null || _options4 === void 0 ? void 0 : _options4.operationName) !== null && _options$operationNam !== void 0 ? _options$operationNam : 'getData'; - var _state$configuration5 = state.configuration, - username = _state$configuration5.username, - password = _state$configuration5.password, - hostUrl = _state$configuration5.hostUrl; - var responseType = (_options$responseType3 = (_options5 = options) === null || _options5 === void 0 ? void 0 : _options5.responseType) !== null && _options$responseType3 !== void 0 ? _options$responseType3 : 'json'; - var filters = (_params4 = params) === null || _params4 === void 0 ? void 0 : _params4.filters; - var dimensions = (_params5 = params) === null || _params5 === void 0 ? void 0 : _params5.dimensions; - (_params6 = params) === null || _params6 === void 0 ? true : delete _params6.filters; - var queryParams = new URLSearchParams(params); - filters === null || filters === void 0 ? void 0 : filters.map(function (f) { - return queryParams.append('filter', f); - }); - dimensions === null || dimensions === void 0 ? void 0 : dimensions.map(function (d) { - return queryParams.append('dimension', d); - }); - var apiVersion = (_options$apiVersion2 = (_options6 = options) === null || _options6 === void 0 ? void 0 : _options6.apiVersion) !== null && _options$apiVersion2 !== void 0 ? _options$apiVersion2 : state.configuration.apiVersion; - var headers = { - Accept: (_CONTENT_TYPES$respon3 = _Utils.CONTENT_TYPES[responseType]) !== null && _CONTENT_TYPES$respon3 !== void 0 ? _CONTENT_TYPES$respon3 : 'application/json' - }; - var url = (0, _Utils.buildUrl)("/".concat(resourceType), hostUrl, apiVersion); - (0, _Utils.logOperation)(operationName); - (0, _Utils.logApiVersion)(apiVersion); - (0, _Utils.logWaitingForServer)(url, queryParams); - (0, _Utils.warnExpectLargeResult)(resourceType, url); - return _axios["default"].request({ - method: 'GET', - url: url, - auth: { - username: username, - password: password - }, - responseType: responseType, - params: queryParams, - headers: headers - }).then(function (result) { - _Utils.Log.info("".concat(operationName, " succeeded. The result of this operation will be in ").concat(operationName, " state.data or in your callback.")); - - if (callback) return callback((0, _languageCommon.composeNextState)(state, result.data)); - return (0, _languageCommon.composeNextState)(state, result.data); - }); - }; -} -/** - * Get metadata. A generic helper function to get metadata records from a given DHIS2 instance - * @public - * @function - * @param {string[]} resources - Required. List of metadata resources to fetch. E.g. `['organisationUnits', 'attributes']` or like `'dataSets'` if you only want a single type of resource. See `getResources` to see the types of resources available. - * @param {Object} [params] - Optional `query parameters` e.g. `{filters: ['name:like:ANC'],fields:'*'}`. See `discover` or visit {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html#metadata-export DHIS2 API docs} - * @param {{apiVersion: number,operationName: string,resourceType: string}} [options] - Optional `options` for `getMetadata` operation. Defaults to `{operationName: 'getMetadata', apiVersion: state.configuration.apiVersion, responseType: 'json'}` - * @param {function} [callback] - Optional `callback` to handle the response - * @returns {Operation} - * @example Example getting a list of `data elements` and `indicators` where `name` includes the word **ANC** - * getMetadata(['dataElements', 'indicators'], { - * filters: ['name:like:ANC'], - * }); - */ - - -function getMetadata(resources, params, options, callback) { - return function (state) { - var _options$responseType4, _options7, _queryParams, _queryParams2, _options$apiVersion3, _options8, _CONTENT_TYPES$respon4; - - resources = (0, _languageCommon.expandReferences)(resources)(state); - params = (0, _languageCommon.expandReferences)(params)(state); - options = (0, _languageCommon.expandReferences)(options)(state); - var operationName = 'getMetadata'; - var _state$configuration6 = state.configuration, - username = _state$configuration6.username, - password = _state$configuration6.password, - hostUrl = _state$configuration6.hostUrl; - var responseType = (_options$responseType4 = (_options7 = options) === null || _options7 === void 0 ? void 0 : _options7.responseType) !== null && _options$responseType4 !== void 0 ? _options$responseType4 : 'json'; - - if (typeof resources === 'string') { - var res = {}; - res[resources] = true; - resources = res; - } else { - resources = resources.reduce(function (acc, currentValue) { - acc[currentValue] = true; - return acc; - }, {}); - } - - var queryParams = _objectSpread({}, resources, {}, params); - - var filters = (_queryParams = queryParams) === null || _queryParams === void 0 ? void 0 : _queryParams.filters; - (_queryParams2 = queryParams) === null || _queryParams2 === void 0 ? true : delete _queryParams2.filters; - queryParams = new URLSearchParams(queryParams); - filters === null || filters === void 0 ? void 0 : filters.map(function (f) { - return queryParams.append('filter', f); - }); - var apiVersion = (_options$apiVersion3 = (_options8 = options) === null || _options8 === void 0 ? void 0 : _options8.apiVersion) !== null && _options$apiVersion3 !== void 0 ? _options$apiVersion3 : state.configuration.apiVersion; - var headers = { - Accept: (_CONTENT_TYPES$respon4 = _Utils.CONTENT_TYPES[responseType]) !== null && _CONTENT_TYPES$respon4 !== void 0 ? _CONTENT_TYPES$respon4 : 'application/json' - }; - var url = (0, _Utils.buildUrl)('/metadata', hostUrl, apiVersion); - (0, _Utils.logOperation)(operationName); - (0, _Utils.logApiVersion)(apiVersion); - (0, _Utils.logWaitingForServer)(url, queryParams); - (0, _Utils.warnExpectLargeResult)(queryParams, url); - return _axios["default"].request({ - method: 'GET', - url: url, - auth: { - username: username, - password: password - }, - responseType: responseType, - params: queryParams, - headers: headers - }).then(function (result) { - _Utils.Log.info("".concat(operationName, " succeeded. The result of this operation will be in ").concat(operationName, " state.data or in your callback.")); - - if (callback) return callback((0, _languageCommon.composeNextState)(state, result.data)); - return (0, _languageCommon.composeNextState)(state, result.data); - }); - }; -} -/** - * create data. A generic helper method to create a record of any kind in DHIS2 - * @public - * @function - * @param {string} resourceType - Type of resource to create. E.g. `trackedEntityInstances` - * @param {Object} data - Data that will be used to create a given instance of resource - * @param {Object} [options] - Optional `options` to control the behavior of the `create` operation.` Defaults to `{operationName: 'create', apiVersion: null, responseType: 'json'}` - * @param {Object} [params] - Optional `import parameters` for a given a resource. E.g. `{dryRun: true, importStrategy: CREATE}` See {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html DHIS2 API documentation} or {@link discover}. Defauls to `DHIS2 default params` for a given resource type. - * @param {function} [callback] - Optional callback to handle the response - * @returns {Operation} - * @example - Example `expression.js` of `create` - * create('events', { - * program: 'eBAyeGv0exc', - * orgUnit: 'DiszpKrYNg8', - * eventDate: date, - * status: 'COMPLETED', - * completedDate: date, - * storedBy: 'admin', - * coordinate: { - * latitude: 59.8, - * longitude: 10.9, - * }, - * dataValues: [ - * { - * dataElement: 'qrur9Dvnyt5', - * value: '33', - * }, - * { - * dataElement: 'oZg33kd9taw', - * value: 'Male', - * }, - * { - * dataElement: 'msodh3rEMJa', - * value: date, - * }, - * ], - * }); - */ - - -function create(resourceType, data, options, params, callback) { - return function (state) { - var _options$operationNam2, _options9, _options$responseType5, _options10, _params7, _params8, _options$apiVersion4, _options11, _CONTENT_TYPES$respon5; - - resourceType = (0, _languageCommon.expandReferences)(resourceType)(state); - var body = (0, _languageCommon.expandReferences)(data)(state); - options = (0, _languageCommon.expandReferences)(options)(state); - params = (0, _languageCommon.expandReferences)(params)(state); - var operationName = (_options$operationNam2 = (_options9 = options) === null || _options9 === void 0 ? void 0 : _options9.operationName) !== null && _options$operationNam2 !== void 0 ? _options$operationNam2 : 'create'; - var _state$configuration7 = state.configuration, - username = _state$configuration7.username, - password = _state$configuration7.password, - hostUrl = _state$configuration7.hostUrl; - var responseType = (_options$responseType5 = (_options10 = options) === null || _options10 === void 0 ? void 0 : _options10.responseType) !== null && _options$responseType5 !== void 0 ? _options$responseType5 : 'json'; - var filters = (_params7 = params) === null || _params7 === void 0 ? void 0 : _params7.filters; - (_params8 = params) === null || _params8 === void 0 ? true : delete _params8.filters; - var queryParams = new URLSearchParams(params); - var apiVersion = (_options$apiVersion4 = (_options11 = options) === null || _options11 === void 0 ? void 0 : _options11.apiVersion) !== null && _options$apiVersion4 !== void 0 ? _options$apiVersion4 : state.configuration.apiVersion; - var url = (0, _Utils.buildUrl)('/' + resourceType, hostUrl, apiVersion); - var headers = { - Accept: (_CONTENT_TYPES$respon5 = _Utils.CONTENT_TYPES[responseType]) !== null && _CONTENT_TYPES$respon5 !== void 0 ? _CONTENT_TYPES$respon5 : 'application/json', - 'Content-Type': 'application/json' - }; - (0, _Utils.logOperation)(operationName); - (0, _Utils.logApiVersion)(apiVersion); - (0, _Utils.logWaitingForServer)(url, queryParams); - (0, _Utils.warnExpectLargeResult)(resourceType, url); - return _axios["default"].request({ - method: 'POST', - url: url, - auth: { - username: username, - password: password - }, - params: queryParams, - data: body, - headers: headers - }).then(function (result) { - var _result$data$response, _result$data$response2; - - _Utils.Log.info("".concat(operationName, " succeeded. Created ").concat(resourceType, ": ").concat(((_result$data$response = result.data.response) === null || _result$data$response === void 0 ? void 0 : _result$data$response.importSummaries) ? result.data.response.importSummaries[0].href : (_result$data$response2 = result.data.response) === null || _result$data$response2 === void 0 ? void 0 : _result$data$response2.reference, ".\nSummary:\n").concat((0, _Utils.prettyJson)(result.data))); - - if (callback) return callback((0, _languageCommon.composeNextState)(state, result.data)); - return (0, _languageCommon.composeNextState)(state, result.data); - }); - }; -} -/** - * Update data. A generic helper function to update a resource object of any type. - * - It requires to send `all required fields` or the `full body` - * @public - * @function - * @param {string} resourceType - The type of resource to be updated. E.g. `dataElements`, `organisationUnits`, etc. - * @param {string} path - The `id` or `path` to the `object` to be updated. E.g. `FTRrcoaog83` or `FTRrcoaog83/{collection-name}/{object-id}` - * @param {Object} data - Data to update. It requires to send `all required fields` or the `full body`. If you want `partial updates`, use `patch` operation. - * @param {Object} [params] - Optional `update` parameters 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} - * @param {{apiVersion: number,operationName: string,resourceType: string}} [options] - Optional options for update method. Defaults to `{operationName: 'update', apiVersion: state.configuration.apiVersion, responseType: 'json'}` - * @param {function} [callback] - Optional callback to handle the response - * @returns {Operation} - * @example Example `updating` a `data element` - * update('dataElements', 'FTRrcoaog83', - * { - * displayName: 'New display name', - * aggregationType: 'SUM', - * domainType: 'AGGREGATE', - * valueType: 'NUMBER', - * name: 'Accute Flaccid Paralysis (Deaths < 5 yrs)', - * shortName: 'Accute Flaccid Paral (Deaths < 5 yrs)', - * }); - * - */ - - -function update(resourceType, path, data, params, options, callback) { - return function (state) { - var _options$operationNam3, _options12, _options$responseType6, _options13, _params9, _params10, _options$apiVersion5, _options14, _CONTENT_TYPES$respon6; - - resourceType = (0, _languageCommon.expandReferences)(resourceType)(state); - path = (0, _languageCommon.expandReferences)(path)(state); - var body = (0, _languageCommon.expandReferences)(data)(state); - params = (0, _languageCommon.expandReferences)(params)(state); - options = (0, _languageCommon.expandReferences)(options)(state); - var _state$configuration8 = state.configuration, - username = _state$configuration8.username, - password = _state$configuration8.password, - hostUrl = _state$configuration8.hostUrl; - var operationName = (_options$operationNam3 = (_options12 = options) === null || _options12 === void 0 ? void 0 : _options12.operationName) !== null && _options$operationNam3 !== void 0 ? _options$operationNam3 : 'update'; - var responseType = (_options$responseType6 = (_options13 = options) === null || _options13 === void 0 ? void 0 : _options13.responseType) !== null && _options$responseType6 !== void 0 ? _options$responseType6 : 'json'; - var filters = (_params9 = params) === null || _params9 === void 0 ? void 0 : _params9.filters; - (_params10 = params) === null || _params10 === void 0 ? true : delete _params10.filters; - var queryParams = new URLSearchParams(params); - filters === null || filters === void 0 ? void 0 : filters.map(function (f) { - return queryParams.append('filter', f); - }); - var apiVersion = (_options$apiVersion5 = (_options14 = options) === null || _options14 === void 0 ? void 0 : _options14.apiVersion) !== null && _options$apiVersion5 !== void 0 ? _options$apiVersion5 : state.configuration.apiVersion; - var url = (0, _Utils.buildUrl)('/' + resourceType + '/' + path, hostUrl, apiVersion); - var headers = { - Accept: (_CONTENT_TYPES$respon6 = _Utils.CONTENT_TYPES[responseType]) !== null && _CONTENT_TYPES$respon6 !== void 0 ? _CONTENT_TYPES$respon6 : 'application/json' - }; - (0, _Utils.logOperation)(operationName); - (0, _Utils.logApiVersion)(apiVersion); - (0, _Utils.logWaitingForServer)(url, queryParams); - (0, _Utils.warnExpectLargeResult)(resourceType, url); - return _axios["default"].request({ - method: 'PUT', - url: url, - auth: { - username: username, - password: password - }, - params: queryParams, - data: body, - headers: headers - }).then(function (result) { - _Utils.Log.info("".concat(operationName, " succeeded. Updated ").concat(resourceType, ".\nSummary:\n").concat((0, _Utils.prettyJson)(result.data))); - - if (callback) return callback((0, _languageCommon.composeNextState)(state, result.data)); - return (0, _languageCommon.composeNextState)(state, result.data); + }; }); }; } @@ -1451,53 +694,52 @@ function update(resourceType, path, data, params, options, callback) { function patch(resourceType, path, data, params, options, callback) { - return function (state) { - var _options$operationNam4, _options15, _options$responseType7, _options16, _queryParams3, _queryParams4, _options$apiVersion6, _options17, _CONTENT_TYPES$respon7; + return state => { + var _options$operationNam, _options, _options$responseType, _options2, _queryParams, _queryParams2, _options$apiVersion, _options3, _CONTENT_TYPES$respon; resourceType = (0, _languageCommon.expandReferences)(resourceType)(state); path = (0, _languageCommon.expandReferences)(path)(state); - var body = (0, _languageCommon.expandReferences)(data)(state); + const body = (0, _languageCommon.expandReferences)(data)(state); params = (0, _languageCommon.expandReferences)(params)(state); options = (0, _languageCommon.expandReferences)(options)(state); - var operationName = (_options$operationNam4 = (_options15 = options) === null || _options15 === void 0 ? void 0 : _options15.operationName) !== null && _options$operationNam4 !== void 0 ? _options$operationNam4 : 'patch'; - var _state$configuration9 = state.configuration, - username = _state$configuration9.username, - password = _state$configuration9.password, - hostUrl = _state$configuration9.hostUrl; - var responseType = (_options$responseType7 = (_options16 = options) === null || _options16 === void 0 ? void 0 : _options16.responseType) !== null && _options$responseType7 !== void 0 ? _options$responseType7 : 'json'; - var queryParams = params; - var filters = (_queryParams3 = queryParams) === null || _queryParams3 === void 0 ? void 0 : _queryParams3.filters; - (_queryParams4 = queryParams) === null || _queryParams4 === void 0 ? true : delete _queryParams4.filters; + const operationName = (_options$operationNam = (_options = options) === null || _options === void 0 ? void 0 : _options.operationName) !== null && _options$operationNam !== void 0 ? _options$operationNam : 'patch'; + const { + username, + password, + hostUrl + } = state.configuration; + const responseType = (_options$responseType = (_options2 = options) === null || _options2 === void 0 ? void 0 : _options2.responseType) !== null && _options$responseType !== void 0 ? _options$responseType : 'json'; + let queryParams = params; + const filters = (_queryParams = queryParams) === null || _queryParams === void 0 ? void 0 : _queryParams.filters; + (_queryParams2 = queryParams) === null || _queryParams2 === void 0 ? true : delete _queryParams2.filters; queryParams = new URLSearchParams(queryParams); - filters === null || filters === void 0 ? void 0 : filters.map(function (f) { - return queryParams.append('filter', f); - }); - var apiVersion = (_options$apiVersion6 = (_options17 = options) === null || _options17 === void 0 ? void 0 : _options17.apiVersion) !== null && _options$apiVersion6 !== void 0 ? _options$apiVersion6 : state.configuration.apiVersion; - var url = (0, _Utils.buildUrl)('/' + resourceType + '/' + path, hostUrl, apiVersion); - var headers = { - Accept: (_CONTENT_TYPES$respon7 = _Utils.CONTENT_TYPES[responseType]) !== null && _CONTENT_TYPES$respon7 !== void 0 ? _CONTENT_TYPES$respon7 : 'application/json' + filters === null || filters === void 0 ? void 0 : filters.map(f => queryParams.append('filter', f)); + const apiVersion = (_options$apiVersion = (_options3 = options) === null || _options3 === void 0 ? void 0 : _options3.apiVersion) !== null && _options$apiVersion !== void 0 ? _options$apiVersion : state.configuration.apiVersion; + const url = (0, _Utils.buildUrl)('/' + resourceType + '/' + path, hostUrl, apiVersion); + const headers = { + Accept: (_CONTENT_TYPES$respon = _Utils.CONTENT_TYPES[responseType]) !== null && _CONTENT_TYPES$respon !== void 0 ? _CONTENT_TYPES$respon : 'application/json' }; (0, _Utils.logOperation)(operationName); (0, _Utils.logApiVersion)(apiVersion); (0, _Utils.logWaitingForServer)(url, queryParams); (0, _Utils.warnExpectLargeResult)(resourceType, url); - return _axios["default"].request({ + return _axios.default.request({ method: 'PATCH', - url: url, + url, auth: { - username: username, - password: password + username, + password }, params: queryParams, data: body, - headers: headers - }).then(function (result) { - var resultObject = { + headers + }).then(result => { + let resultObject = { status: result.status, statusText: result.statusText }; - _Utils.Log.info("".concat(operationName, " succeeded. Updated ").concat(resourceType, ".\nSummary:\n").concat((0, _Utils.prettyJson)(resultObject))); + _Utils.Log.info(`${operationName} succeeded. Updated ${resourceType}.\nSummary:\n${(0, _Utils.prettyJson)(resultObject)}`); if (callback) return callback((0, _languageCommon.composeNextState)(state, resultObject)); return (0, _languageCommon.composeNextState)(state, resultObject); @@ -1521,244 +763,53 @@ function patch(resourceType, path, data, params, options, callback) { function del(resourceType, path, data, params, options, callback) { - return function (state) { - var _options$operationNam5, _options18, _options$responseType8, _options19, _queryParams5, _queryParams6, _options$apiVersion7, _options20, _CONTENT_TYPES$respon8; + return state => { + var _options$operationNam2, _options4, _options$responseType2, _options5, _queryParams3, _queryParams4, _options$apiVersion2, _options6, _CONTENT_TYPES$respon2; resourceType = (0, _languageCommon.expandReferences)(resourceType)(state); path = (0, _languageCommon.expandReferences)(path)(state); - var body = (0, _languageCommon.expandReferences)(data)(state); + const body = (0, _languageCommon.expandReferences)(data)(state); params = (0, _languageCommon.expandReferences)(params)(state); options = (0, _languageCommon.expandReferences)(options)(state); - var operationName = (_options$operationNam5 = (_options18 = options) === null || _options18 === void 0 ? void 0 : _options18.operationName) !== null && _options$operationNam5 !== void 0 ? _options$operationNam5 : 'delete'; - var _state$configuration10 = state.configuration, - username = _state$configuration10.username, - password = _state$configuration10.password, - hostUrl = _state$configuration10.hostUrl; - var responseType = (_options$responseType8 = (_options19 = options) === null || _options19 === void 0 ? void 0 : _options19.responseType) !== null && _options$responseType8 !== void 0 ? _options$responseType8 : 'json'; - var queryParams = params; - var filters = (_queryParams5 = queryParams) === null || _queryParams5 === void 0 ? void 0 : _queryParams5.filters; - (_queryParams6 = queryParams) === null || _queryParams6 === void 0 ? true : delete _queryParams6.filters; + const operationName = (_options$operationNam2 = (_options4 = options) === null || _options4 === void 0 ? void 0 : _options4.operationName) !== null && _options$operationNam2 !== void 0 ? _options$operationNam2 : 'delete'; + const { + username, + password, + hostUrl + } = state.configuration; + const responseType = (_options$responseType2 = (_options5 = options) === null || _options5 === void 0 ? void 0 : _options5.responseType) !== null && _options$responseType2 !== void 0 ? _options$responseType2 : 'json'; + let queryParams = params; + const filters = (_queryParams3 = queryParams) === null || _queryParams3 === void 0 ? void 0 : _queryParams3.filters; + (_queryParams4 = queryParams) === null || _queryParams4 === void 0 ? true : delete _queryParams4.filters; queryParams = new URLSearchParams(queryParams); - filters === null || filters === void 0 ? void 0 : filters.map(function (f) { - return queryParams.append('filter', f); - }); - var apiVersion = (_options$apiVersion7 = (_options20 = options) === null || _options20 === void 0 ? void 0 : _options20.apiVersion) !== null && _options$apiVersion7 !== void 0 ? _options$apiVersion7 : state.configuration.apiVersion; - var headers = { - Accept: (_CONTENT_TYPES$respon8 = _Utils.CONTENT_TYPES[responseType]) !== null && _CONTENT_TYPES$respon8 !== void 0 ? _CONTENT_TYPES$respon8 : 'application/json' + filters === null || filters === void 0 ? void 0 : filters.map(f => queryParams.append('filter', f)); + const apiVersion = (_options$apiVersion2 = (_options6 = options) === null || _options6 === void 0 ? void 0 : _options6.apiVersion) !== null && _options$apiVersion2 !== void 0 ? _options$apiVersion2 : state.configuration.apiVersion; + const headers = { + Accept: (_CONTENT_TYPES$respon2 = _Utils.CONTENT_TYPES[responseType]) !== null && _CONTENT_TYPES$respon2 !== void 0 ? _CONTENT_TYPES$respon2 : 'application/json' }; - var url = (0, _Utils.buildUrl)('/' + resourceType + '/' + path, hostUrl, apiVersion); + const url = (0, _Utils.buildUrl)('/' + resourceType + '/' + path, hostUrl, apiVersion); (0, _Utils.logOperation)(operationName); (0, _Utils.logApiVersion)(apiVersion); (0, _Utils.logWaitingForServer)(url, queryParams); (0, _Utils.warnExpectLargeResult)(resourceType, url); - return _axios["default"].request({ + return _axios.default.request({ method: 'DELETE', - url: url, + url, auth: { - username: username, - password: password + username, + password }, params: queryParams, data: body, - headers: headers - }).then(function (result) { - _Utils.Log.info("".concat(operationName, " succeeded. DELETED ").concat(resourceType, ".\nSummary:\n").concat((0, _Utils.prettyJson)(result.data))); + headers + }).then(result => { + _Utils.Log.info(`${operationName} succeeded. DELETED ${resourceType}.\nSummary:\n${(0, _Utils.prettyJson)(result.data)}`); if (callback) return callback((0, _languageCommon.composeNextState)(state, result.data)); return (0, _languageCommon.composeNextState)(state, result.data); }); }; } -/** - * 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 `insert` or `update`. E.g. `trackedEntityInstances` - * @param {{attributeId: string,attributeValue:any}} uniqueAttribute - An object containing a `attributeId` and `attributeValue` which will be used to uniquely identify the record - * @param {Object} data - The update data containing new values - * @param {Object} [params] - Optional `import` parameters e.g. `{ou: 'lZGmxYbs97q', filters: ['w75KJ2mc4zz:EQ:Jane']}` - * @param {{replace:boolean, apiVersion: number,strict: boolean,responseType: string}} [options] - `Optional` options for `upsertTEI` operation. Defaults to `{replace: false, apiVersion: state.configuration.apiVersion,strict: true,responseType: 'json'}`. - * @param {function} [callback] - Optional callback to handle the response - * @throws {RangeError} - Throws range error - * @returns {Operation} - * @example - Example `expression.js` of upsert - * upsert( - * 'trackedEntityInstances', - * { - * attributeId: 'lZGmxYbs97q', - * attributeValue: state => - * state.data.attributes.find(obj => obj.attribute === 'lZGmxYbs97q') - * .value, - * }, - * state.data, - * { ou: 'TSyzvBiovKh' } - * ); - * @todo Tweak/refine to mimic implementation based on the following inspiration: {@link https://sqlite.org/lang_upsert.html sqlite upsert} and {@link https://wiki.postgresql.org/wiki/UPSERT postgresql upsert} - * @todo Test implementation for upserting metadata - * @todo Test implementation for upserting data values - * @todo Implement the updateCondition - */ - - -function upsert(resourceType, uniqueAttribute, data, params, options, callback) { - return function (state) { - var _options$operationNam6, _options21, _options$replace, _options22, _options$responseType9, _options23, _params11, _options$apiVersion8, _options24, _CONTENT_TYPES$respon9; - - resourceType = (0, _languageCommon.expandReferences)(resourceType)(state); - uniqueAttribute = (0, _languageCommon.expandReferences)(uniqueAttribute)(state); - var body = (0, _languageCommon.expandReferences)(data)(state); - params = (0, _languageCommon.expandReferences)(params)(state); - options = (0, _languageCommon.expandReferences)(options)(state); - var operationName = (_options$operationNam6 = (_options21 = options) === null || _options21 === void 0 ? void 0 : _options21.operationName) !== null && _options$operationNam6 !== void 0 ? _options$operationNam6 : 'upsert'; - var _state$configuration11 = state.configuration, - username = _state$configuration11.username, - password = _state$configuration11.password, - hostUrl = _state$configuration11.hostUrl; - var replace = (_options$replace = (_options22 = options) === null || _options22 === void 0 ? void 0 : _options22.replace) !== null && _options$replace !== void 0 ? _options$replace : false; - var responseType = (_options$responseType9 = (_options23 = options) === null || _options23 === void 0 ? void 0 : _options23.responseType) !== null && _options$responseType9 !== void 0 ? _options$responseType9 : 'json'; - var _uniqueAttribute = uniqueAttribute, - attributeId = _uniqueAttribute.attributeId, - attributeValue = _uniqueAttribute.attributeValue; - var filters = (_params11 = params) === null || _params11 === void 0 ? void 0 : _params11.filters; - delete params.filters; - var queryParams = new URLSearchParams(params); - filters === null || filters === void 0 ? void 0 : filters.map(function (f) { - return queryParams.append('filter', f); - }); - var op = resourceType === 'trackedEntityInstances' ? 'EQ' : 'eq'; - queryParams.append('filter', "".concat(attributeId, ":").concat(op, ":").concat(attributeValue)); - var apiVersion = (_options$apiVersion8 = (_options24 = options) === null || _options24 === void 0 ? void 0 : _options24.apiVersion) !== null && _options$apiVersion8 !== void 0 ? _options$apiVersion8 : state.configuration.apiVersion; - var url = (0, _Utils.buildUrl)('/' + resourceType, hostUrl, apiVersion); - var headers = { - Accept: (_CONTENT_TYPES$respon9 = _Utils.CONTENT_TYPES[responseType]) !== null && _CONTENT_TYPES$respon9 !== void 0 ? _CONTENT_TYPES$respon9 : 'application/json' - }; - (0, _Utils.logOperation)(operationName); - (0, _Utils.logApiVersion)(apiVersion); - (0, _Utils.logWaitingForServer)(url, queryParams); - (0, _Utils.warnExpectLargeResult)(resourceType, url); - - var getResouceName = function getResouceName() { - return _axios["default"].get(hostUrl + '/api/resources', { - auth: { - username: username, - password: password - }, - transformResponse: [function (data, headers) { - var filter = "plural:eq:".concat(resourceType); - - if (filter) { - var _headers$contentType3, _headers$contentType4; - - if (((_headers$contentType3 = (_headers$contentType4 = headers['content-type']) === null || _headers$contentType4 === void 0 ? void 0 : _headers$contentType4.split(';')[0]) !== null && _headers$contentType3 !== void 0 ? _headers$contentType3 : null) === _Utils.CONTENT_TYPES.json) { - var tempData = JSON.parse(data); - return _objectSpread({}, tempData, { - resources: _Utils.applyFilter.apply(void 0, [tempData.resources].concat(_toConsumableArray((0, _Utils.parseFilter)(filter)))) - }); - } else { - _Utils.Log.warn('Filters on this resource are only supported for json content types. Skipping filtering ...'); - } - } - - return data; - }] - }).then(function (result) { - return result.data.resources[0].singular; - }); - }; - - var findRecordsWithValueOnAttribute = function findRecordsWithValueOnAttribute() { - console.log(queryParams); - return _axios["default"].request({ - method: 'GET', - url: url, - auth: { - username: username, - password: password - }, - params: queryParams, - headers: headers - }); - }; - - _Utils.Log.info("Checking if a record exists that matches this filter: attribute{ id: ".concat(attributeId, ", value: ").concat(attributeValue, " } ...")); - - return Promise.all([getResouceName(), findRecordsWithValueOnAttribute()]).then(function (_ref3) { - var _ref4 = _slicedToArray(_ref3, 2), - resourceName = _ref4[0], - recordsWithValue = _ref4[1]; - - var recordsWithValueCount = recordsWithValue.data[resourceType].length; - - if (recordsWithValueCount > 1) { - _Utils.Log.error(''); - - throw new RangeError("Cannot upsert on Non-unique attribute. The operation found more than one records with the same value of ".concat(attributeValue, " for ").concat(attributeId)); - } else if (recordsWithValueCount === 1) { - var _row1$id; - - // TODO - // Log.info( - // `Unique record found, proceeding to checking if attribute is NULLABLE ...` - // ); - // if (recordsWithNulls.data[resourceType].length > 0) { - // throw new Error( - // `Cannot upsert on Nullable attribute. The operation found records with a NULL value on ${attributeId}.` - // ); - // } - _Utils.Log.info("Attribute has unique values. Proceeding to ".concat(replace ? 'replace' : 'merge', " ...")); - - var row1 = recordsWithValue.data[resourceType][0]; - var useCustomPATCH = ['trackedEntityInstances'].includes(resourceType) ? true : false; - var method = replace ? 'PUT' : useCustomPATCH === true ? 'PUT' : 'PATCH'; - var id = (_row1$id = row1['id']) !== null && _row1$id !== void 0 ? _row1$id : row1[resourceName]; - var updateUrl = "".concat(url, "/").concat(id); - var payload = useCustomPATCH ? _objectSpread({}, row1, {}, body, { - attributes: [].concat(_toConsumableArray(row1.attributes), _toConsumableArray(body.attributes)) - }) : body; - return _axios["default"].request({ - method: method, - url: updateUrl, - auth: { - username: username, - password: password - }, - data: payload, - params: queryParams, - headers: headers - }).then(function (result) { - _Utils.Log.info("".concat(operationName, " succeeded. Updated ").concat(resourceName, ": ").concat(updateUrl, ".\nSummary:\n").concat((0, _Utils.prettyJson)(result.data))); - - if (callback) return callback((0, _languageCommon.composeNextState)(state, result.data)); - return (0, _languageCommon.composeNextState)(state, result.data); - }); - } else if (recordsWithValueCount === 0) { - _Utils.Log.info("Existing record not found, proceeding to CREATE(POST) ..."); // We must delete the filter and ou params so the POST request is not interpreted as a GET request by the server - - - queryParams["delete"]('filter'); - queryParams["delete"]('ou'); - return _axios["default"].request({ - method: 'POST', - url: url, - auth: { - username: username, - password: password - }, - data: body, - params: queryParams, - headers: headers - }).then(function (result) { - var _result$data$response3; - - _Utils.Log.info("".concat(operationName, " succeeded. Created ").concat(resourceName, ": ").concat(result.data.response.importSummaries ? result.data.response.importSummaries[0].href : (_result$data$response3 = result.data.response) === null || _result$data$response3 === void 0 ? void 0 : _result$data$response3.reference, ".\nSummary:\n").concat((0, _Utils.prettyJson)(result.data))); - - if (callback) return callback((0, _languageCommon.composeNextState)(state, result.data)); - return (0, _languageCommon.composeNextState)(state, result.data); - }); - } - }); - }; -} /** * Gets an attribute value by its case-insensitive display name * @public @@ -1774,7 +825,5 @@ function upsert(resourceType, uniqueAttribute, data, params, options, callback) function attrVal(tei, attributeName) { var _tei$attributes, _tei$attributes$find; - return tei === null || tei === void 0 ? void 0 : (_tei$attributes = tei.attributes) === null || _tei$attributes === void 0 ? void 0 : (_tei$attributes$find = _tei$attributes.find(function (a) { - return (a === null || a === void 0 ? void 0 : a.displayName.toLowerCase()) == attributeName.toLowerCase(); - })) === null || _tei$attributes$find === void 0 ? void 0 : _tei$attributes$find.value; + return tei === null || tei === void 0 ? void 0 : (_tei$attributes = tei.attributes) === null || _tei$attributes === void 0 ? void 0 : (_tei$attributes$find = _tei$attributes.find(a => (a === null || a === void 0 ? void 0 : a.displayName.toLowerCase()) == attributeName.toLowerCase())) === null || _tei$attributes$find === void 0 ? void 0 : _tei$attributes$find.value; } \ No newline at end of file diff --git a/lib/Client.js b/lib/Client.js new file mode 100644 index 0000000..db468be --- /dev/null +++ b/lib/Client.js @@ -0,0 +1,35 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.request = request; + +var _axios = _interopRequireDefault(require("axios")); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function request({ + method, + url, + data, + options +}) { + let headers = { + 'Content-Type': 'application/json' + }; + let req = { + method, + url, + headers, + ...options + }; + + if (method !== 'get') { + req = { ...req, + data + }; + } + + return _axios.default.request(req); +} \ No newline at end of file diff --git a/lib/Utils.js b/lib/Utils.js index db8dd18..a5bbb06 100644 --- a/lib/Utils.js +++ b/lib/Utils.js @@ -18,47 +18,47 @@ exports.getIndicesOf = getIndicesOf; exports.isLike = isLike; exports.applyFilter = applyFilter; exports.parseFilter = parseFilter; +exports.expandAndSetOperation = expandAndSetOperation; +exports.nestArray = nestArray; +exports.expandExtractAndLog = expandExtractAndLog; exports.CONTENT_TYPES = exports.dhis2OperatorMap = exports.Log = void 0; var _lodash = require("lodash"); var _axios = _interopRequireDefault(require("axios")); -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } +var _languageCommon = require("@openfn/language-common"); -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } - -function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } - -function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function composeSuccessMessage(operation) { - return "".concat(operation, " succeeded. The body of this result will be available in state.data or in your callback."); + return `${operation} succeeded. The body of this result will be available in state.data or in your callback.`; } function warnExpectLargeResult(paramOrResourceType, endpointUrl) { - if (!paramOrResourceType) Log.warn(" Missing params or resourceType. This may take a while. This endpoint(".concat(endpointUrl, ") may return a large collection of records, since 'params' or 'resourceType' is not specified. We recommend you specify 'params' or 'resourceType' or use 'filter' parameter to limit the content of the result.")); + if (!paramOrResourceType) Log.warn(` Missing params or resourceType. This may take a while. This endpoint(${endpointUrl}) may return a large collection of records, since 'params' or 'resourceType' is not specified. We recommend you specify 'params' or 'resourceType' or use 'filter' parameter to limit the content of the result.`); } function logWaitingForServer(url, params) { - console.info('Request params: ', _typeof(params) === 'object' && !(params instanceof URLSearchParams) ? prettyJson(params) : params); - console.info("Waiting for response from ".concat(url)); + if (params) { + console.info('Request params: ', typeof params === 'object' && !(params instanceof URLSearchParams) ? prettyJson(params) : params); + } + + console.info(`Waiting for response from ${url}`); } function logApiVersion(apiVersion) { - var message = apiVersion && apiVersion ? "Using DHIS2 api version ".concat(apiVersion) : ' Attempting to use apiVersion without providing it in state.configuration or in options parameter. You may encounter errors. api_version_missing.'; - if (apiVersion) console.warn(message);else console.warn("Using latest version of DHIS2 api."); + const message = apiVersion && apiVersion ? `Using DHIS2 api version ${apiVersion}` : ' Attempting to use apiVersion without providing it in state.configuration or in options parameter. You may encounter errors. api_version_missing.'; + if (apiVersion) console.warn(message);else console.warn(`Using latest version of DHIS2 api.`); } function logOperation(operation) { - console.info("Executing ".concat(operation, " ...")); + console.info(`Executing ${operation} ...`); } function buildUrl(path, hostUrl, apiVersion) { - var pathSuffix = apiVersion ? "/".concat(apiVersion).concat(path) : "".concat(path); - var url = hostUrl + '/api' + pathSuffix; + const pathSuffix = apiVersion ? `/${apiVersion}${path}` : `${path}`; + const url = hostUrl + '/api' + pathSuffix; return url; } @@ -69,33 +69,30 @@ function attribute(attributeId, attributeValue) { }; } -function requestHttpHead(endpointUrl, _ref) { - var username = _ref.username, - password = _ref.password; - return _axios["default"].request({ +function requestHttpHead(endpointUrl, { + username, + password +}) { + return _axios.default.request({ method: 'HEAD', url: endpointUrl, auth: { - username: username, - password: password + username, + password } - }).then(function (result) { - return result.headers['content-length']; - }); + }).then(result => result.headers['content-length']); } function validateMetadataPayload(payload, resourceType) { - return _axios["default"].request({ + return _axios.default.request({ method: 'POST', - url: "https://play.dhis2.org/dev/api/schemas/".concat(resourceType), + url: `https://play.dhis2.org/dev/api/schemas/${resourceType}`, auth: { username: 'admin', password: 'distict' }, data: payload - }).then(function (result) { - return result.data; - }); + }).then(result => result.data); } function handleResponse(result, state, callback) { @@ -113,14 +110,14 @@ function getIndicesOf(string, regex) { regex = new RegExp(regex); while (match = regex.exec(string)) { - var schemaRef = void 0; + let schemaRef; if (!indexes[match[0]]) { indexes[match[0]] = {}; } - var hrefString = string.slice(match.index, (0, _lodash.indexOf)(string, '}', match.index) - 1); - var lastIndex = (0, _lodash.lastIndexOf)(hrefString, '/') + 1; + let hrefString = string.slice(match.index, (0, _lodash.indexOf)(string, '}', match.index) - 1); + let lastIndex = (0, _lodash.lastIndexOf)(hrefString, '/') + 1; schemaRef = (0, _lodash.trim)(hrefString.slice(lastIndex)); indexes[match[0]][match.index] = schemaRef; } @@ -128,39 +125,29 @@ function getIndicesOf(string, regex) { return indexes; } -var Log = /*#__PURE__*/function () { - function Log() { - _classCallCheck(this, Log); +class Log { + static info(message) { + return console.info('(info)', new Date(), `\n${message}`); } - _createClass(Log, null, [{ - key: "info", - value: function info(message) { - return console.info('(info)', new Date(), "\n".concat(message)); - } - }, { - key: "warn", - value: function warn(message) { - return console.warn('⚠ WARNING', new Date(), "\n".concat(message)); - } - }, { - key: "error", - value: function error(message) { - return console.error('✗ ERROR', new Date(), "\n".concat(message)); - } - }]); + static warn(message) { + return console.warn('⚠ WARNING', new Date(), `\n${message}`); + } - return Log; -}(); + static error(message) { + return console.error('✗ ERROR', new Date(), `\n${message}`); + } + +} exports.Log = Log; function isLike(string, words) { var _words$match; - var wordsArrary = words === null || words === void 0 ? void 0 : (_words$match = words.match(/([^\W]+[^\s,]*)/)) === null || _words$match === void 0 ? void 0 : _words$match.splice(0, 1); + const wordsArrary = words === null || words === void 0 ? void 0 : (_words$match = words.match(/([^\W]+[^\s,]*)/)) === null || _words$match === void 0 ? void 0 : _words$match.splice(0, 1); - var isFound = function isFound(word) { + const isFound = word => { var _RegExp; return (_RegExp = RegExp(word, 'i')) === null || _RegExp === void 0 ? void 0 : _RegExp.test(string); @@ -169,7 +156,7 @@ function isLike(string, words) { return (0, _lodash.some)(wordsArrary, isFound); } -var dhis2OperatorMap = { +const dhis2OperatorMap = { eq: _lodash.eq, like: isLike }; @@ -178,32 +165,126 @@ exports.dhis2OperatorMap = dhis2OperatorMap; function applyFilter(arrObject, targetProperty, operator, valueToCompareWith) { if (targetProperty && operator && valueToCompareWith) { try { - return (0, _lodash.filter)(arrObject, function (obj) { - return Reflect.apply(operator, obj, [obj[targetProperty], valueToCompareWith]); - }); + return (0, _lodash.filter)(arrObject, obj => Reflect.apply(operator, obj, [obj[targetProperty], valueToCompareWith])); } catch (error) { - Log.warn("Returned unfiltered data. Failed to apply custom filter(".concat(prettyJson({ + Log.warn(`Returned unfiltered data. Failed to apply custom filter(${prettyJson({ targetProperty: targetProperty !== null && targetProperty !== void 0 ? targetProperty : null, operator: operator !== null && operator !== void 0 ? operator : null, value: valueToCompareWith !== null && valueToCompareWith !== void 0 ? valueToCompareWith : null - }), ") on this collection. The operator you supplied maybe unsupported on this resource at the moment.")); + })}) on this collection. The operator you supplied maybe unsupported on this resource at the moment.`); return arrObject; } } - Log.info("No filters applied, returned all records on this resource."); + Log.info(`No filters applied, returned all records on this resource.`); return arrObject; } function parseFilter(filterExpression) { var _filterTokens$; - var filterTokens = filterExpression === null || filterExpression === void 0 ? void 0 : filterExpression.split(':'); + const filterTokens = filterExpression === null || filterExpression === void 0 ? void 0 : filterExpression.split(':'); filterTokens ? filterTokens[1] = dhis2OperatorMap[(_filterTokens$ = filterTokens[1]) !== null && _filterTokens$ !== void 0 ? _filterTokens$ : null] : null; return filterTokens; } -var CONTENT_TYPES = { +function expandAndSetOperation(options, state, operationName) { + return { + operationName, + ...(0, _languageCommon.expandReferences)(options)(state) + }; +} + +const isArray = variable => !!variable && variable.constructor === Array; + +function nestArray(data, key) { + return isArray(data) ? { + [key]: data + } : data; +} + +function log(operationName, apiVersion, url, resourceType, params) { + logOperation(operationName); + logApiVersion(apiVersion); + logWaitingForServer(url, params); + warnExpectLargeResult(resourceType, url); +} + +function extractValuesForAxios(operationName, values) { + return state => { + var _values$options$apiVe, _values$options; + + const apiVersion = (_values$options$apiVe = (_values$options = values.options) === null || _values$options === void 0 ? void 0 : _values$options.apiVersion) !== null && _values$options$apiVe !== void 0 ? _values$options$apiVe : state.configuration.apiVersion; + const { + username, + password, + hostUrl + } = state.configuration; + const auth = { + username, + password + }; + let urlString = '/' + values.resourceType; + + if (operationName === 'update') { + urlString += '/' + values.path; + } + + const url = buildUrl(urlString, hostUrl, apiVersion); + let urlParams = null; + + if (operationName === 'get' || operationName === 'upsert') { + var _values$options2, _values$options2$para, _values$options3, _values$options3$para, _values$options4, _values$options4$para, _values$options5, _values$options5$para, _values$options6; + + const filters = (_values$options2 = values.options) === null || _values$options2 === void 0 ? void 0 : (_values$options2$para = _values$options2.params) === null || _values$options2$para === void 0 ? void 0 : _values$options2$para.filters; + const dimensions = (_values$options3 = values.options) === null || _values$options3 === void 0 ? void 0 : (_values$options3$para = _values$options3.params) === null || _values$options3$para === void 0 ? void 0 : _values$options3$para.dimensions; + (_values$options4 = values.options) === null || _values$options4 === void 0 ? true : (_values$options4$para = _values$options4.params) === null || _values$options4$para === void 0 ? true : delete _values$options4$para.filters; + (_values$options5 = values.options) === null || _values$options5 === void 0 ? true : (_values$options5$para = _values$options5.params) === null || _values$options5$para === void 0 ? true : delete _values$options5$para.dimensions; + urlParams = new URLSearchParams((_values$options6 = values.options) === null || _values$options6 === void 0 ? void 0 : _values$options6.params); + filters === null || filters === void 0 ? void 0 : filters.map(f => urlParams.append('filter', f)); + dimensions === null || dimensions === void 0 ? void 0 : dimensions.map(d => urlParams.append('dimension', d)); + } + + const resourceType = values.resourceType; + const data = values.data; + const callback = values.callback; + const extractedValues = { + resourceType, + data, + apiVersion, + auth, + url, + urlParams, + callback + }; + return extractedValues; + }; +} + +function expandExtractAndLog(operationName, initialParams) { + return state => { + const { + resourceType, + data, + apiVersion, + auth, + url, + urlParams, + callback + } = extractValuesForAxios(operationName, (0, _languageCommon.expandReferences)(initialParams)(state))(state); + log(operationName, apiVersion, url, resourceType, urlParams); + return { + url, + data, + resourceType, + auth, + urlParams, + callback + }; + }; +} + +const CONTENT_TYPES = { xml: 'application/xml', json: 'application/json', pdf: 'application/pdf', diff --git a/lib/index.js b/lib/index.js index d9c288e..9f3b414 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,19 +1,17 @@ "use strict"; -function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } - Object.defineProperty(exports, "__esModule", { value: true }); -exports.Adaptor = exports["default"] = void 0; +exports.Adaptor = exports.default = void 0; var Adaptor = _interopRequireWildcard(require("./Adaptor")); exports.Adaptor = Adaptor; -function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } +function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } -function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } +function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } var _default = Adaptor; -exports["default"] = _default; \ No newline at end of file +exports.default = _default; \ No newline at end of file diff --git a/package.json b/package.json index e58f8d6..b829d06 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,10 @@ }, "main": "lib/index.js", "scripts": { - "test:watch": "mocha -w --require @babel/register", + "test:watch": "mocha -w --require @babel/register test/index.js", "build": "node_modules/.bin/babel src -d lib && npm run ast", - "test": "mocha --require @babel/register", + "test": "mocha --require @babel/register test/index.js", + "integration-test": "mocha --require @babel/register test/integration.js", "ast": "simple-ast --adaptor ./src/Adaptor.js --output ast.json", "postversion": "git push && git push --tags", "version": "npm run build && git add -A lib ast.json" diff --git a/src/Adaptor.js b/src/Adaptor.js index 4464638..4922cf4 100644 --- a/src/Adaptor.js +++ b/src/Adaptor.js @@ -18,7 +18,11 @@ import { parseFilter, logOperation, prettyJson, + expandExtractAndLog, + nestArray, + expandAndSetOperation, } from './Utils'; +import { request } from './Client'; /** * Execute a sequence of operations. @@ -102,1429 +106,514 @@ axios.interceptors.response.use( return response; }, function (error) { + console.log(error); Log.error(`${error?.message}`); - return Promise.reject(JSON.stringify(error.response.data, null, 2)); + return Promise.reject(error); } ); -function expandAndSetOperation(options, state, operationName) { - return { - operationName, - ...expandReferences(options)(state), - }; -} - /** - * Get Tracked Entity Instance(s). + * Create a record * @public * @function - * @param {Object} [params] - Optional `query parameters` e.g. `{ou: 'DiszpKrYNg8', filters: ['lZGmxYbs97q':GT:5']}`. Run `discover` or see {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html DHIS2 docs} for more details on which params to use when querying tracked entities instances. - * @param {{apiVersion: number,responseType: string}} [options] - `Optional` options for `getTEIs` operation. Defaults to `{apiVersion: state.configuration.apiVersion, responseType: 'json'}`. - * @param {function} [callback] - Optional callback to handle the response. + * @param {string} resourceType - Type of resource to create. E.g. `trackedEntityInstances`, `programs`, `events`, ... + * @param {Object} data - 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 control the behavior of the `create` operation and to pass `import parameters` E.g. `{dryRun: true, importStrategy: CREATE}` See {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html DHIS2 API documentation} or {@link discover}..` Defaults to `{operationName: 'create', apiVersion: null, responseType: 'json'}` + * @param {function} [callback] - Optional callback to handle the response * @returns {Operation} - * @example - Example `getTEIs` `expression.js` for fetching a `single` `Tracked Entity Instance` with all the fields included. - * getTEIs({ - * fields: '*', - * ou: 'CMqUILyVnBL', - * trackedEntityInstance: 'HNTA9qD6EEG', - * skipPaging: true, + * + * @example -a `program` + * create('programs', { + * name: 'name 20', + * shortName: 'n20', + * programType: 'WITHOUT_REGISTRATION', * }); - */ -export function getTEIs(params, options, callback) { - return state => { - const expandedOptions = expandAndSetOperation(options, state, 'getTEIs'); - return getData( - 'trackedEntityInstances', - params, - expandedOptions, - callback - )(state); - }; -} - -/** - * Update TEI if exists otherwise create. - * - Update if the record exists otherwise insert a new record. - * - This is useful for idempotency and duplicate record management. - * @public - * @function - * @param {string} uniqueAttributeId - Tracked Entity Instance unique identifier attribute used during matching. - * @param {Object} data - Payload data for new tracked entity instance or updated data for an existing tracked entity instance. - * @param {{apiVersion: number,strict: boolean,responseType: string}} [options] - `Optional` options for `upsertTEI` operation. Defaults to `{apiVersion: state.configuration.apiVersion,strict: true,responseType: 'json'}`. - * @param {function} [callback] - Optional `callback` to handle the response. - * @throws {RangeError} - Throws `RangeError` when `uniqueAttributeId` is `invalid` or `not unique`. - * @returns {Operation} - * @example - Example `expression.js` for upserting a tracked entity instance on attribute with Id `lZGmxYbs97q`. - * upsertTEI('lZGmxYbs97q', { + * + * @example -an `event` + * create('events', { + * program: 'eBAyeGv0exc', + * orgUnit: 'DiszpKrYNg8', + * status: 'COMPLETED', + * }); + * + * @example -a `trackedEntityInstance` + * create('trackedEntityInstances', { * orgUnit: 'TSyzvBiovKh', * trackedEntityType: 'nEenWmSyUEp', * attributes: [ * { - * attribute: 'lZGmxYbs97q', - * value: '77790012', - * }, - * { * attribute: 'w75KJ2mc4zz', * value: 'Gigiwe', * }, + * ] + * }); + * + * @example -a `dataSet` + * create('dataSets', { name: 'OpenFn Data Set', periodType: 'Monthly' }); + * + * @example -a `dataSetNotification` + * create('dataSetNotificationTemplates', { + * dataSetNotificationTrigger: 'DATA_SET_COMPLETION', + * notificationRecipient: 'ORGANISATION_UNIT_CONTACT', + * name: 'Notification', + * messageTemplate: 'Hello', + * deliveryChannels: ['SMS'], + * dataSets: [], + * }); + * + * @example -a `dataElement` + * create('dataElements', { + * aggregationType: 'SUM', + * domainType: 'AGGREGATE', + * valueType: 'NUMBER', + * name: 'Paracetamol', + * shortName: 'Para', + * }); + * + * @example -a `dataElementGroup` + * create('dataElementGroups', { + * name: 'Data Element Group 1', + * dataElements: [], + * }); + * + * @example -a `dataElementGroupSet` + * create('dataElementGroupSets', { + * name: 'Data Element Group Set 4', + * dataDimension: true, + * shortName: 'DEGS4', + * dataElementGroups: [], + * }); + * + * @example -a `dataValueSet` + * create('dataValueSets', { + * dataElement: 'f7n9E0hX8qk', + * period: '201401', + * orgUnit: 'DiszpKrYNg8', + * value: '12', + * }); + * + * @example -a `dataValueSet` with related `dataValues` + * create('dataValueSets', { + * dataSet: 'pBOMPrpg1QX', + * completeDate: '2014-02-03', + * period: '201401', + * orgUnit: 'DiszpKrYNg8', + * dataValues: [ + * { + * dataElement: 'f7n9E0hX8qk', + * value: '1', + * }, * { - * attribute: 'zDhUuAYrxNC', - * value: 'Mwanza', + * dataElement: 'Ix2HsbDMLea', + * value: '2', + * }, + * { + * dataElement: 'eY5ehpbEsB7', + * value: '3', * }, * ], * }); - */ -export function upsertTEI(uniqueAttributeId, data, options, callback) { - return state => { - uniqueAttributeId = expandReferences(uniqueAttributeId)(state); - - const body = expandReferences(data)(state); - - const expandedOptions = expandAndSetOperation(options, state, 'upsertTEI'); - - const { password, username, hostUrl } = state.configuration; - - const apiVersion = - expandedOptions?.apiVersion ?? state.configuration.apiVersion; - - const strict = expandedOptions?.strict ?? true; - - const params = { - ou: body.orgUnit, - }; - - const uniqueAttributeValue = body.attributes?.find( - obj => obj?.attribute === uniqueAttributeId - )?.value; - - const trackedEntityType = body.trackedEntityType; - - const uniqueAttributeUrl = buildUrl( - `/trackedEntityAttributes/${uniqueAttributeId}`, - hostUrl, - apiVersion - ); - - const trackedEntityTypeUrl = buildUrl( - `/trackedEntityTypes/${trackedEntityType}?fields=*`, - hostUrl, - apiVersion - ); - - const findTrackedEntityType = () => { - return axios - .get(trackedEntityTypeUrl, { auth: { username, password } }) - .then(result => { - const attribute = result.data?.trackedEntityTypeAttributes?.find( - obj => obj?.trackedEntityAttribute?.id === uniqueAttributeId - ); - return { - ...result.data, - upsertAttributeAssigned: attribute ? true : false, - }; - }); - }; - - const isAttributeUnique = () => { - return axios - .get(uniqueAttributeUrl, { auth: { username, password } }) - .then(result => { - const foundAttribute = result.data; - return { unique: foundAttribute.unique, name: foundAttribute.name }; - }); - }; - return Promise.all([ - findTrackedEntityType(), - strict === true ? isAttributeUnique() : Promise.resolve({ unique: true }), - ]).then(([entityType, attribute]) => { - if (!entityType.upsertAttributeAssigned) { - Log.error(''); - throw new RangeError( - `Tracked Entity Attribute ${uniqueAttributeId} is not assigned to the ${entityType.name} Entity Type.` - ); - } - if (!attribute.unique) { - Log.error(''); - throw new RangeError( - `Attribute ${ - attribute.name ?? '' - }(${uniqueAttributeId}) is not marked as unique.` - ); - } - return upsert( - 'trackedEntityInstances', - { - attributeId: uniqueAttributeId, - attributeValue: uniqueAttributeValue, - }, - body, - params, - expandedOptions, - callback - )(state); - }); - }; -} - -/** - * Create Tracked Entity Instance. - * @public - * @function - * @param {Object} data - The update data containing new values. - * @param {Object} [params] - Optional `import parameters` for a given a resource. E.g. `{dryRun: true, importStrategy: CREATE}` See {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html#import-parameters_1 DHIS2 Import parameters documentation} or run `discover`. Defauls to `DHIS2 default import parameters`. - * @param {{apiVersion: number,responseType: string}} [options] - `Optional` options for `createTEI` operation. Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}`. - * @param {function} [callback] - Optional callback to handle the response. - * @returns {Operation} - * @example - Example `expression.js` of `createTEI`. - * createTEI({ - * orgUnit: 'TSyzvBiovKh', - * trackedEntityType: 'nEenWmSyUEp', - * attributes: [ - * { - * attribute: 'lZGmxYbs97q', - * value: valUpsertTEI, - * }, - * { - * attribute: 'w75KJ2mc4zz', - * value: 'Gigiwe', - * }, - * ], - * enrollments: [ - * { - * orgUnit: 'TSyzvBiovKh', - * program: 'fDd25txQckK', - * programState: 'lST1OZ5BDJ2', - * enrollmentDate: '2021-01-04', - * incidentDate: '2021-01-04', - * }, - * ], + * + * @example -an `enrollment` + * create('enrollments', { + * trackedEntityInstance: 'bmshzEacgxa', + * orgUnit: 'TSyzvBiovKh', + * program: 'gZBxv9Ujxg0', + * enrollmentDate: '2013-09-17', + * incidentDate: '2013-09-17', * }); */ -export function createTEI(data, params, options, callback) { +export function create(resourceType, data, options, callback) { + const initialParams = { resourceType, data, options, callback }; return state => { - const expandedOptions = expandAndSetOperation(options, state, 'createTEI'); - return create( - 'trackedEntityInstances', + const { + url, data, - params, - expandedOptions, - callback - )(state); + resourceType, + auth, + urlParams, + callback, + } = expandExtractAndLog('create', initialParams)(state); + + return request({ + method: 'post', + url, + data: nestArray(data, resourceType), + options: { + auth, + params: urlParams, + }, + }) + .then(result => { + Log.info( + `\nOperation succeeded. Created ${resourceType}: ${result.headers.location}.\n` + ); + if (callback) return callback(composeNextState(state, result.data)); + return composeNextState(state, result.data); + }) + .catch(error => { + throw error; + }); }; } /** - * Update a Tracked Entity Instance. + * Update data. A generic helper function to update a resource object of any type. + * Updating an object requires to send `all required fields` or the `full body` * @public * @function - * @param {string} path - Path to the object being updated. This can be an `id` or path to an `object` in a `nested collection` on the object(E.g. `/api/{collection-object}/{collection-object-id}/{collection-name}/{object-id}`). - * @param {Object} data - The update data containing new values. - * @param {Object} [params] - Optional `import parameters` for a given a resource. E.g. `{dryRun: true, importStrategy: CREATE, filters:[]}` See {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html#import-parameters_1 DHIS2 Import parameters documentation} or run `discover`. Defauls to `DHIS2 default import parameters`. - * @param {{apiVersion: number,responseType: string}} [options] - `Optional` options for `updateTEI` operation. Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}`. - * @param {function} [callback] - Optional callback to handle the response. + * @param {string} resourceType - The type of resource to be updated. E.g. `dataElements`, `organisationUnits`, etc. + * @param {string} path - The `id` or `path` to the `object` to be updated. E.g. `FTRrcoaog83` or `FTRrcoaog83/{collection-name}/{object-id}` + * @param {Object} data - Data to update. It requires to send `all required fields` or the `full body`. If you want `partial updates`, use `patch` operation. + * @param {{apiVersion: number,operationName: string,resourceType: string}} [options] - Optional options for update method. Defaults to `{operationName: 'update', apiVersion: state.configuration.apiVersion, responseType: 'json'}` + * @param {function} [callback] - Optional callback to handle the response * @returns {Operation} - * @example - Example `expression.js` of `updateTEI`. - * updateTEI('PVqUD2hvU4E', { - * orgUnit: 'TSyzvBiovKh', - * trackedEntityType: 'nEenWmSyUEp', - * attributes: [ - * { - * attribute: 'lZGmxYbs97q', - * value: valUpsertTEI, - * }, - * { - * attribute: 'w75KJ2mc4zz', - * value: 'Gigiwe', - * }, - * ], - * enrollments: [ - * { - * orgUnit: 'TSyzvBiovKh', - * program: 'fDd25txQckK', - * programState: 'lST1OZ5BDJ2', - * enrollmentDate: '2021-01-04', - * incidentDate: '2021-01-04', - * }, - * ], + * @example -a program + * update('programs', 'qAZJCrNJK8H', { + * name: '14e1aa02c3f0a31618e096f2c6d03bed', + * shortName: '14e1aa02', + * programType: 'WITHOUT_REGISTRATION', * }); - */ -export function updateTEI(path, data, params, options, callback) { - return state => { - const expandedOptions = expandAndSetOperation(options, state, 'updateTEI'); - return update( - 'trackedEntityInstances', - path, - data, - params, - expandedOptions, - callback - )(state); - }; -} - -/** - * Get annonymous events or tracker events. - * @public - * @function - * @param {Object} params - `import` parameters for `getEvents`. See examples here - * @param {{apiVersion: number,responseType: string}} [options] - `Optional` options for `getEvents` operation. Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}`. - * @param {function} [callback] - Optional callback to handle the response. - * @returns {Operation} - * @example - Query for `all events` with `children` of a certain `organisation unit` - * getEvents({ orgUnit: 'YuQRtpLP10I', ouMode: 'CHILDREN' }); - */ -export function getEvents(params, options, callback) { - return state => { - const expandedOptions = expandAndSetOperation(options, state, 'getEvents'); - return getData('events', params, expandedOptions, callback)(state); - }; -} - -/** - * Create DHIS2 Events - * - You will need a `program` which can be looked up using the `getPrograms` operation, an `orgUnit` which can be looked up using the `getMetadata` operation and passing `{organisationUnits: true}` as `resources` param, and a list of `valid data element identifiers` which can be looked up using the `getMetadata` passing `{dataElements: true}` as `resources` param. - * - For events with registration, a `tracked entity instance identifier is required` - * - For sending `events` to `programs with multiple stages`, you will need to also include the `programStage` identifier, the identifiers for `programStages` can be found in the `programStages` resource via a call to `getMetadata` operation. - * @public - * @function - * @param {Object} data - The payload containing new values - * @param {Object} [params] - Optional `import parameters` for events. E.g. `{dryRun: true, importStrategy: CREATE, filters:[]}` See {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html#events DHIS2 Event Import parameters documentation} or run `discover`. Defauls to `DHIS2 default import parameters`. - * @param {{apiVersion: number,responseType: string}} [options] - Optional `flags` for the behavior of the `createEvents` operation.Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}` - * @param {function} [callback] - Optional callback to handle the response - * @returns {Operation} state - * @example - Example `expression.js` of `createEvents` for a `single event` can look like this: - * createEvents({ + * + * @example an `event` + * update('events', 'PVqUD2hvU4E', { * program: 'eBAyeGv0exc', - * orgUnit: 'DiszpKrYNg8', - * eventDate: date, + * orgUnit: 'Ngelehun CHC', * status: 'COMPLETED', - * completedDate: date, * storedBy: 'admin', - * coordinate: { - * latitude: 59.8, - * longitude: 10.9, - * }, + * dataValues: [], + * }); + * + * @example a `trackedEntityInstance` + * update('trackedEntityInstances', 'IeQfgUtGPq2', { + * created: '2015-08-06T21:12:37.256', + * orgUnit: 'TSyzvBiovKh', + * createdAtClient: '2015-08-06T21:12:37.256', + * trackedEntityInstance: 'IeQfgUtGPq2', + * lastUpdated: '2015-08-06T21:12:37.257', + * trackedEntityType: 'nEenWmSyUEp', + * inactive: false, + * deleted: false, + * featureType: 'NONE', + * programOwners: [ + * { + * ownerOrgUnit: 'TSyzvBiovKh', + * program: 'IpHINAT79UW', + * trackedEntityInstance: 'IeQfgUtGPq2', + * }, + * ], + * enrollments: [], + * relationships: [], + * attributes: [ + * { + * lastUpdated: '2016-01-12T00:00:00.000', + * displayName: 'Last name', + * created: '2016-01-12T00:00:00.000', + * valueType: 'TEXT', + * attribute: 'zDhUuAYrxNC', + * value: 'Russell', + * }, + * { + * 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: 'Catherine', + * }, + * ], + * }); + * + * @example -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, + * deliveryChannels: ['SMS'], + * dataSets: [], + * }); + * + * @example -a `dataElement` + * update('dataElements', 'FTRrcoaog83', { + * aggregationType: 'SUM', + * domainType: 'AGGREGATE', + * valueType: 'NUMBER', + * name: 'Paracetamol', + * shortName: 'Para', + * }); + * + * @example -a `dataElementGroup` + * update('dataElementGroups', 'QrprHT61XFk', { + * name: 'Data Element Group 1', + * dataElements: [], + * }); + * + * @example -a `dataElementGroupSet` + * update('dataElementGroupSets', 'VxWloRvAze8', { + * name: 'Data Element Group Set 4', + * dataDimension: true, + * shortName: 'DEGS4', + * dataElementGroups: [], + * }); + * + * @example -a `dataValueSet` + * update('dataValueSets', 'AsQj6cDsUq4', { + * dataElement: 'f7n9E0hX8qk', + * period: '201401', + * orgUnit: 'DiszpKrYNg8', + * value: '12', + * }); + * + * @example -a `dataValueSet` with related `dataValues` + * update('dataValueSets', 'Ix2HsbDMLea', { + * dataSet: 'pBOMPrpg1QX', + * completeDate: '2014-02-03', + * period: '201401', + * orgUnit: 'DiszpKrYNg8', * dataValues: [ * { - * dataElement: 'qrur9Dvnyt5', - * value: '33', + * dataElement: 'f7n9E0hX8qk', + * value: '1', * }, * { - * dataElement: 'oZg33kd9taw', - * value: 'Male', + * dataElement: 'Ix2HsbDMLea', + * value: '2', * }, * { - * dataElement: 'msodh3rEMJa', - * value: date, + * dataElement: 'eY5ehpbEsB7', + * value: '3', * }, * ], * }); - */ -export function createEvents(data, params, options, callback) { - return state => { - const expandedOptions = expandAndSetOperation( - options, - state, - 'createEvents' - ); - return create('events', data, params, expandedOptions, callback)(state); - }; -} - -/** - * Update DHIS2 Event. - * - To update an existing event, the format of the payload is the same as that of `creating an event` via `createEvents` operations - * - But you should supply the `identifier` of the object you are updating - * - The payload has to contain `all`, even `non-modified`, `attributes`. - * - Attributes that were present before and are not present in the current payload any more will be removed by DHIS2. - * - If you do not want this behavior, please use `upsert` operation to upsert your events. - * @public - * @function - * @param {string} path - Path to the object being updated. This can be an `id` or path to an `object` in a `nested collection` on the object(E.g. `/api/{collection-object}/{collection-object-id}/{collection-name}/{object-id}`) - * @param {Object} data - The update data containing new values - * @param {Object} [params] - Optional `import` parameters for `updateEvents`. E.g. `{dryRun: true, IdScheme: 'CODE'}. Defaults to DHIS2 `default params` - * @param {{apiVersion: number,responseType: string}} [options] - Optional `flags` for the behavior of the `updateEvents` operation.Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}` - * @param {function} [callback] - Optional callback to handle the response - * @returns {Operation} - * @example - Example `expression.js` of `updateEvents` - * updateEvents('PVqUD2hvU4E', { events: [ - * { - * program: 'eBAyeGv0exc', - * orgUnit: 'DiszpKrYNg8', - * eventDate: date, - * status: 'COMPLETED', - * storedBy: 'admin', - * coordinate: { - * latitude: '59.8', - * longitude: '10.9', - * }, - * dataValues: [ - * { - * dataElement: 'qrur9Dvnyt5', - * value: '22', - * }, - * { - * dataElement: 'oZg33kd9taw', - * value: 'Male', - * }, - * ], - * }] + * + * @example a single enrollment + * update('enrollments', 'CmsHzercTBa' { + * trackedEntityInstance: 'bmshzEacgxa', + * orgUnit: 'TSyzvBiovKh', + * program: 'gZBxv9Ujxg0', + * enrollmentDate: '2013-10-17', + * incidentDate: '2013-10-17', * }); */ -export function updateEvents(path, data, params, options, callback) { +export function update(resourceType, path, data, options, callback) { + const initialParams = { resourceType, path, data, options, callback }; return state => { - const expandedOptions = expandAndSetOperation( - options, - state, - 'updateEvents' - ); - return update( - 'events', - path, - data, - params, - expandedOptions, - callback + const { url, data, resourceType, auth, _, callback } = expandExtractAndLog( + 'update', + initialParams )(state); - }; -} - -/** - * Get DHIS2 Tracker Programs. - * @public - * @function - * @param {Object} params - `import` parameters for `getPrograms`. See {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html#tracker-web-api DHIS2 api documentation for allowed query parameters } - * @param {{apiVersion: number,responseType: string}} [options] - Optional `flags` for the behavior of the `getPrograms` operation.Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}` - * @param {function} [callback] - Optional callback to handle the response - * @returns {Operation} - * @example - Query for `all programs` with a certain `organisation unit` - * getPrograms({ orgUnit: 'DiszpKrYNg8' , fields: '*' }); - */ -export function getPrograms(params, options, callback) { - return state => { - const expandedOptions = expandAndSetOperation( - options, - state, - 'getPrograms' - ); - return getData('programs', params, expandedOptions, callback)(state); - }; -} - -/** - * Create a DHIS2 Tracker Program - * @public - * @function - * @param {Object} data - The update data containing new values - * @param {Object} [params] - Optional `import` parameters for `createPrograms`. E.g. `{dryRun: true, IdScheme: 'CODE'}. Defaults to DHIS2 `default params` - * @param {{apiVersion: number,responseType: string}} [options] - Optional `flags` for the behavior of the `getPrograms` operation.Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}` - * @param {function} [callback] - Optional callback to handle the response - * @returns {Operation} - * @example - Example `expression.js` of `createPrograms` for a `single program` can look like this: - * createPrograms(state.data); - */ -export function createPrograms(data, params, options, callback) { - return state => { - const expandedOptions = expandAndSetOperation( - options, - state, - 'createPrograms' - ); - return create('programs', data, expandedOptions, params, callback)(state); - }; -} -/** - * Update DHIS2 Tracker Programs - * - To update an existing program, the format of the payload is the same as that of `creating an event` via `createEvents` operations - * - But you should supply the `identifier` of the object you are updating - * - The payload has to contain `all`, even `non-modified`, `attributes`. - * - Attributes that were present before and are not present in the current payload any more will be removed by DHIS2. - * - If you do not want this behavior, please use `upsert` operation to upsert your events. - * @public - * @function - * @param {string} path - Path to the object being updated. This can be an `id` or path to an `object` in a `nested collection` on the object(E.g. `/api/{collection-object}/{collection-object-id}/{collection-name}/{object-id}`) - * @param {Object} data - The update data containing new values - * @param {Object} [params] - Optional `import` parameters for `updatePrograms`. E.g. `{dryRun: true, IdScheme: 'CODE'}. Defaults to DHIS2 `default params` - * @param {{apiVersion: number,responseType: string}} [options] - Optional `flags` for the behavior of the `getPrograms` operation.Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}` - * @param {function} [callback] - Optional callback to handle the response - * @returns {Operation} - * @example - Example `expression.js` of `updatePrograms` - * updatePrograms('PVqUD2hvU4E', state.data); - */ -export function updatePrograms(path, data, params, options, callback) { - return state => { - const expandedOptions = expandAndSetOperation( - options, - state, - 'updatePrograms' - ); - return update( - 'programs', - path, - data, - params, - expandedOptions, - callback - )(state); + return request({ method: 'put', url, data, options: { auth } }) + .then(result => { + Log.info(`\nOperation succeeded. Updated ${resourceType} ${path}.\n`); + if (callback) return callback(composeNextState(state, result.data)); + return composeNextState(state, result.data); + }) + .catch(error => { + throw error; + }); }; } /** - * Get DHIS2 Enrollments + * Get data. Generic helper method for getting data of any kind from DHIS2. + * - This can be used to get `DataValueSets`,`events`,`trackedEntityInstances`,`etc.` * @public * @function - * @param {Object} params - `Query` parameters for `getEnrollments`. See {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html#enrollment-management here} - * @param {{apiVersion: number,responseType: string}} [options] - Optional `flags` for the behavior of the `getEnrollments` operation.Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}` - * @param {function} [callback] - Optional callback to handle the response - * @returns {Operation} - * @example - To constrain the response to `enrollments` which are part of a `specific program` you can include a `program query parameter` - * getEnrollments({ - * ou: 'O6uvpzGd5pu', - * ouMode: 'DESCENDANTS', - * program: 'ur1Edk5Oe2n', - * fields: '*', + * @param {string} resourceType - The type of resource to get(use its `plural` name). E.g. `dataElements`, `trackedEntityInstances`,`organisationUnits`, etc. + * @param {{apiVersion: number,operationName: string,responseType: string}}[options] - `Optional` options for `getData` operation. Defaults to `{operationName: 'getData', apiVersion: state.configuration.apiVersion, responseType: 'json'}`. + * @param {function} [callback] - Optional callback to handle the response + * @returns {Operation} state + * @example Example getting one `trackedEntityInstance` with `Id` 'dNpxRu1mWG5' for a given `orgUnit(DiszpKrYNg8)` + * getData('trackedEntityInstances', { + * fields: '*', + * ou: 'DiszpKrYNg8', + * entityType: 'nEenWmSyUEp', + * trackedEntityInstance: 'dNpxRu1mWG5', * }); */ -export function getEnrollments(params, options, callback) { +export function get(resourceType, options, callback) { + const initialParams = { resourceType, options, callback }; return state => { - const expandedOptions = expandAndSetOperation( - options, - state, - 'getEnrollments' - ); - return getData('enrollments', params, expandedOptions, callback)(state); + const { + url: url, + _data, + _resourceType, + auth, + urlParams, + callback, + } = expandExtractAndLog('get', initialParams)(state); + + return request({ + method: 'get', + url, + options: { auth, params: urlParams, responseType: 'json' }, + }) + .then(result => { + Log.info( + `\nOperation succeeded. Retrieved ${result.data[resourceType].length} ${resourceType}.\n` + ); + if (callback) return callback(composeNextState(state, result.data)); + return composeNextState(state, result.data); + }) + .catch(error => { + throw error; + }); }; } /** - * Enroll a TEI into a program - * - Enrolling a tracked entity instance into a program - * - For enrolling `persons` into a `program`, you will need to first get the `identifier of the person` from the `trackedEntityInstances resource` via the `getTEIs` operation. - * - Then, you will need to get the `program identifier` from the `programs` resource via the `getPrograms` operation. + * 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 {Object} data - The enrollment data. See example {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html#enrollment-management here } - * @param {Object} [params] - Optional `import` parameters for `createEnrollment`. E.g. `{dryRun: true, IdScheme: 'CODE'}. Defaults to DHIS2 `default params` - * @param {{apiVersion: number,responseType: string}} [options] - Optional `flags` for the behavior of the `enrollTEI` operation.Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}` - * @param {function} [callback] - Optional callback to handle the response - * @returns {Operation} - * @example - Example `expression.js` of `createEnrollment` of a `person` into a `program` can look like this: - * enrollTEI({ - * trackedEntity: 'tracked-entity-id', - * orgUnit: 'org-unit-id', - * attributes: [ - * { - * attribute: 'attribute-id', - * value: 'attribute-value', - * }, - * ], - * enrollments: [ - * { - * orgUnit: 'org-unit-id', - * program: 'program-id', - * enrollmentDate: '2013-09-17', - * incidentDate: '2013-09-17', - * }, - * ], - *}); - */ -export function enrollTEI(data, params, options, callback) { - return state => { - const expandedOptions = expandAndSetOperation(options, state, 'enrollTEI'); - return create( - 'enrollments', - data, - expandedOptions, - params, - callback - )(state); - }; -} - -/** - * Update a DHIS2 Enrollemts - * - To update an existing enrollment, the format of the payload is the same as that of `creating an event` via `createEvents` operations - * - But you should supply the `identifier` of the object you are updating - * - The payload has to contain `all`, even `non-modified`, `attributes`. - * - Attributes that were present before and are not present in the current payload any more will be removed by DHIS2. - * - If you do not want this behavior, please use `upsert` operation to upsert your events. - * @public - * @function - * @param {string} path - Path to the object being updated. This can be an `id` or path to an `object` in a `nested collection` on the object(E.g. `/api/{collection-object}/{collection-object-id}/{collection-name}/{object-id}`) - * @param {Object} data - The update data containing new values - * @param {Object} [params] - Optional `import` parameters for `updateEnrollments`. E.g. `{dryRun: true, IdScheme: 'CODE'}. Defaults to DHIS2 `default params` - * @param {{apiVersion: number,responseType: string}} [options] - Optional `flags` for the behavior of the `updateEnrollments` operation.Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}` - * @param {function} [callback] - Optional callback to handle the response - * @returns {Operation} - * @example - Example `expression.js` of `updateEnromments` - * updateEnrollments('PVqUD2hvU4E', state.data); - */ -export function updateEnrollments(path, data, params, options, callback) { - return state => { - const expandedOptions = expandAndSetOperation( - options, - state, - 'updateEnrollments' - ); - return update( - 'enrollments', - path, - data, - params, - expandedOptions, - callback - )(state); - }; -} - -/** - * Cancel a DHIS2 Enrollment - * - To cancel an existing enrollment, you should supply the `enrollment identifier`(`enrollemt-id`) - * @public - * @function - * @param {string} enrollmentId - The `enrollment-id` of the enrollment you wish to cancel - * @param {Object} [params] - Optional `import` parameters for `cancelEnrollment`. E.g. `{dryRun: true, IdScheme: 'CODE'}. Defaults to DHIS2 `default params` - * @param {{apiVersion: number,responseType: string}} [options] - Optional `flags` for the behavior of the `cancelEnrollment` operation.Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}` - * @param {function} [callback] - Optional callback to handle the response - * @returns {Operation} - * @example - Example `expression.js` of `cancelEnrollment` - * cancelEnrollments('PVqUD2hvU4E'); - */ -export function cancelEnrollment(enrollmentId, params, options, callback) { - return state => { - enrollmentId = expandReferences(enrollmentId)(state); - - const path = `${enrollmentId}/cancelled`; - - const expandedOptions = expandAndSetOperation( - options, - state, - 'cancelEnrollment' - ); - - return update( - 'enrollments', - path, - null, - params, - expandedOptions, - callback - )(state); - }; -} - -/** - * Complete a DHIS2 Enrollment - * - To complete an existing enrollment, you should supply the `enrollment identifier`(`enrollemt-id`) - * @public - * @function - * @param {string} enrollmentId - The `enrollment-id` of the enrollment you wish to cancel - * @param {Object} [params] - Optional `import` parameters for `completeEnrollment`. E.g. `{dryRun: true, IdScheme: 'CODE'}. Defaults to DHIS2 `default params` - * @param {{apiVersion: number,responseType: string}} [options] - Optional `flags` for the behavior of the `completeEnrollment` operation.Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}` - * @param {function} [callback] - Optional callback to handle the response - * @returns {Operation} - * @example - Example `expression.js` of `completeEnrollment` - * completeEnrollment('PVqUD2hvU4E'); - */ -export function completeEnrollment(enrollmentId, params, options, callback) { - return state => { - enrollmentId = expandReferences(enrollmentId)(state); - - const path = `${enrollmentId}/completed`; - - const expandedOptions = expandAndSetOperation( - options, - state, - 'enrollments' - ); - return update( - 'enrollments', - path, - null, - params, - expandedOptions, - callback - )(state); - }; -} - -/** - * Get DHIS2 Relationships(links) between two entities in tracker. These entities can be tracked entity instances, enrollments and events. - * - All the tracker operations, `getTEIs`, `getEnrollments` and `getEvents` also list their relationships if requested in the `field` filter. - * - To list all relationships, this requires you to provide the UID of the trackedEntityInstance, Enrollment or event that you want to list all the relationships for. - * @public - * @function - * @param {Object} params - `Query` parameters for `getRelationships`. See examples {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html#relationships here} - * @param {{apiVersion: number,responseType: string}} [options] - Optional `flags` for the behavior of the `getRelationships` operation.Defaults to `{apiVersion: state.configuration.apiVersion,responseType: 'json'}` - * @param {function} [callback] - Optional callback to handle the response - * @returns {Operation} - * @example - A query for `all relationships` associated with a `specific tracked entity instance` can look like this: - * getRelationships({ tei: 'F8yKM85NbxW', fields: '*' }); - */ -export function getRelationships(params, options, callback) { - return state => { - const expandedOptions = expandAndSetOperation( - options, - state, - 'getRelationships' - ); - return getData('relationships', params, expandedOptions, callback)(state); - }; -} - -/** - * Get DHIS2 Data Values. - * - This operation retrives data values from DHIS2 Web API by interacting with the `dataValueSets` resource - * - Data values can be retrieved in XML, JSON and CSV format. - * @public - * @function - * @param {Object} params - `Query` parameters for `getDataValues`. E.g. `{dataset: 'pBOMPrpg1QX', limit: 3, period: 2021, orgUnit: 'DiszpKrYNg8'} Run `discover` or see {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html#data-values DHIS2 API docs} for available `Data Value Set Query Parameters`. - * @param {{apiVersion: number,responseType: string}} [options] - Optional `options` for `getDataValues` operation. Defaults to `{apiVersion: state.configuration.apiVersion, responseType: 'json'}` - * @param {function} [callback] - Optional `callback` to handle the response - * @returns {Operation} - * @example - Example getting **two** `data values` associated with a specific `orgUnit`, `dataSet`, and `period ` - * getDataValues({ - * orgUnit: 'DiszpKrYNg8', - * period: '202010', - * dataSet: 'pBOMPrpg1QX', - * limit: 2, - * }); - */ -export function getDataValues(params, options, callback) { - return state => { - const expandedOptions = expandAndSetOperation( - options, - state, - 'getDataValues' - ); - return getData('dataValueSets', params, expandedOptions, callback)(state); - }; -} - -/** - * Create DHIS2 Data Values - * - This is used to send aggregated data to DHIS2 - * - A data value set represents a set of data values which have a relationship, usually from being captured off the same data entry form. - * - To send a set of related data values sharing the same period and organisation unit, we need to identify the period, the data set, the org unit (facility) and the data elements for which to report. - * - You can also use this operation to send large bulks of data values which don't necessarily are logically related. - * - To send data values that are not linked to a `dataSet`, you do not need to specify the dataSet and completeDate attributes. Instead, you will specify the period and orgUnit attributes on the individual data value elements instead of on the outer data value set element. This will enable us to send data values for various periods and organisation units - * @public - * @function - * @param {object} data - The `data values` to upload or create. See example shape. - * @param {{apiVersion: number,responseType: string}} [options] - Optional `flags` for the behavior of the `createDataVaues` operation. - * @param {object} [params] - Optional `import` parameters for `createDataValues`. E.g. `{dryRun: true, IdScheme: 'CODE'}. Defaults to DHIS2 `default params`. Run `discover` or visit {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html#data-values DHIS2 Docs API} to learn about available data values import parameters. - * @param {function} [callback] - Optional callback to handle the response - * @returns {Operation} - * @example - Example `expression.js` of `createDataValues` for sending a set of related data values sharing the same period and organisation unit - * createDataValues({ - * dataSet: 'pBOMPrpg1QX', - * completeDate: '2014-02-03', - * period: '201401', - * orgUnit: 'DiszpKrYNg8', - * dataValues: [ - * { - * dataElement: 'f7n9E0hX8qk', - * value: '1', - * }, - * { - * dataElement: 'Ix2HsbDMLea', - * value: '2', - * }, - * { - * dataElement: 'eY5ehpbEsB7', - * value: '3', - * }, - * ], - * }); - */ -export function createDataValues(data, options, params, callback) { - return state => { - const expandedOptions = expandAndSetOperation( - options, - state, - 'createDataValues' - ); - return create( - 'dataValueSets', - data, - expandedOptions, - params, - callback - )(state); - }; -} - -/** - * Generate valid, random DHIS2 identifiers - * - Useful for client generated Ids compatible with DHIS2 - * @public - * @function - * @param {{apiVersion: number,limit: number,responseType: string}} [options] - Optional `options` for `generateDhis2UID` operation. Defaults to `{apiVersion: state.configuration.apiVersion,limit: 1,responseType: 'json'}` - * @param {function} [callback] - Callback to handle response - * @returns {Operation} - * @example Example generating `three UIDs` from the DHIS2 server - * generateDhis2UID({limit: 3}); - */ -export function generateDhis2UID(options, callback) { - return state => { - const expandedOptions = expandAndSetOperation( - options, - state, - 'generateDhis2UID' - ); - const limit = { limit: options?.limit ?? 1 }; - - delete options?.limit; - - return getData('system/id', limit, expandedOptions, callback)(state); - }; -} - -/** - * Discover `DHIS2` `api` `endpoint` `query parameters` and allowed `operators` for a given resource's endpoint. - * @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` - * @returns {Operation} - * @example Example getting a list of `parameters allowed` on a given `endpoint` for specific `http method` - * discover('post', '/trackedEntityInstances') - */ -export function discover(httpMethod, endpoint) { - return state => { - Log.info( - `Discovering query/import parameters for ${httpMethod} on ${endpoint}` - ); - return axios - .get( - 'https://dhis2.github.io/dhis2-api-specification/spec/metadata_openapi.json', - { - transformResponse: [ - data => { - let tempData = JSON.parse(data); - let filteredData = tempData.paths[endpoint][httpMethod]; - return { - ...filteredData, - parameters: filteredData.parameters.reduce( - (acc, currentValue) => { - let index = currentValue['$ref'].lastIndexOf('/') + 1; - let paramRef = currentValue['$ref'].slice(index); - let param = tempData.components.parameters[paramRef]; - - if (param.schema['$ref']) { - let schemaRefIndex = - param.schema['$ref'].lastIndexOf('/') + 1; - let schemaRef = - param.schema['$ref'].slice(schemaRefIndex); - param.schema = tempData.components.schemas[schemaRef]; - } - - param.schema = JSON.stringify(param.schema); - - let descIndex; - if ( - indexOf(param.description, ',') === -1 && - indexOf(param.description, '.') > -1 - ) - descIndex = indexOf(param.description, '.'); - else if ( - indexOf(param.description, ',') > -1 && - indexOf(param.description, '.') > -1 - ) { - descIndex = - indexOf(param.description, '.') < - indexOf(param.description, ',') - ? indexOf(param.description, '.') - : indexOf(param.description, ','); - } else { - descIndex = param.description.length; - } - - param.description = param.description.slice(0, descIndex); - - acc[paramRef] = param; - return acc; - }, - {} - ), - }; - }, - ], - } - ) - .then(result => { - Log.info( - `\t=======================================================================================\n\tQuery Parameters for ${httpMethod} on ${endpoint} [${ - result.data.description ?? '' - }]\n\t=======================================================================================` - ); - console.table(result.data.parameters, [ - 'in', - 'required', - 'description', - ]); - console.table(result.data.parameters, ['schema']); - console.log( - `=========================================Responses===============================\n${prettyJson( - result.data.responses - )}\n=======================================================================================` - ); - return { ...state, data: result.data }; - }); - }; -} - -/** - * Get analytical, aggregated data - * - The analytics resource is powerful as it lets you query and retrieve data aggregated along all available data dimensions. - * - For instance, you can ask the analytics resource to provide the aggregated data values for a set of data elements, periods and organisation units. - * - Also, you can retrieve the aggregated data for a combination of any number of dimensions based on data elements and organisation unit group sets. - * @public - * @function - * @param {Object} params - Analytics `query parameters`, e.g. `{dx: 'fbfJHSPpUQD;cYeuwXTCPkU',filters: ['pe:2014Q1;2014Q2','ou:O6uvpzGd5pu;lc3eMKXaEfw']}`. Run `discover` or visit {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html#analytics DHIS2 API docs} to get the params available. - * @param {{apiVersion: number,responseType: string}}[options] - `Optional` options for `getAnalytics` operation. Defaults to `{apiVersion: state.configuration.apiVersion, responseType: 'json'}`. - * @param {function} [callback] - Callback to handle response - * @returns {Operation} - * @example Example getting only records where the data value is greater or equal to 6500 and less than 33000 - * getAnalytics({ - * dimensions: [ - * 'dx:fbfJHSPpUQD;cYeuwXTCPkU', - * 'pe:2014', - * 'ou:O6uvpzGd5pu;lc3eMKXaEfw', - * ], - * measureCriteria: 'GE:6500;LT:33000', - * }); - */ -export function getAnalytics(params, options, callback) { - return state => { - const expandedOptions = expandAndSetOperation( - options, - state, - 'getAnalytics' - ); - return getData(`analytics`, params, expandedOptions, callback)(state); - }; -} - -/** - * Get DHIS2 api resources - * @public - * @function - * @param {Object} [params] - The `optional` query parameters for this endpoint. E.g `{filter: 'singular:like:attribute'}`. - * @param {{filter: string, fields: string, responseType: string}} [options] - The `optional` options, specifiying the filter expression. E.g. `singular:eq:attribute`. - * @param {function} [callback] - The `optional callback function that will be called to handle data returned by this function. - * @returns {Operation} - * @example Example getting a resource named `attribute`, in `xml` format, returning all the fields - * getResources('dataElement', { - * filter: 'singular:eq:attribute', - * fields: '*', - * responseType: 'xml', - * }); - */ -export function getResources(params, options, callback) { - return state => { - params = expandReferences(params)(state); - - options = expandReferences(options)(state); - - const operationName = 'getResources'; - - const { username, password, hostUrl } = state.configuration; - - const responseType = options?.responseType ?? 'json'; - - const filter = params?.filter; - - const queryParams = params; - - const headers = { - Accept: CONTENT_TYPES[responseType] ?? 'application/json', - }; - - const path = '/resources'; - - const url = buildUrl(path, hostUrl, null, false); - - const transformResponse = function (data, headers) { - if (filter) { - if ( - (headers['content-type']?.split(';')[0] ?? null) === - CONTENT_TYPES.json - ) { - let tempData = JSON.parse(data); - return { - ...tempData, - resources: applyFilter(tempData.resources, ...parseFilter(filter)), - }; - } else { - Log.warn( - 'Filters on this resource are only supported for json content types. Skipping filtering ...' - ); - } - } - return data; - }; - - logOperation(operationName); - - logWaitingForServer(url, queryParams); - - warnExpectLargeResult(queryParams, url); - - return axios - .request({ - url, - method: 'GET', - auth: { username, password }, - responseType, - headers, - params: queryParams, - transformResponse, - }) - .then(result => { - Log.info( - `${operationName} succeeded. The result of this operation will be in ${operationName} state.data or in your ${operationName} callback.` - ); - if (callback) return callback(composeNextState(state, result.data)); - return composeNextState(state, result.data); - }); - }; -} - -/** - * Get schema of a given resource type, in any data format supported by DHIS2 - * @public - * @function - * @param {string} resourceType - The type of resource to be updated(`singular` version of the `resource name`). E.g. `dataElement`, `organisationUnit`, etc. Run `getResources` to see available resources and their corresponding `singular` names. - * @param {Object} params - Optional `query parameters` for the `getSchema` operation. e.g. `{ fields: 'properties' ,skipPaging: true}`. Run`discover` or See {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html#metadata-export-examples DHIS2 API Docs} - * @param {{apiVersion: number,resourceType: string}} [options] - Optional options for `getSchema` method. Defaults to `{apiVersion: state.configuration.apiVersion, responseType: 'json'}` - * @param {function} [callback] - Optional `callback` to handle the response - * @returns {Operation} - * @example Example getting the `schema` for `dataElement` in XML - * getSchema('dataElement', '{ fields: '*' }, { responseType: 'xml' }); - */ -export function getSchema(resourceType, params, options, callback) { - return state => { - resourceType = expandReferences(resourceType)(state); - - params = expandReferences(params)(state); - - options = expandReferences(options)(state); - - const operationName = 'getSchema'; - - const { username, password, hostUrl } = state.configuration; - - const responseType = options?.responseType ?? 'json'; - - const filters = params?.filters; - - delete params?.filters; - - let queryParams = new URLSearchParams(params); - - filters?.map(f => queryParams.append('filter', f)); - - const apiVersion = options?.apiVersion ?? state.configuration.apiVersion; - - const headers = { - Accept: CONTENT_TYPES[responseType] ?? 'application/json', - }; - - const url = buildUrl(`/schemas/${resourceType ?? ''}`, hostUrl, apiVersion); - - logOperation(operationName); - - logApiVersion(apiVersion); - - logWaitingForServer(url, queryParams); - - warnExpectLargeResult(resourceType, url); - - return axios - .request({ - method: 'GET', - url, - auth: { - username, - password, - }, - responseType, - params: queryParams, - headers, - }) - .then(result => { - Log.info( - `${operationName} succeeded. The result of this operation will be in ${operationName} state.data or in your ${operationName} callback.` - ); - if (callback) return callback(composeNextState(state, result.data)); - return composeNextState(state, result.data); - }); - }; -} - -/** - * Get data. Generic helper method for getting data of any kind from DHIS2. - * - This can be used to get `DataValueSets`,`events`,`trackedEntityInstances`,`etc.` - * @public - * @function - * @param {string} resourceType - The type of resource to get(use its `plural` name). E.g. `dataElements`, `trackedEntityInstances`,`organisationUnits`, etc. - * @param {Object} [params] - Optional `query parameters` e.g. `{ou: 'DiszpKrYNg8'}`. Run `discover` or see {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html DHIS2 docs} for more details on which params to use for a given type of resource. - * @param {{apiVersion: number,operationName: string,responseType: string}}[options] - `Optional` options for `getData` operation. Defaults to `{operationName: 'getData', apiVersion: state.configuration.apiVersion, responseType: 'json'}`. - * @param {function} [callback] - Optional callback to handle the response - * @returns {Operation} state - * @example Example getting one `trackedEntityInstance` with `Id` 'dNpxRu1mWG5' for a given `orgUnit(DiszpKrYNg8)` - * getData('trackedEntityInstances', { - * fields: '*', - * ou: 'DiszpKrYNg8', - * entityType: 'nEenWmSyUEp', - * trackedEntityInstance: 'dNpxRu1mWG5', - * }); - */ -export function getData(resourceType, params, options, callback) { - return state => { - resourceType = expandReferences(resourceType)(state); - - params = expandReferences(params)(state); - - options = expandReferences(options)(state); - - const operationName = options?.operationName ?? 'getData'; - - const { username, password, hostUrl } = state.configuration; - - const responseType = options?.responseType ?? 'json'; - - const filters = params?.filters; - - const dimensions = params?.dimensions; - - delete params?.filters; - - let queryParams = new URLSearchParams(params); - - filters?.map(f => queryParams.append('filter', f)); - - dimensions?.map(d => queryParams.append('dimension', d)); - - const apiVersion = options?.apiVersion ?? state.configuration.apiVersion; - - const headers = { - Accept: CONTENT_TYPES[responseType] ?? 'application/json', - }; - - const url = buildUrl(`/${resourceType}`, hostUrl, apiVersion); - - logOperation(operationName); - - logApiVersion(apiVersion); - - logWaitingForServer(url, queryParams); - - warnExpectLargeResult(resourceType, url); - - return axios - .request({ - method: 'GET', - url, - auth: { - username, - password, - }, - responseType, - params: queryParams, - headers, - }) - .then(result => { - Log.info( - `${operationName} succeeded. The result of this operation will be in ${operationName} state.data or in your callback.` - ); - if (callback) return callback(composeNextState(state, result.data)); - return composeNextState(state, result.data); - }); - }; -} - -/** - * Get metadata. A generic helper function to get metadata records from a given DHIS2 instance - * @public - * @function - * @param {string[]} resources - Required. List of metadata resources to fetch. E.g. `['organisationUnits', 'attributes']` or like `'dataSets'` if you only want a single type of resource. See `getResources` to see the types of resources available. - * @param {Object} [params] - Optional `query parameters` e.g. `{filters: ['name:like:ANC'],fields:'*'}`. See `discover` or visit {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html#metadata-export DHIS2 API docs} - * @param {{apiVersion: number,operationName: string,resourceType: string}} [options] - Optional `options` for `getMetadata` operation. Defaults to `{operationName: 'getMetadata', apiVersion: state.configuration.apiVersion, responseType: 'json'}` - * @param {function} [callback] - Optional `callback` to handle the response - * @returns {Operation} - * @example Example getting a list of `data elements` and `indicators` where `name` includes the word **ANC** - * getMetadata(['dataElements', 'indicators'], { - * filters: ['name:like:ANC'], - * }); - */ -export function getMetadata(resources, params, options, callback) { - return state => { - resources = expandReferences(resources)(state); - - params = expandReferences(params)(state); - - options = expandReferences(options)(state); - - const operationName = 'getMetadata'; - - const { username, password, hostUrl } = state.configuration; - - const responseType = options?.responseType ?? 'json'; - - if (typeof resources === 'string') { - let res = {}; - res[resources] = true; - resources = res; - } else { - resources = resources.reduce((acc, currentValue) => { - acc[currentValue] = true; - return acc; - }, {}); - } - - let queryParams = { - ...resources, - ...params, - }; - - const filters = queryParams?.filters; - - delete queryParams?.filters; - - queryParams = new URLSearchParams(queryParams); - - filters?.map(f => queryParams.append('filter', f)); - - const apiVersion = options?.apiVersion ?? state.configuration.apiVersion; - - const headers = { - Accept: CONTENT_TYPES[responseType] ?? 'application/json', - }; - - const url = buildUrl('/metadata', hostUrl, apiVersion); - - logOperation(operationName); - - logApiVersion(apiVersion); - - logWaitingForServer(url, queryParams); - - warnExpectLargeResult(queryParams, url); - - return axios - .request({ - method: 'GET', - url, - auth: { - username, - password, - }, - responseType, - params: queryParams, - headers, - }) - .then(result => { - Log.info( - `${operationName} succeeded. The result of this operation will be in ${operationName} state.data or in your callback.` - ); - if (callback) return callback(composeNextState(state, result.data)); - return composeNextState(state, result.data); - }); - }; -} - -/** - * create data. A generic helper method to create a record of any kind in DHIS2 - * @public - * @function - * @param {string} resourceType - Type of resource to create. E.g. `trackedEntityInstances` - * @param {Object} data - Data that will be used to create a given instance of resource - * @param {Object} [options] - Optional `options` to control the behavior of the `create` operation.` Defaults to `{operationName: 'create', apiVersion: null, responseType: 'json'}` - * @param {Object} [params] - Optional `import parameters` for a given a resource. E.g. `{dryRun: true, importStrategy: CREATE}` See {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html DHIS2 API documentation} or {@link discover}. Defauls to `DHIS2 default params` for a given resource type. - * @param {function} [callback] - Optional callback to handle the response - * @returns {Operation} - * @example - Example `expression.js` of `create` - * create('events', { - * program: 'eBAyeGv0exc', - * orgUnit: 'DiszpKrYNg8', - * eventDate: date, - * status: 'COMPLETED', - * completedDate: date, - * storedBy: 'admin', - * coordinate: { - * latitude: 59.8, - * longitude: 10.9, - * }, - * dataValues: [ - * { - * dataElement: 'qrur9Dvnyt5', - * value: '33', - * }, - * { - * dataElement: 'oZg33kd9taw', - * value: 'Male', - * }, - * { - * dataElement: 'msodh3rEMJa', - * value: date, - * }, - * ], - * }); - */ -export function create(resourceType, data, options, params, callback) { - return state => { - resourceType = expandReferences(resourceType)(state); - - const body = expandReferences(data)(state); - - options = expandReferences(options)(state); - - params = expandReferences(params)(state); - - const operationName = options?.operationName ?? 'create'; - - const { username, password, hostUrl } = state.configuration; - - const responseType = options?.responseType ?? 'json'; - - const filters = params?.filters; - - delete params?.filters; - - let queryParams = new URLSearchParams(params); - - const apiVersion = options?.apiVersion ?? state.configuration.apiVersion; - - const url = buildUrl('/' + resourceType, hostUrl, apiVersion); - - const headers = { - Accept: CONTENT_TYPES[responseType] ?? 'application/json', - 'Content-Type': 'application/json', - }; - - logOperation(operationName); - - logApiVersion(apiVersion); - - logWaitingForServer(url, queryParams); - - warnExpectLargeResult(resourceType, url); - - return axios - .request({ - method: 'POST', - url, - auth: { - username, - password, - }, - params: queryParams, - data: body, - headers, + * @param {string} resourceType - The type of a resource to `insert` or `update`. E.g. `trackedEntityInstances` + * @param {Object} data - The update data containing new values + * @param {{replace:boolean, apiVersion: number,strict: boolean,responseType: string}} [options] - `Optional` options for `upsertTEI` operation. Defaults to `{replace: false, apiVersion: state.configuration.apiVersion,strict: true,responseType: 'json'}`. + * @param {function} [callback] - Optional callback to handle the response + * @throws {RangeError} - Throws range error + * @returns {Operation} + * @example Example `expression.js` of upsert + * upsert( + * 'trackedEntityInstances', + * { + * attributeId: 'lZGmxYbs97q', + * attributeValue: state => + * state.data.attributes.find(obj => obj.attribute === 'lZGmxYbs97q') + * .value, + * }, + * state.data, + * { ou: 'TSyzvBiovKh' } + * ); + * @todo Tweak/refine to mimic implementation based on the following inspiration: {@link https://sqlite.org/lang_upsert.html sqlite upsert} and {@link https://wiki.postgresql.org/wiki/UPSERT postgresql upsert} + * @todo Test implementation for upserting metadata + * @todo Test implementation for upserting data values + * @todo Implement the updateCondition + */ +export function upsert(resourceType, data, options, callback) { + return state => { + return get( + resourceType, + options + )(state) + .then(res => { + const resources = res.data[resourceType]; + if (resources.length > 1) { + throw new RangeError( + `Cannot upsert on Non-unique attribute. The operation found more than one records for your request.` + ); + } else if (resources.length <= 0) { + return create(resourceType, data, options, callback)(state); + } else { + const pathName = + resourceType === 'trackedEntityInstances' + ? 'trackedEntityInstance' + : 'id'; + const path = resources[0][pathName]; + return update(resourceType, path, data, options, callback)(state); + } }) - .then(result => { - Log.info( - `${operationName} succeeded. Created ${resourceType}: ${ - result.data.response?.importSummaries - ? result.data.response.importSummaries[0].href - : result.data.response?.reference - }.\nSummary:\n${prettyJson(result.data)}` - ); - if (callback) return callback(composeNextState(state, result.data)); - return composeNextState(state, result.data); + .catch(err => { + throw err; }); }; } /** - * Update data. A generic helper function to update a resource object of any type. - * - It requires to send `all required fields` or the `full body` + * Discover `DHIS2` `api` `endpoint` `query parameters` and allowed `operators` for a given resource's endpoint. * @public * @function - * @param {string} resourceType - The type of resource to be updated. E.g. `dataElements`, `organisationUnits`, etc. - * @param {string} path - The `id` or `path` to the `object` to be updated. E.g. `FTRrcoaog83` or `FTRrcoaog83/{collection-name}/{object-id}` - * @param {Object} data - Data to update. It requires to send `all required fields` or the `full body`. If you want `partial updates`, use `patch` operation. - * @param {Object} [params] - Optional `update` parameters 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} - * @param {{apiVersion: number,operationName: string,resourceType: string}} [options] - Optional options for update method. Defaults to `{operationName: 'update', apiVersion: state.configuration.apiVersion, responseType: 'json'}` - * @param {function} [callback] - Optional callback to handle the response + * @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` * @returns {Operation} - * @example Example `updating` a `data element` - * update('dataElements', 'FTRrcoaog83', - * { - * displayName: 'New display name', - * aggregationType: 'SUM', - * domainType: 'AGGREGATE', - * valueType: 'NUMBER', - * name: 'Accute Flaccid Paralysis (Deaths < 5 yrs)', - * shortName: 'Accute Flaccid Paral (Deaths < 5 yrs)', - * }); - * + * @example Example getting a list of `parameters allowed` on a given `endpoint` for specific `http method` + * discover('post', '/trackedEntityInstances') */ -export function update(resourceType, path, data, params, options, callback) { +export function discover(httpMethod, endpoint) { return state => { - resourceType = expandReferences(resourceType)(state); - - path = expandReferences(path)(state); - - const body = expandReferences(data)(state); - - params = expandReferences(params)(state); - - options = expandReferences(options)(state); - - const { username, password, hostUrl } = state.configuration; - - const operationName = options?.operationName ?? 'update'; - - const responseType = options?.responseType ?? 'json'; - - const filters = params?.filters; - - delete params?.filters; - - let queryParams = new URLSearchParams(params); - - filters?.map(f => queryParams.append('filter', f)); - - const apiVersion = options?.apiVersion ?? state.configuration.apiVersion; - - const url = buildUrl('/' + resourceType + '/' + path, hostUrl, apiVersion); - - const headers = { - Accept: CONTENT_TYPES[responseType] ?? 'application/json', - }; + Log.info( + `Discovering query/import parameters for ${httpMethod} on ${endpoint}` + ); + return axios + .get( + 'https://dhis2.github.io/dhis2-api-specification/spec/metadata_openapi.json', + { + transformResponse: [ + data => { + let tempData = JSON.parse(data); + let filteredData = tempData.paths[endpoint][httpMethod]; + return { + ...filteredData, + parameters: filteredData.parameters.reduce( + (acc, currentValue) => { + let index = currentValue['$ref'].lastIndexOf('/') + 1; + let paramRef = currentValue['$ref'].slice(index); + let param = tempData.components.parameters[paramRef]; - logOperation(operationName); + if (param.schema['$ref']) { + let schemaRefIndex = + param.schema['$ref'].lastIndexOf('/') + 1; + let schemaRef = param.schema['$ref'].slice( + schemaRefIndex + ); + param.schema = tempData.components.schemas[schemaRef]; + } - logApiVersion(apiVersion); + param.schema = JSON.stringify(param.schema); - logWaitingForServer(url, queryParams); + let descIndex; + if ( + indexOf(param.description, ',') === -1 && + indexOf(param.description, '.') > -1 + ) + descIndex = indexOf(param.description, '.'); + else if ( + indexOf(param.description, ',') > -1 && + indexOf(param.description, '.') > -1 + ) { + descIndex = + indexOf(param.description, '.') < + indexOf(param.description, ',') + ? indexOf(param.description, '.') + : indexOf(param.description, ','); + } else { + descIndex = param.description.length; + } - warnExpectLargeResult(resourceType, url); + param.description = param.description.slice(0, descIndex); - return axios - .request({ - method: 'PUT', - url, - auth: { - username, - password, - }, - params: queryParams, - data: body, - headers, - }) + acc[paramRef] = param; + return acc; + }, + {} + ), + }; + }, + ], + } + ) .then(result => { Log.info( - `${operationName} succeeded. Updated ${resourceType}.\nSummary:\n${prettyJson( - result.data - )}` + `\t=======================================================================================\n\tQuery Parameters for ${httpMethod} on ${endpoint} [${ + result.data.description ?? '' + }]\n\t=======================================================================================` ); - if (callback) return callback(composeNextState(state, result.data)); - return composeNextState(state, result.data); + console.table(result.data.parameters, [ + 'in', + 'required', + 'description', + ]); + console.table(result.data.parameters, ['schema']); + console.log( + `=========================================Responses===============================\n${prettyJson( + result.data.responses + )}\n=======================================================================================` + ); + return { ...state, data: result.data }; }); }; } @@ -1702,246 +791,6 @@ export function del(resourceType, path, data, params, options, 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 `insert` or `update`. E.g. `trackedEntityInstances` - * @param {{attributeId: string,attributeValue:any}} uniqueAttribute - An object containing a `attributeId` and `attributeValue` which will be used to uniquely identify the record - * @param {Object} data - The update data containing new values - * @param {Object} [params] - Optional `import` parameters e.g. `{ou: 'lZGmxYbs97q', filters: ['w75KJ2mc4zz:EQ:Jane']}` - * @param {{replace:boolean, apiVersion: number,strict: boolean,responseType: string}} [options] - `Optional` options for `upsertTEI` operation. Defaults to `{replace: false, apiVersion: state.configuration.apiVersion,strict: true,responseType: 'json'}`. - * @param {function} [callback] - Optional callback to handle the response - * @throws {RangeError} - Throws range error - * @returns {Operation} - * @example - Example `expression.js` of upsert - * upsert( - * 'trackedEntityInstances', - * { - * attributeId: 'lZGmxYbs97q', - * attributeValue: state => - * state.data.attributes.find(obj => obj.attribute === 'lZGmxYbs97q') - * .value, - * }, - * state.data, - * { ou: 'TSyzvBiovKh' } - * ); - * @todo Tweak/refine to mimic implementation based on the following inspiration: {@link https://sqlite.org/lang_upsert.html sqlite upsert} and {@link https://wiki.postgresql.org/wiki/UPSERT postgresql upsert} - * @todo Test implementation for upserting metadata - * @todo Test implementation for upserting data values - * @todo Implement the updateCondition - */ -export function upsert( - resourceType, - uniqueAttribute, - data, - params, - options, - callback -) { - return state => { - resourceType = expandReferences(resourceType)(state); - - uniqueAttribute = expandReferences(uniqueAttribute)(state); - - const body = expandReferences(data)(state); - - params = expandReferences(params)(state); - - options = expandReferences(options)(state); - - const operationName = options?.operationName ?? 'upsert'; - - const { username, password, hostUrl } = state.configuration; - - const replace = options?.replace ?? false; - - const responseType = options?.responseType ?? 'json'; - - const { attributeId, attributeValue } = uniqueAttribute; - - const filters = params?.filters; - - delete params.filters; - - let queryParams = new URLSearchParams(params); - - filters?.map(f => queryParams.append('filter', f)); - - const op = resourceType === 'trackedEntityInstances' ? 'EQ' : 'eq'; - - queryParams.append('filter', `${attributeId}:${op}:${attributeValue}`); - - const apiVersion = options?.apiVersion ?? state.configuration.apiVersion; - - const url = buildUrl('/' + resourceType, hostUrl, apiVersion); - - const headers = { - Accept: CONTENT_TYPES[responseType] ?? 'application/json', - }; - - logOperation(operationName); - - logApiVersion(apiVersion); - - logWaitingForServer(url, queryParams); - - warnExpectLargeResult(resourceType, url); - - const getResouceName = () => { - return axios - .get(hostUrl + '/api/resources', { - auth: { username, password }, - transformResponse: [ - function (data, headers) { - let filter = `plural:eq:${resourceType}`; - if (filter) { - if ( - (headers['content-type']?.split(';')[0] ?? null) === - CONTENT_TYPES.json - ) { - let tempData = JSON.parse(data); - return { - ...tempData, - resources: applyFilter( - tempData.resources, - ...parseFilter(filter) - ), - }; - } else { - Log.warn( - 'Filters on this resource are only supported for json content types. Skipping filtering ...' - ); - } - } - return data; - }, - ], - }) - .then(result => result.data.resources[0].singular); - }; - - const findRecordsWithValueOnAttribute = () => { - console.log(queryParams); - return axios.request({ - method: 'GET', - url, - auth: { - username, - password, - }, - params: queryParams, - headers, - }); - }; - - Log.info( - `Checking if a record exists that matches this filter: attribute{ id: ${attributeId}, value: ${attributeValue} } ...` - ); - return Promise.all([ - getResouceName(), - findRecordsWithValueOnAttribute(), - ]).then(([resourceName, recordsWithValue]) => { - const recordsWithValueCount = recordsWithValue.data[resourceType].length; - if (recordsWithValueCount > 1) { - Log.error(''); - throw new RangeError( - `Cannot upsert on Non-unique attribute. The operation found more than one records with the same value of ${attributeValue} for ${attributeId}` - ); - } else if (recordsWithValueCount === 1) { - // TODO - // Log.info( - // `Unique record found, proceeding to checking if attribute is NULLABLE ...` - // ); - // if (recordsWithNulls.data[resourceType].length > 0) { - // throw new Error( - // `Cannot upsert on Nullable attribute. The operation found records with a NULL value on ${attributeId}.` - // ); - // } - Log.info( - `Attribute has unique values. Proceeding to ${ - replace ? 'replace' : 'merge' - } ...` - ); - - const row1 = recordsWithValue.data[resourceType][0]; - const useCustomPATCH = ['trackedEntityInstances'].includes(resourceType) - ? true - : false; - const method = replace - ? 'PUT' - : useCustomPATCH === true - ? 'PUT' - : 'PATCH'; - - const id = row1['id'] ?? row1[resourceName]; - - const updateUrl = `${url}/${id}`; - - const payload = useCustomPATCH - ? { - ...row1, - ...body, - attributes: [...row1.attributes, ...body.attributes], - } - : body; - - return axios - .request({ - method, - url: updateUrl, - auth: { - username, - password, - }, - data: payload, - params: queryParams, - headers, - }) - .then(result => { - Log.info( - `${operationName} succeeded. Updated ${resourceName}: ${updateUrl}.\nSummary:\n${prettyJson( - result.data - )}` - ); - if (callback) return callback(composeNextState(state, result.data)); - return composeNextState(state, result.data); - }); - } else if (recordsWithValueCount === 0) { - Log.info(`Existing record not found, proceeding to CREATE(POST) ...`); - - // We must delete the filter and ou params so the POST request is not interpreted as a GET request by the server - queryParams.delete('filter'); - queryParams.delete('ou'); - - return axios - .request({ - method: 'POST', - url, - auth: { - username, - password, - }, - data: body, - params: queryParams, - headers, - }) - .then(result => { - Log.info( - `${operationName} succeeded. Created ${resourceName}: ${ - result.data.response.importSummaries - ? result.data.response.importSummaries[0].href - : result.data.response?.reference - }.\nSummary:\n${prettyJson(result.data)}` - ); - if (callback) return callback(composeNextState(state, result.data)); - return composeNextState(state, result.data); - }); - } - }); - }; -} - /** * Gets an attribute value by its case-insensitive display name * @public diff --git a/src/Client.js b/src/Client.js new file mode 100644 index 0000000..cd050de --- /dev/null +++ b/src/Client.js @@ -0,0 +1,10 @@ +import axios from 'axios'; + +export function request({ method, url, data, options }) { + let headers = { 'Content-Type': 'application/json' }; + let req = { method, url, headers, ...options }; + if (method !== 'get') { + req = { ...req, data }; + } + return axios.request(req); +} diff --git a/src/Utils.js b/src/Utils.js index 5f05164..b2563e1 100644 --- a/src/Utils.js +++ b/src/Utils.js @@ -1,5 +1,6 @@ import { eq, filter, some, indexOf, lastIndexOf, trim } from 'lodash'; import axios from 'axios'; +import { expandReferences } from '@openfn/language-common'; export function composeSuccessMessage(operation) { return `${operation} succeeded. The body of this result will be available in state.data or in your callback.`; @@ -13,12 +14,14 @@ export function warnExpectLargeResult(paramOrResourceType, endpointUrl) { } export function logWaitingForServer(url, params) { - console.info( - 'Request params: ', - typeof params === 'object' && !(params instanceof URLSearchParams) - ? prettyJson(params) - : params - ); + if (params) { + console.info( + 'Request params: ', + typeof params === 'object' && !(params instanceof URLSearchParams) + ? prettyJson(params) + : params + ); + } console.info(`Waiting for response from ${url}`); } @@ -170,6 +173,89 @@ export function parseFilter(filterExpression) { return filterTokens; } +export function expandAndSetOperation(options, state, operationName) { + return { + operationName, + ...expandReferences(options)(state), + }; +} + +const isArray = variable => !!variable && variable.constructor === Array; + +export function nestArray(data, key) { + return isArray(data) ? { [key]: data } : data; +} + +function log(operationName, apiVersion, url, resourceType, params) { + logOperation(operationName); + logApiVersion(apiVersion); + logWaitingForServer(url, params); + warnExpectLargeResult(resourceType, url); +} + +function extractValuesForAxios(operationName, values) { + return state => { + const apiVersion = + values.options?.apiVersion ?? state.configuration.apiVersion; + const { username, password, hostUrl } = state.configuration; + const auth = { username, password }; + + let urlString = '/' + values.resourceType; + if (operationName === 'update') { + urlString += '/' + values.path; + } + const url = buildUrl(urlString, hostUrl, apiVersion); + + let urlParams = null; + if (operationName === 'get' || operationName === 'upsert') { + const filters = values.options?.params?.filters; + const dimensions = values.options?.params?.dimensions; + delete values.options?.params?.filters; + delete values.options?.params?.dimensions; + urlParams = new URLSearchParams(values.options?.params); + filters?.map(f => urlParams.append('filter', f)); + dimensions?.map(d => urlParams.append('dimension', d)); + } + + const resourceType = values.resourceType; + const data = values.data; + const callback = values.callback; + + const extractedValues = { + resourceType, + data, + apiVersion, + auth, + url, + urlParams, + callback, + }; + + return extractedValues; + }; +} + +export function expandExtractAndLog(operationName, initialParams) { + return state => { + const { + resourceType, + data, + apiVersion, + auth, + url, + urlParams, + callback, + } = extractValuesForAxios( + operationName, + expandReferences(initialParams)(state) + )(state); + + log(operationName, apiVersion, url, resourceType, urlParams); + + return { url, data, resourceType, auth, urlParams, callback }; + }; +} + export const CONTENT_TYPES = { xml: 'application/xml', json: 'application/json', diff --git a/test/index.js b/test/index.js index eae93f8..8050b3d 100644 --- a/test/index.js +++ b/test/index.js @@ -1,47 +1,10 @@ import { expect } from 'chai'; +import { execute, create, update } from '../lib/Adaptor'; import { dataValue } from '@openfn/language-common'; -import { - execute, - getData, - upsert, - upsertTEI, - create, - attribute, - update, - patch, - del, - getMetadata, - getSchema, - getResources, - getAnalytics, - discover, - generateDhis2UID, - getDataValues, - createEvents, - enrollTEI, -} from '../lib/Adaptor'; +import { buildUrl, nestArray } from '../lib/Utils'; import nock from 'nock'; -import { - upsertNewState, - upsertExistingState, - upsertExistingTEIState, - upsertNewTEIState, - createState, - updateState, - patchState, - delState, - getState, - createBulkUnrelatedDataValues, - createRelatedDataValues, - createEventsState, - sendDataForMultipleEventsState, - enrollTEIState, - demoVersion, -} from './ClientFixtures'; -import { permissions, userRoles, personAttributes } from './SetupFixtures'; -import { result } from 'lodash'; -import { prettyJson } from '../src/Utils'; -import { createDataValues } from '../src/Adaptor'; + +const testServer = nock('https://play.dhis2.org/2.36.4'); describe('execute', () => { it('executes each operation in sequence', done => { @@ -91,760 +54,166 @@ describe('execute', () => { }); }); -describe('live adaptor testing', () => { - console.log(demoVersion); - // before ALL tests run, we must re-configure the dhis2 environment - before(function () { - this.timeout(30000); - let state = { - configuration: { - username: 'admin', - password: 'district', - hostUrl: `https://play.dhis2.org/${demoVersion}`, - }, - }; - - return execute(update('users', 'xE7jOejl9FI', permissions))(state) - .then(() => { - console.log('updated user permissions'); - }) - .then(() => { - console.log('updated user roles'); - return execute(update('users', 'xE7jOejl9FI', userRoles))(state); - }) - .then(() => { - console.log('assigned attributes to person entity type'); - return execute( - update('trackedEntityTypes', 'nEenWmSyUEp', personAttributes, { - mergeMode: 'REPLACE', - }) - )(state); +describe('CREATE', () => { + const state = { + configuration: { + username: 'admin', + password: 'district', + hostUrl: 'https://play.dhis2.org/2.36.4', + }, + data: { + program: 'program1', + orgUnit: 'org50', + status: 'COMPLETED', + date: '02-02-20', + }, + }; + + 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', }) - .then(() => { - // PUT add Programs... // ?? - // console.log('updated programs'); - console.log('dhis2 instance configured, starting tests...'); + .times(2) + .matchHeader('authorization', 'Basic YWRtaW46ZGlzdHJpY3Q=') + .reply(200, { + httpStatus: 'OK', + message: 'the response', }); - }); - - describe('buildUrl for getData', () => { - before(() => { - nock('https://play.dhis2.org/2.35.0/') - .get(uri => uri.includes('api/34')) - .reply(200, { - trackedEntityInstances: ['from v34'], - }); - nock('https://play.dhis2.org/2.35.0/') - .get(uri => uri.includes('api/999')) - .reply(200, { - trackedEntityInstances: ['from v999'], - }); - }); - - it('should respect api version when passed through configuration', () => { - let state = { - configuration: { - username: 'admin', - password: 'district', - hostUrl: `https://play.dhis2.org/${demoVersion}`, - apiVersion: 34, - }, - }; - - return execute(getData('trackedEntityInstances', {}))(state).then( - state => { - expect(state.data.trackedEntityInstances[0]).to.eq('from v34'); - } - ); - }).timeout(10 * 1000); - - it('should respect the api version when passed through the options argument', () => { - let state = { - configuration: { - username: 'admin', - password: 'district', - hostUrl: `https://play.dhis2.org/${demoVersion}`, - }, - }; - - return execute( - getData('trackedEntityInstances', {}, { apiVersion: 999 }) - )(state).then(state => { - expect(state.data.trackedEntityInstances[0]).to.eq('from v999'); - }); - }).timeout(10 * 1000); - }); - - describe('getData', () => { - before(() => { - nock.cleanAll(); - nock.enableNetConnect(); - }); - - it("should return one trackedEntityInstance with trackedInstanceInstance Id 'dNpxRu1mWG5' for a given orgUnit(DiszpKrYNg8)", () => { - let state = { - configuration: { - username: 'admin', - password: 'district', - hostUrl: `https://play.dhis2.org/${demoVersion}`, - }, - }; - - return execute( - getData('trackedEntityInstances', { - fields: '*', - ou: 'DiszpKrYNg8', - entityType: 'nEenWmSyUEp', - trackedEntityInstance: 'dNpxRu1mWG5', - }) - )(state).then(state => { - const instances = state.data.trackedEntityInstances; - expect(instances.length).to.eq(1); - expect(instances[0].trackedEntityInstance).to.eq('dNpxRu1mWG5'); - }); - }).timeout(10 * 1000); + const response = await execute(create('events', state => state.data))( + state + ); + expect(response.data).to.eql({ httpStatus: 'OK', message: 'the response' }); }); - describe('upsert', () => { - let state = upsertExistingState; - state.attributeVal = state => - state.data.attributes.find(obj => obj.attribute === 'lZGmxYbs97q').value; - before(() => { - nock.cleanAll(); - nock.enableNetConnect(); - }); - - it('should create a new TEI', () => { - return execute( - upsert( - 'trackedEntityInstances', - { - attributeId: 'lZGmxYbs97q', - attributeValue: state => - state.data.attributes.find(obj => obj.attribute === 'lZGmxYbs97q') - .value, - }, - state.data, - { ou: 'TSyzvBiovKh' } - ) - )(state).then(result => { - expect(result.data.httpStatus).to.eq('OK'); - expect(result.data.httpStatusCode).to.eq(200); - expect(result.data.response.imported).to.eq(1); - expect(result.data.response.updated).to.eq(0); - expect(result.data.response.deleted).to.eq(0); - expect(result.data.response.ignored).to.eq(0); - }); - }).timeout(20 * 1000); - - it('should update an existing TEI when a matching TEI is found by attribute ID', () => { - return execute( - upsert( - 'trackedEntityInstances', - { - attributeId: 'lZGmxYbs97q', - attributeValue: state.attributeVal, - }, - state.data, - { ou: 'TSyzvBiovKh' } - ) - )(state).then(state => { - expect(state.data.response.importCount.imported).to.eq(0); - expect(state.data.response.importCount.updated).to.eq(1); - expect(state.data.response.importCount.deleted).to.eq(0); - expect(state.data.response.importCount.ignored).to.eq(0); - expect(state.data.response.importCount.ignored).to.eq(0); - }); - }).timeout(30 * 1000); - - it('should create a new TEI when a matching TEI is not found by attribute ID', () => { - let state = upsertNewState; - - return execute( - upsert( - 'trackedEntityInstances', - { - attributeId: 'lZGmxYbs97q', - attributeValue: state => - state.data.attributes.find(obj => obj.attribute === 'lZGmxYbs97q') - .value, - }, - state.data, - { ou: 'TSyzvBiovKh' } - ) - )(state).then(state => { - expect(state.data.response.imported).to.eq(1); - expect(state.data.response.updated).to.eq(0); - expect(state.data.response.deleted).to.eq(0); - expect(state.data.response.ignored).to.eq(0); - }); - }).timeout(20 * 1000); - }); - - describe('upsertTEI', () => { - before(() => { - nock.cleanAll(); - nock.enableNetConnect(); - }); - - it('should update an existing TEI when a matching TEI is found by attribute ID', () => { - let state = upsertExistingTEIState; - - return execute(upsertTEI('lZGmxYbs97q', state.data))(state).then( - state => { - expect(state.data.response.importCount.imported).to.eq(0); - expect(state.data.response.importCount.updated).to.eq(1); - expect(state.data.response.importCount.deleted).to.eq(0); - expect(state.data.response.importCount.ignored).to.eq(0); - } - ); - }).timeout(20 * 1000); - - it('should create a new TEI when a matching TEI is not found by attribute ID', () => { - let state = upsertNewTEIState; - - return execute(upsertTEI('lZGmxYbs97q', state.data))(state).then( - state => { - expect(state.data.response.imported).to.eq(1); - expect(state.data.response.updated).to.eq(0); - expect(state.data.response.deleted).to.eq(0); - expect(state.data.response.ignored).to.eq(0); - } - ); - }).timeout(20 * 1000); - - it('should allow the user to build a TEI object from a generic state', () => { - let state = { - ...upsertNewTEIState, - data: { - form: { - name: 'Taylor', - uniqueId: '1135353', - organization: 'TSyzvBiovKh', - programsJoined: ['fDd25txQckK'], - }, - }, - }; - - return execute( - upsertTEI('lZGmxYbs97q', { - orgUnit: state.data.form.organization, - trackedEntityType: 'nEenWmSyUEp', - attributes: [ - { - attribute: 'w75KJ2mc4zz', - value: state.data.form.name, - }, - { - attribute: 'lZGmxYbs97q', - value: state.data.form.uniqueId, - }, - ], - enrollments: state => - state.data.form.programsJoined.map(item => ({ - orgUnit: state.data.form.organization, - program: item, - programState: 'lST1OZ5BDJ2', - enrollmentDate: '2021-01-05', - incidentDate: '2021-01-05', - })), - }) - )(state).then(state => { - expect(state.data.response.status).to.eq('SUCCESS'); - expect(state.data.httpStatusCode).to.eq(200); - expect( - state.data.response.deleted ?? state.data.response.importCount.deleted - ).to.eq(0); - expect( - state.data.response.ignored ?? state.data.response.importCount.ignored - ).to.eq(0); - }); - }).timeout(20 * 1000); - - it('should allow the user to use `attribute` and `dataValue` helper functions', () => { - let state = { - ...upsertNewTEIState, - data: { - form: { - name: 'Taylor', - uniqueId: '1135354', - organization: 'TSyzvBiovKh', - programsJoined: ['fDd25txQckK'], - }, - }, - }; - - return execute( - upsertTEI('lZGmxYbs97q', { - orgUnit: dataValue('form.organization'), - trackedEntityType: 'nEenWmSyUEp', - attributes: [ - attribute('w75KJ2mc4zz', dataValue('form.name')), - attribute('lZGmxYbs97q', dataValue('form.uniqueId')), - ], - enrollments: state => - state.data.form.programsJoined.map(item => ({ - orgUnit: dataValue('form.organization'), - program: item, - programState: 'lST1OZ5BDJ2', - enrollmentDate: '2021-01-05', - incidentDate: '2021-01-05', - })), - }) - )(state).then(state => { - expect(state.data.response.status).to.eq('SUCCESS'); - expect(state.data.httpStatusCode).to.eq(200); - expect( - state.data.response.deleted ?? state.data.response.importCount.deleted - ).to.eq(0); - expect( - state.data.response.ignored ?? state.data.response.importCount.ignored - ).to.eq(0); - }); - }).timeout(20 * 1000); - - it('should allow the user to use `arrow function` to access data', () => { - let state = { - ...upsertNewTEIState, - data: { - form: { - name: 'Taylor', - uniqueId: '1135354', - organization: 'TSyzvBiovKh', - programsJoined: ['fDd25txQckK'], - }, - }, - }; - - return execute( - upsertTEI('lZGmxYbs97q', { - orgUnit: state => state.data.form.organization, - trackedEntityType: 'nEenWmSyUEp', - attributes: [ - attribute('w75KJ2mc4zz', state => state.data.form.name), - attribute('lZGmxYbs97q', state => state.data.form.uniqueId), - ], - enrollments: state => - state.data.form.programsJoined.map(item => ({ - orgUnit: state.data.form.organization, - program: item, - programState: 'lST1OZ5BDJ2', - enrollmentDate: '2021-01-05', - incidentDate: '2021-01-05', - })), - }) - )(state).then(state => { - expect(state.data.response.status).to.eq('SUCCESS'); - expect(state.data.httpStatusCode).to.eq(200); - expect( - state.data.response.deleted ?? state.data.response.importCount.deleted - ).to.eq(0); - expect( - state.data.response.ignored ?? state.data.response.importCount.ignored - ).to.eq(0); - }); - }).timeout(20 * 1000); - }); - - describe('create', () => { - before(() => { - nock.cleanAll(); - nock.enableNetConnect(); - }); - - it.skip( - 'should create a new single event and link it to a given program', - () => { - let state = createState; - return execute(create('events', state.data))(state).then(state => { - expect(state.data.response.imported).to.eq(1); - expect(state.data.response.updated).to.eq(0); - expect(state.data.response.deleted).to.eq(0); - expect(state.data.response.ignored).to.eq(0); - }); - } - ).timeout(20 * 1000); - }); - - describe('update', () => { - let state = updateState; - state.data.name += Date.now(); - before(() => { - nock.cleanAll(); - nock.enableNetConnect(); - }); - - it('should update the name of a data element', () => { - return execute(update('dataElements', state.data.id, state.data))( - state - ).then(result => { - expect(result.data.httpStatusCode).to.eq(200); - expect(result.data.response.uid).to.eq(state.data.id); - }); - }).timeout(20 * 1000); - - it('should verify that the name of the data element was updated', () => { - return execute(getData(`dataElements/${state.data.id}`))(state).then( - result => { - expect(result.data.name).to.eq(state.data.name); - } - ); - }).timeout(20 * 1000); - }); - - describe('patch', () => { - let state = patchState; - state.id = 'FTRrcoaog83'; - before(() => { - nock.cleanAll(); - nock.enableNetConnect(); - }); - - it('should do a partial update(patch) of a data element', () => { - return execute(patch('dataElements', state.id, state.data))(state).then( - result => { - expect(result.data.status).to.eq(204); - } - ); - }).timeout(20 * 1000); - - it('should verify that the name of the data element was updated', () => { - return execute(getData(`dataElements/${state.id}`))(state).then( - result => { - expect(result.data.name).to.eq(state.data.name); - } - ); - }).timeout(20 * 1000); - }); - - describe('delete', () => { - let id = ''; - let state = delState; - before(() => { - nock.cleanAll(); - nock.enableNetConnect(); - }); - - it('should create a new tracked entity instance', () => { - return execute(create('trackedEntityInstances', state.data))(state).then( - result => { - id = result.data.response.importSummaries[0].reference; - expect(result.data.response.imported).to.eq(1); - expect(result.data.response.updated).to.eq(0); - expect(result.data.response.deleted).to.eq(0); - expect(result.data.response.ignored).to.eq(0); - } - ); - }).timeout(20 * 1000); - - it('should delete the newly created tracked entity instance', () => { - return execute(del('trackedEntityInstances', id))(state).then(result => { - expect(result.data.response.importCount.imported).to.eq(0); - expect(result.data.response.importCount.updated).to.eq(0); - expect(result.data.response.importCount.ignored).to.eq(0); - expect(result.data.response.importCount.deleted).to.eq(1); - }); - }).timeout(20 * 1000); - }); - - describe('getMetadata', () => { - before(() => { - nock.cleanAll(); - nock.enableNetConnect(); - }); - - it('should get a list of orgUnits', () => { - let state = getState; - return execute( - getMetadata('organisationUnits', { fields: ['id', 'name'] }) - )(state).then(result => { - expect(result.data.organisationUnits.length).to.be.gte(1); - }); - }).timeout(30 * 1000); - - it('should get data elements and indicators where name includes "ANC"', () => { - let state = getState; - return execute( - getMetadata(['dataElements', 'indicators'], { - filters: ['name:like:ANC'], - fields: ['id', 'name'], - }) - )(state).then(result => { - expect(result.data.dataElements.length).to.be.gte(1); - expect(result.data.indicators.length).to.be.gte(1); - }); - }).timeout(20 * 1000); - }); - - describe('getSchema', () => { - before(() => { - nock.cleanAll(); - nock.enableNetConnect(); - }); - - it('should get the schema for dataElement', () => { - let state = getState; - return execute(getSchema('dataElement'))(state).then(result => { - expect(result.data.name).to.eq('dataElement'); - }); - }).timeout(20 * 1000); - - it('should get the schema for dataElement, only returning the `properties` field', () => { - let state = getState; - return execute(getSchema('dataElement', { fields: 'properties' }))( - state - ).then(result => { - expect(result.data).to.have.a.key('properties'); - expect(Object.keys(result.data).length).to.eq(1); - }); - }).timeout(20 * 1000); - - it('should get the schema for dataElement in XML, returning all the fields', () => { - let state = getState; - return execute( - getSchema('dataElement', { fields: '*' }, { responseType: 'xml' }) - )(state).then(result => { - expect(result.data.slice(2, 5)).to.eq('xml'); - }); - }).timeout(20 * 1000); - }); - - describe('getResources', () => { - let state = getState; - before(() => { - nock.cleanAll(); - nock.enableNetConnect(); - }); - - it('should get a list of all DHIS2 resources', () => { - return execute(getResources())(state).then(result => { - expect(result.data.resources.length).to.be.gte(1); - }); - }).timeout(20 * 1000); - - it('should get a resource named `attribute`, in `json` format', () => { - return execute(getResources({ filter: 'singular:eq:attribute' }))( - state - ).then(result => { - expect(result.data.resources.length).to.be.eq(1); - expect(result.data.resources[0].singular).to.be.eq('attribute'); - }); - }).timeout(20 * 1000); - - it('should get a resource named `attribute`, in `xml` format, returning all the fields', () => { - return execute( - getResources('dataElement', { - filter: 'singular:eq:attribute', - fields: '*', - responseType: 'xml', - }) - )(state).then(result => { - expect(result.data.slice(2, 5)).to.be.eq('xml'); - }); - }).timeout(20 * 1000); - }); - - describe('getAnalytics', () => { - let state = getState; - before(() => { - nock.cleanAll(); - nock.enableNetConnect(); - }); - - it('should return a list of data elements filtered by the periods and organisation units', () => { - return execute( - getAnalytics({ - dimensions: ['dx:fbfJHSPpUQD;cYeuwXTCPkU'], - filters: ['pe:2014Q1;2014Q2', 'ou:O6uvpzGd5pu;lc3eMKXaEfw'], - }) - )(state).then(result => { - expect(result.data).to.be.not.null; - expect(result.data).to.haveOwnProperty('rows'); - }); - }).timeout(20 * 1000); - - it('should return only records where the data value is greater or equal to 6500 and less than 33000', () => { - return execute( - getAnalytics({ - dimensions: [ - 'dx:fbfJHSPpUQD;cYeuwXTCPkU', - 'pe:2014', - 'ou:O6uvpzGd5pu;lc3eMKXaEfw', - ], - measureCriteria: 'GE:6500;LT:33000', - }) - )(state).then(result => { - expect(result.data).to.be.not.null; - expect(result.data).to.haveOwnProperty('rows'); + it('should recursively expand references', async () => { + testServer + .post('/api/events', { + program: 'abc', + orgUnit: 'org50', + }) + .reply(200, { + httpStatus: 'OK', + message: 'the response', }); - }).timeout(20 * 1000); - it('should allow users to send a date range using startDate and endDate', () => { - return execute( - getAnalytics({ - dimensions: ['dx:fbfJHSPpUQD;cYeuwXTCPkU', 'ou:ImspTQPwCqd'], - startDate: '2018-01-01', - endDate: '2018-06-01', - }) - )(state).then(result => { - expect(result.data).to.be.not.null; - expect(result.data).to.haveOwnProperty('rows'); - }); - }).timeout(20 * 1000); - }); - - describe('discover', () => { - let state = getState; - it('should return a list of parameters allowed on a given endpoint for specific http method', () => { - return execute(discover('get', '/trackedEntityInstances'))(state).then( - result => { - expect(result.data.description).to.be.eq( - 'list tracked entity instances (TEIs)' - ); - } - ); - }).timeout(30 * 1000); + const response = await execute( + create('events', { program: 'abc', orgUnit: state => state.data.orgUnit }) + )(state); + expect(response.data).to.eql({ httpStatus: 'OK', message: 'the response' }); }); +}); - describe('generateDhis2UID', () => { - let state = getState; - it('should return one UID generated from DHIS2 server', () => { - return execute(generateDhis2UID())(state).then(result => { - expect(result.data.codes.length).to.be.eq(1); +describe('UPDATE', () => { + const state = { + configuration: { + username: 'admin', + password: 'district', + hostUrl: 'https://play.dhis2.org/2.36.4', + }, + data: { + program: 'program', + orgUnit: 'orgunit', + status: 'COMPLETED', + currentDate: '02-02-20', + }, + }; + + it('should make an authenticated PUT to the right url', async () => { + testServer + .put('/api/events/qAZJCrNJK8H') + .matchHeader('authorization', 'Basic YWRtaW46ZGlzdHJpY3Q=') + .reply(200, { + httpStatus: 'OK', + message: 'the response', + }); + + const response = await execute( + update('events', 'qAZJCrNJK8H', state => ({ + ...state.data, + date: state.data.currentDate, + })) + )(state); + expect(response.data).to.eql({ httpStatus: 'OK', message: 'the response' }); + }); + + it('should recursively expand refs', async () => { + testServer + .put('/api/events/qAZJCrNJK8H', { + program: 'program', + orgUnit: 'hardcoded', + date: '02-02-20', + }) + .reply(200, { + httpStatus: 'OK', + message: 'the response', }); - }).timeout(20 * 1000); - it('should return three UIDs generated from DHIS2 server', () => { - return execute(generateDhis2UID({ limit: 3 }))(state).then(result => { - expect(result.data.codes.length).to.be.eq(3); - }); - }).timeout(20 * 1000); + const response = await execute( + update('events', 'qAZJCrNJK8H', { + program: dataValue('program'), + orgUnit: 'hardcoded', + date: state => state.data.currentDate, + }) + )(state); + expect(response.data).to.eql({ httpStatus: 'OK', message: 'the response' }); }); +}); - describe('getDataValues', () => { - let state = getState; - it('should return two `data values` associated with a specific `orgUnit`, `dataSet`, and `period `', () => { - return execute( - getDataValues({ - orgUnit: 'DiszpKrYNg8', - period: '202010', - dataSet: 'pBOMPrpg1QX', - limit: 2, - }) - )(state).then(result => { - expect(result.data.orgUnit).to.be.eq('DiszpKrYNg8'); - expect(result.data.period).to.be.eq('202010'); - expect(result.data.dataSet).to.be.eq('pBOMPrpg1QX'); - expect(result.data.dataValues.length).to.be.eq(2); - }); - }).timeout(20 * 1000); - }); +describe('buildUrl', () => { + it('the proper URL gets built from the "entity" string and the config', async () => { + const state = { + configuration: { + username: 'admin', + password: 'district', + hostUrl: 'https://dhis2.moh.gov', + apiVersion: '2.36.4', + }, + }; - describe('createDataValues', () => { - it('should create large bulks of data values which are not logically related', () => { - let state = createBulkUnrelatedDataValues; - return execute(createDataValues(state.data))(state).then(result => { - expect( - (result.data.status === 'WARNING' && - result.data.importCount.ignored > 0) || - result.data.importCount.imported > 0 || - result.data.importCount.updated > 0 - ); - expect(result.data.importCount.deleted).to.eq(0); - }); - }).timeout(20 * 1000); + const url = buildUrl( + '/' + 'events', + state.configuration.hostUrl, + state.configuration.apiVersion + ); - it('should create large bulks of data values which are not logically related', () => { - let state = createRelatedDataValues; - return execute(createDataValues(state.data))(state).then(result => { - expect( - (result.data.status === 'WARNING' && - result.data.importCount.ignored > 0) || - result.data.importCount.imported > 0 || - result.data.importCount.updated > 0 - ); - expect(result.data.importCount.deleted).to.eq(0); - }); - }).timeout(20 * 1000); + expect(url).to.eql('https://dhis2.moh.gov/api/2.36.4/events'); }); +}); - describe('createEvents', () => { - before(() => { - nock.cleanAll(); - nock.enableNetConnect(); - }); +describe('nestArray', () => { + it('when an array is passed it gets nested inside that "entity" key', async () => { + const state = { + configuration: { + username: 'admin', + password: 'district', + hostUrl: 'https://play.dhis2.org/2.36.4', + apiVersion: '2.36.4', + }, + data: [{ a: 1 }], + }; - it.skip( - 'should create a new single event and link it to a given program', - () => { - let state = createEventsState; - return execute(createEvents(state.data))(state).then(result => { - expect(result.data.response.imported).to.eq(1); - expect(result.data.response.updated).to.eq(0); - expect(result.data.response.deleted).to.eq(0); - expect(result.data.response.ignored).to.eq(0); - }); - } - ).timeout(20 * 1000); + const body = nestArray(state.data, 'events'); - it.skip( - 'should create two new events and link them to respective programs', - () => { - let state = sendDataForMultipleEventsState; - return execute(createEvents(state.data))(state).then(result => { - expect(result.data.response.imported).to.eq(2); - expect(result.data.response.updated).to.eq(0); - expect(result.data.response.deleted).to.eq(0); - expect(result.data.response.ignored).to.eq(0); - }); - } - ).timeout(20 * 1000); + expect(body).to.eql({ events: [{ a: 1 }] }); }); - describe('enrollTEI', () => { - let trackedEntityInstance = ''; - - let state = enrollTEIState; - - before(() => { - nock.cleanAll(); - nock.enableNetConnect(); - }); - - it('should create a new tracked entity instance', () => { - return execute(create('trackedEntityInstances', state.data))(state).then( - result => { - trackedEntityInstance = - result.data.response.importSummaries[0].reference; - expect(result.data.response.imported).to.eq(1); - expect(result.data.response.updated).to.eq(0); - expect(result.data.response.deleted).to.eq(0); - expect(result.data.response.ignored).to.eq(0); - } - ); - }).timeout(20 * 1000); + it("when an object is passed it doesn't get nested", async () => { + const state = { + configuration: { + username: 'admin', + password: 'district', + hostUrl: 'https://play.dhis2.org/2.36.4', + }, + data: { b: 2 }, + }; - it('should enroll TEI into a given program', () => { - let date = new Date(); - state = { - ...state, - data: { - trackedEntityInstance: trackedEntityInstance, - orgUnit: 'ImspTQPwCqd', - program: 'WSGAb5XwJ3Y', - enrollmentDate: date, - incidentDate: date, - }, - }; + const body = nestArray(state.data, 'events'); - console.log('state', state); - return execute(enrollTEI(state.data))(state).then(result => { - expect(result.data.response.imported).to.eq(1); - expect(result.data.response.updated).to.eq(0); - expect(result.data.response.deleted).to.eq(0); - expect(result.data.response.ignored).to.eq(0); - }); - }).timeout(20 * 1000); + expect(body).to.eql({ b: 2 }); }); }); diff --git a/test/integration.js b/test/integration.js new file mode 100644 index 0000000..176bcff --- /dev/null +++ b/test/integration.js @@ -0,0 +1,484 @@ +const { expect } = require('chai'); +const { create, execute, get, update } = require('../src/Adaptor'); +const crypto = require('crypto'); +const { upsert } = require('../lib/Adaptor'); + +const getRandomOrganisationUnitPayload = user => { + const name = crypto.randomBytes(16).toString('hex'); + const shortName = name.substring(0, 5); + const displayName = name; + const openingDate = new Date().toISOString(); + return { name, shortName, displayName, openingDate, users: [user] }; +}; + +const getRandomProgramPayload = () => { + const name = crypto.randomBytes(16).toString('hex'); + const shortName = name.substring(0, 5); + const programType = 'WITHOUT_REGISTRATION'; + return { name, shortName, programType }; +}; + +const getRandomProgramStagePayload = program => { + const name = crypto.randomBytes(16).toString('hex'); + const displayName = name; + return { name, displayName, program }; +}; + +const globalState = { + configuration: { + username: 'admin', + password: 'district', + hostUrl: 'https://play.dhis2.org/2.36.4', + }, + program: 'IpHINAT79UW', + organisationUnit: 'DiszpKrYNg8', + dataSet: 'pBOMPrpg1QX', + trackedEntityInstance: 'bmshzEacgxa', + programStage: 'A03MvHHogjR', + dataElement: 'Ix2HsbDMLea', + enrollment: 'CmsHzercTBa', +}; + +describe('create', () => { + it('should create an event program', async () => { + const state = { + ...globalState, + data: { program: getRandomProgramPayload() }, + }; + + const response = await execute( + create('programs', state => state.data.program) + )(state); + expect({ + httpStatus: response.data.httpStatus, + httpStatusCode: response.data.httpStatusCode, + status: response.data.status, + }).to.eql({ + httpStatus: 'Created', + httpStatusCode: 201, + status: 'OK', + }); + }); + + it('should create a single event', async () => { + const state = { + ...globalState, + data: { + program: 'eBAyeGv0exc', + orgUnit: 'DiszpKrYNg8', + status: 'COMPLETED', + }, + }; + const response = await execute(create('events', state => state.data))( + state + ); + globalState.event = response.data.response.uid; + expect({ + httpStatus: response.data.httpStatus, + httpStatusCode: response.data.httpStatusCode, + status: response.data.status, + }).to.eql({ + httpStatus: 'OK', + httpStatusCode: 200, + status: 'OK', + }); + }); + + it('should create a single tracked entity instance', async () => { + const state = { + ...globalState, + data: { + orgUnit: globalState.organisationUnit, + trackedEntityType: 'nEenWmSyUEp', + attributes: [ + { + attribute: 'w75KJ2mc4zz', + value: 'Gigiwe', + }, + ], + }, + }; + const response = await execute( + create('trackedEntityInstances', state => state.data) + )(state); + globalState.trackedEntityInstance = + response.data.response.importSummaries[0].reference; + expect({ + httpStatus: response.data.httpStatus, + httpStatusCode: response.data.httpStatusCode, + status: response.data.status, + }).to.eql({ + httpStatus: 'OK', + httpStatusCode: 200, + status: 'OK', + }); + }); + + it('should create a single dataValueSet', async () => { + const state = { + ...globalState, + data: { + dataElement: 'f7n9E0hX8qk', + period: '201401', + orgUnit: globalState.organisationUnit, + value: '12', + }, + }; + + const response = await execute( + create('dataValueSets', state => state.data) + )(state); + expect({ status: response.data.status }).to.eql({ status: 'SUCCESS' }); + }); + + it('should create a set of related data values sharing the same period and organisation unit', async () => { + const state = { + ...globalState, + data: { + dataSet: globalState.dataSet, + completeDate: '2014-02-03', + period: '201401', + orgUnit: globalState.organisationUnit, + dataValues: [ + { + dataElement: 'f7n9E0hX8qk', + value: '1', + }, + { + dataElement: 'Ix2HsbDMLea', + value: '2', + }, + { + dataElement: 'eY5ehpbEsB7', + value: '3', + }, + ], + }, + }; + + const response = await execute( + create('dataValueSets', state => state.data) + )(state); + expect({ status: response.data.status }).to.eql({ status: 'SUCCESS' }); + }); + + // it('should create a single enrollment of a trackedEntityInstance into a given program', async () => { + // const state = { + // ...globalState, + // data: { + // trackedEntityInstance: globalState.trackedEntityInstance, + // orgUnit: globalState.organisationUnit, + // program: globalState.program, + // enrollmentDate: new Date().toISOString().split('T')[0], + // incidentDate: new Date().toISOString().split('T')[0], + // }, + // }; + + // const response = await execute(create('enrollments', state => state.data))( + // state + // ); + // expect({ status: response.data.status }).to.eql({ status: 'SUCCESS' }); + // }); +}); + +describe('update', () => { + it('should update an event program', async () => { + const state = { + ...globalState, + data: { program: getRandomProgramPayload() }, + }; + + const response = await execute( + update( + 'programs', + state => state.program, + state => state.data.program + ) + )(state); + expect({ + httpStatus: response.data.httpStatus, + httpStatusCode: response.data.httpStatusCode, + status: response.data.status, + }).to.eql({ + httpStatus: 'OK', + httpStatusCode: 200, + status: 'OK', + }); + }); + + it('should update a single event', async () => { + const state = { + ...globalState, + event: 'OZ3mVgaIAqw', + data: { + program: 'eBAyeGv0exc', + orgUnit: 'DiszpKrYNg8', + status: 'COMPLETED', + }, + }; + const response = await execute( + update( + 'events', + state => state.event, + state => state.data + ) + )(state); + expect({ + httpStatus: response.data.httpStatus, + httpStatusCode: response.data.httpStatusCode, + status: response.data.status, + }).to.eql({ + httpStatus: 'OK', + httpStatusCode: 200, + status: 'OK', + }); + }); + + it('should update a single tracked entity instance', async () => { + const state = { + ...globalState, + data: { + orgUnit: globalState.organisationUnit, + trackedEntityType: 'nEenWmSyUEp', + attributes: [ + { + attribute: 'w75KJ2mc4zz', + value: 'Gigiwe', + }, + ], + }, + }; + console.log(state); + const response = await execute( + update( + 'trackedEntityInstances', + state => state.trackedEntityInstance, + state => state.data + ) + )(state); + expect({ + httpStatus: response.data.httpStatus, + httpStatusCode: response.data.httpStatusCode, + status: response.data.status, + }).to.eql({ + httpStatus: 'OK', + httpStatusCode: 200, + status: 'OK', + }); + }); + + it('should update a single dataValueSet', async () => { + const state = { + ...globalState, + data: { + dataElement: 'f7n9E0hX8qk', + period: '201401', + orgUnit: globalState.organisationUnit, + value: '12', + }, + }; + const response = await execute( + update( + 'dataValueSets', + state => state.dataSet, + state => state.data + ) + )(state); + expect({ status: response.data.status }).to.eql({ status: 'SUCCESS' }); + }); + + it('should update a set of related data values sharing the same period and organisation unit', async () => { + const state = { + ...globalState, + data: { + dataSet: globalState.dataSet, + completeDate: '2014-02-03', + period: '201401', + orgUnit: globalState.organisationUnit, + dataValues: [ + { + dataElement: 'f7n9E0hX8qk', + value: '1', + }, + { + dataElement: 'Ix2HsbDMLea', + value: '2', + }, + { + dataElement: 'eY5ehpbEsB7', + value: '3', + }, + ], + }, + }; + + const response = await execute( + update( + 'dataValueSets', + state => state.dataSet, + state => state.data + ) + )(state); + expect({ status: response.data.status }).to.eql({ status: 'SUCCESS' }); + }); +}); + +describe('get', () => { + const state = { + configuration: { + username: 'admin', + password: 'district', + hostUrl: 'https://play.dhis2.org/2.36.4', + }, + data: {}, + }; + + it('should get trackedEntityInstances matching the URL parameters specified', async () => { + const response = await execute( + get('trackedEntityInstances', { + params: { + fields: '*', + ou: 'DiszpKrYNg8', + entityType: 'nEenWmSyUEp', + trackedEntityInstance: 'dNpxRu1mWG5', + }, + }) + )(state); + expect(response.data.trackedEntityInstances.length).to.gte(1); + }); + + it('should get all programs in the organisation unit TSyzvBiovKh', async () => { + const response = await execute( + get('programs', { + params: { orgUnit: 'TSyzvBiovKh', fields: '*' }, + }) + )(state); + expect(response.data.programs.length).to.gte(1); + }); +}); + +describe('upsert', () => { + const state = { + configuration: { + username: 'admin', + password: 'district', + hostUrl: 'https://play.dhis2.org/2.36.4', + }, + data: {}, + }; + + it('should upsert a trackedEntityInstance matching the URL parameters', async () => { + const response = await execute( + upsert( + 'trackedEntityInstances', + { + created: '2019-08-21T13:27:51.119', + orgUnit: 'DiszpKrYNg8', + createdAtClient: '2019-03-19T01:11:03.924', + trackedEntityInstance: 'dNpxRu1mWG5', + lastUpdated: '2019-09-27T00:02:11.604', + trackedEntityType: 'We9I19a3vO1', + lastUpdatedAtClient: '2019-03-19T01:11:03.924', + coordinates: + '[[[-11.8049,8.3374],[-11.8032,8.3436],[-11.8076,8.3441],[-11.8096,8.3387],[-11.8049,8.3374]]]', + inactive: false, + deleted: false, + featureType: 'POLYGON', + geometry: { + type: 'Polygon', + coordinates: [ + [ + [-11.8049, 8.3374], + [-11.8032, 8.3436], + [-11.8076, 8.3441], + [-11.8096, 8.3387], + [-11.8049, 8.3374], + ], + ], + }, + programOwners: [ + { + ownerOrgUnit: 'DiszpKrYNg8', + program: 'M3xtLkYBlKI', + trackedEntityInstance: 'dNpxRu1mWG5', + }, + ], + enrollments: [], + relationships: [ + { + lastUpdated: '2019-08-21T00:00:00.000', + created: '2019-08-21T00:00:00.000', + relationshipName: 'Focus to Case', + bidirectional: false, + relationshipType: 'Mv8R4MPcNcX', + relationship: 'EDfZpCLcEVN', + from: { + trackedEntityInstance: { + trackedEntityInstance: 'dNpxRu1mWG5', + programOwners: [], + }, + }, + to: { + trackedEntityInstance: { + trackedEntityInstance: 'Fbru4rg4dYV', + programOwners: [], + }, + }, + }, + { + lastUpdated: '2019-08-21T00:00:00.000', + created: '2019-08-21T00:00:00.000', + relationshipName: 'Focus to Case', + bidirectional: false, + relationshipType: 'Mv8R4MPcNcX', + relationship: 'z4ItJx8ul3Z', + from: { + trackedEntityInstance: { + trackedEntityInstance: 'dNpxRu1mWG5', + programOwners: [], + }, + }, + to: { + trackedEntityInstance: { + trackedEntityInstance: 'RHA9RWNvAnC', + programOwners: [], + }, + }, + }, + { + lastUpdated: '2019-08-21T00:00:00.000', + created: '2019-08-21T00:00:00.000', + relationshipName: 'Focus to Case', + bidirectional: false, + relationshipType: 'Mv8R4MPcNcX', + relationship: 'XIfv95ZiM4H', + from: { + trackedEntityInstance: { + trackedEntityInstance: 'dNpxRu1mWG5', + programOwners: [], + }, + }, + to: { + trackedEntityInstance: { + trackedEntityInstance: 'jZRaFaYkAtE', + programOwners: [], + }, + }, + }, + ], + attributes: [], + }, + { + params: { + fields: '*', + ou: 'DiszpKrYNg8', + entityType: 'nEenWmSyUEp', + trackedEntityInstance: 'dNpxRu1mWG5', + }, + } + ) + )(state); + expect(response.data.httpStatusCode).to.eq(200); + expect(response.data.httpStatus).to.eq('OK'); + }); +}); From 9174a484036ea650bf9b098bcf6ce91ea632d1c7 Mon Sep 17 00:00:00 2001 From: Taylor Downs Date: Wed, 8 Dec 2021 20:21:47 +0000 Subject: [PATCH 02/26] update README from earlier branch --- .devcontainer/devcontainer.json | 2 +- README.md | 42 +++++++++++++++++++++++++++++---- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 5eeb7a4..c0a203c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -9,7 +9,7 @@ }, "containerEnv": { - "GH_TOKEN": "${localEnv:GH_TOKEN}", + "GH_TOKEN": "${localEnv:GH_TOKEN}" }, "mounts": [ diff --git a/README.md b/README.md index 51c0819..fb51e04 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Language DHIS2 [![Build Status](https://travis-ci.org/OpenFn/language-dhis2.svg?branch=main)](https://travis-ci.org/OpenFn/language-dhis2) -Language Pack for building expressions and operations for working with -the [DHIS2 API](http://dhis2.github.io/dhis2-docs/master/en/developer/html/dhis2_developer_manual.html). +Language Pack for building expressions and operations for working with the +[DHIS2 API](http://dhis2.github.io/dhis2-docs/master/en/developer/html/dhis2_developer_manual.html). ## Documentation @@ -38,7 +38,7 @@ fetchAnalytics({ outputIdScheme: 'UID', }, }); -```` +``` ## Tracked Entity API @@ -174,4 +174,38 @@ Clone the repo, run `npm install`. Run tests using `npm run test` or `npm run test:watch` -Build the project using `make`. +NB: There are two types of tests: unit tests and integration tests. + +### Unit Tests + +Unit tests allows to test the functionalities of the adaptor helper functions +such as: + +> Does `create('events', payload)` perform a post request to the correct DHIS2 +> API? + +To run unit tests execute `npm run test` (they're the default tests). + +Anytime a new functionality is added to the helper functions, more unit tests +needs to be added. + +### End-to-end integration tests + +Integration tests allow to test the end-to-end behavior of the helper functions +and also to test the examples (snippet of code) we provide in the documentation. +For example with integration tests we answer the following question: + +> Does `create('events', eventPayload)` actually create a new event in a live +> DHIS2 system? + +To run integration tests, execute `npm run integration-test`. These +tests use network I/O and a public connection to a DHIS2 "play" server so their +timing and performance is unpredictable. Consider adding an increased timeout, +and modifying the orgUnit, program, etc., IDs set in `globalState`. + +Anytime a new example is added in the documentation of a helper function, a new +integration test should be done. + +### Build + +Build the project using `npm run build`. From e554c0783466712097bf1ed5642baa5f7d8b239c Mon Sep 17 00:00:00 2001 From: Taylor Downs Date: Wed, 8 Dec 2021 20:27:04 +0000 Subject: [PATCH 03/26] small language changes to readme --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fb51e04..6f107b9 100644 --- a/README.md +++ b/README.md @@ -191,8 +191,9 @@ needs to be added. ### End-to-end integration tests -Integration tests allow to test the end-to-end behavior of the helper functions -and also to test the examples (snippet of code) we provide in the documentation. +Integration tests allow us to test the end-to-end behavior of the helper functions +and also to test the examples we provide via inline documentation. + For example with integration tests we answer the following question: > Does `create('events', eventPayload)` actually create a new event in a live From 8c9a7585e3ec8184d8a7158c09ac9d616c18ad35 Mon Sep 17 00:00:00 2001 From: Taylor Downs Date: Sun, 12 Dec 2021 09:51:53 +0000 Subject: [PATCH 04/26] don't console log the whole error to not expose auth --- lib/Adaptor.js | 2 -- package-lock.json | 14 +++++++------- package.json | 2 +- src/Adaptor.js | 1 - 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/lib/Adaptor.js b/lib/Adaptor.js index f668843..d66651a 100644 --- a/lib/Adaptor.js +++ b/lib/Adaptor.js @@ -181,8 +181,6 @@ _axios.default.interceptors.response.use(function (response) { return response; }, function (error) { - console.log(error); - _Utils.Log.error(`${error === null || error === void 0 ? void 0 : error.message}`); return Promise.reject(error); diff --git a/package-lock.json b/package-lock.json index f4213d0..660c548 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1481,11 +1481,11 @@ "optional": true }, "axios": { - "version": "0.21.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.2.tgz", - "integrity": "sha512-87otirqUw3e8CzHTMO+/9kh/FSgXt/eVDvipijwDtEuwbkySWZ9SBm6VEubmJ/kLKEoLQV/POhxXFb66bfekfg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz", + "integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==", "requires": { - "follow-redirects": "^1.14.0" + "follow-redirects": "^1.14.4" } }, "babel-plugin-dynamic-import-node": { @@ -2414,9 +2414,9 @@ } }, "follow-redirects": { - "version": "1.14.4", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz", - "integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==" + "version": "1.14.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.6.tgz", + "integrity": "sha512-fhUl5EwSJbbl8AR+uYL2KQDxLkdSjZGR36xy46AO7cOMTrCMON6Sa28FmAnC2tRTDbd/Uuzz3aJBv7EBN7JH8A==" }, "for-in": { "version": "1.0.2", diff --git a/package.json b/package.json index b829d06..31671a8 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ ], "dependencies": { "@openfn/language-common": "1.4.1", - "axios": "^0.21.1", + "axios": "^0.24.0", "lodash": "^4.17.19" }, "devDependencies": { diff --git a/src/Adaptor.js b/src/Adaptor.js index 4922cf4..8fbf33a 100644 --- a/src/Adaptor.js +++ b/src/Adaptor.js @@ -106,7 +106,6 @@ axios.interceptors.response.use( return response; }, function (error) { - console.log(error); Log.error(`${error?.message}`); return Promise.reject(error); } From d8c4bc33813d64818edf71a14a20df39f6c4c68a Mon Sep 17 00:00:00 2001 From: Taylor Downs Date: Sun, 12 Dec 2021 09:54:02 +0000 Subject: [PATCH 05/26] Remove console.log(state) from integration tests --- test/integration.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/integration.js b/test/integration.js index 176bcff..9ff4e48 100644 --- a/test/integration.js +++ b/test/integration.js @@ -248,7 +248,7 @@ describe('update', () => { ], }, }; - console.log(state); + const response = await execute( update( 'trackedEntityInstances', @@ -256,6 +256,7 @@ describe('update', () => { state => state.data ) )(state); + expect({ httpStatus: response.data.httpStatus, httpStatusCode: response.data.httpStatusCode, From 531419e934ee8018494fdd0e7f1a1bf3f2bbc3e5 Mon Sep 17 00:00:00 2001 From: Taylor Downs Date: Fri, 17 Dec 2021 17:07:19 -0500 Subject: [PATCH 06/26] new pattern --- lib/Adaptor.js | 178 +++++++++++-------------- lib/Client.js | 39 +++--- lib/Utils.js | 356 ++++++++++++++++--------------------------------- src/Adaptor.js | 223 ++++++++++++++----------------- src/Client.js | 18 ++- src/Utils.js | 345 ++++++++++++++++------------------------------- test/index.js | 5 +- 7 files changed, 436 insertions(+), 728 deletions(-) diff --git a/lib/Adaptor.js b/lib/Adaptor.js index d66651a..fb93c16 100644 --- a/lib/Adaptor.js +++ b/lib/Adaptor.js @@ -12,6 +12,7 @@ exports.discover = discover; exports.patch = patch; exports.del = del; exports.attrVal = attrVal; +exports.attribute = attribute; Object.defineProperty(exports, "field", { enumerable: true, get: function () { @@ -78,12 +79,6 @@ Object.defineProperty(exports, "http", { return _languageCommon.http; } }); -Object.defineProperty(exports, "attribute", { - enumerable: true, - get: function () { - return _Utils.attribute; - } -}); var _axios = _interopRequireDefault(require("axios")); @@ -147,7 +142,9 @@ function configMigrationHelper(state) { } return state; -} +} // NOTE: In order to prevent unintended exposure of authentication information +// in the logs, we make use of an axios interceptor. + _axios.default.interceptors.response.use(function (response) { var _response$headers$con, _response; @@ -159,7 +156,7 @@ _axios.default.interceptors.response.use(function (response) { if ((0, _lodash.indexOf)(acceptHeaders, contentType) === -1) { const newError = { status: 404, - message: 'Unexpected content,returned', + message: 'Unexpected content returned', responseData: response.data }; @@ -175,15 +172,29 @@ _axios.default.interceptors.response.use(function (response) { data: JSON.parse(response.data) }; } catch (error) { - /* Keep quiet */ + _Utils.Log.warn('Non-JSON response detected, unable to parse.'); } } return response; }, function (error) { - _Utils.Log.error(`${error === null || error === void 0 ? void 0 : error.message}`); + try { + var _details$config, _details$config2; + + const details = error.toJSON(); + if (details === null || details === void 0 ? void 0 : (_details$config = details.config) === null || _details$config === void 0 ? void 0 : _details$config.auth) details.config.auth = '--REDACTED--'; + if (details === null || details === void 0 ? void 0 : (_details$config2 = details.config) === null || _details$config2 === void 0 ? void 0 : _details$config2.data) details.config.data = '--REDACTED--'; + + _Utils.Log.error(details.message); - return Promise.reject(error); + return Promise.reject(details); + } catch (e) { + // TODO: @Elias, why does this error sometimes already appear to be JSONified? + // console.log(e) // "not JSONABLE TypeError: error.toJSON is not a function" + _Utils.Log.error(error.message); + + return Promise.reject(error); + } }); /** * Create a record @@ -299,36 +310,22 @@ _axios.default.interceptors.response.use(function (response) { function create(resourceType, data, options, callback) { - const initialParams = { - resourceType, - data, - options, - callback - }; return state => { + console.log(`Preparing create operation...`); + resourceType = (0, _languageCommon.expandReferences)(resourceType)(state); + data = (0, _languageCommon.expandReferences)(data)(state); + options = (0, _languageCommon.expandReferences)(options)(state); const { - url, - data, - resourceType, - auth, - urlParams, - callback - } = (0, _Utils.expandExtractAndLog)('create', initialParams)(state); - return (0, _Client.request)({ + configuration + } = state; + return (0, _Client.request)(configuration, { method: 'post', - url, - data: (0, _Utils.nestArray)(data, resourceType), - options: { - auth, - params: urlParams - } + url: (0, _Utils.generateUrl)(configuration, options, resourceType), + data: (0, _Utils.nestArray)(data, resourceType) }).then(result => { - _Utils.Log.info(`\nOperation succeeded. Created ${resourceType}: ${result.headers.location}.\n`); + _Utils.Log.success(`Created ${resourceType}: ${result.headers.location}`); - if (callback) return callback((0, _languageCommon.composeNextState)(state, result.data)); - return (0, _languageCommon.composeNextState)(state, result.data); - }).catch(error => { - throw error; + return (0, _Utils.handleResponse)(result, state, callback); }); }; } @@ -478,36 +475,24 @@ function create(resourceType, data, options, callback) { function update(resourceType, path, data, options, callback) { - const initialParams = { - resourceType, - path, - data, - options, - callback - }; return state => { + console.log(`Preparing update operation...`); + resourceType = (0, _languageCommon.expandReferences)(resourceType)(state); + path = (0, _languageCommon.expandReferences)(path)(state); + data = (0, _languageCommon.expandReferences)(data)(state); + options = (0, _languageCommon.expandReferences)(options)(state); const { - url, - data, - resourceType, - auth, - _, - callback - } = (0, _Utils.expandExtractAndLog)('update', initialParams)(state); - return (0, _Client.request)({ + configuration + } = state; + return (0, _Client.request)(configuration, { method: 'put', - url, - data, - options: { - auth - } + url: `${(0, _Utils.generateUrl)(configuration, options, resourceType)}/${path}`, + // TODO: @Elias, why no "nestArray" here? + data }).then(result => { - _Utils.Log.info(`\nOperation succeeded. Updated ${resourceType} ${path}.\n`); + _Utils.Log.success(`Updated ${resourceType} at ${path}`); - if (callback) return callback((0, _languageCommon.composeNextState)(state, result.data)); - return (0, _languageCommon.composeNextState)(state, result.data); - }).catch(error => { - throw error; + return (0, _Utils.handleResponse)(result, state, callback); }); }; } @@ -528,38 +513,27 @@ function update(resourceType, path, data, options, callback) { * trackedEntityInstance: 'dNpxRu1mWG5', * }); */ +// TODO: @Elias, I'm not sure "options" is the right name for the second arg here. +// Isn't this more like the filters that you're using to find the right resources? function get(resourceType, options, callback) { - const initialParams = { - resourceType, - options, - callback - }; return state => { + console.log(`Preparing get operation...`); + resourceType = (0, _languageCommon.expandReferences)(resourceType)(state); + options = (0, _languageCommon.expandReferences)(options)(state); const { - url: url, - _data, - _resourceType, - auth, - urlParams, - callback - } = (0, _Utils.expandExtractAndLog)('get', initialParams)(state); - return (0, _Client.request)({ + configuration + } = state; + return (0, _Client.request)(configuration, { method: 'get', - url, - options: { - auth, - params: urlParams, - responseType: 'json' - } + url: (0, _Utils.generateUrl)(configuration, options, resourceType), + params: (0, _Utils.buildUrlParams)(options), + responseType: 'json' }).then(result => { - _Utils.Log.info(`\nOperation succeeded. Retrieved ${result.data[resourceType].length} ${resourceType}.\n`); + _Utils.Log.success(`Retrieved ${result.data[resourceType].length} ${resourceType}.`); - if (callback) return callback((0, _languageCommon.composeNextState)(state, result.data)); - return (0, _languageCommon.composeNextState)(state, result.data); - }).catch(error => { - throw error; + return (0, _Utils.handleResponse)(result, state, callback); }); }; } @@ -594,8 +568,9 @@ function get(resourceType, options, callback) { function upsert(resourceType, data, options, callback) { return state => { - return get(resourceType, options)(state).then(res => { - const resources = res.data[resourceType]; + console.log(`Preparing upsert via 'get' then 'create' OR 'update'...`); + return get(resourceType, options)(state).then(resp => { + const resources = resp.data[resourceType]; if (resources.length > 1) { throw new RangeError(`Cannot upsert on Non-unique attribute. The operation found more than one records for your request.`); @@ -606,8 +581,6 @@ function upsert(resourceType, data, options, callback) { const path = resources[0][pathName]; return update(resourceType, path, data, options, callback)(state); } - }).catch(err => { - throw err; }); }; } @@ -625,8 +598,7 @@ function upsert(resourceType, data, options, callback) { function discover(httpMethod, endpoint) { return state => { - _Utils.Log.info(`Discovering query/import parameters for ${httpMethod} on ${endpoint}`); - + console.log(`Discovering query/import parameters for ${httpMethod} on ${endpoint}`); return _axios.default.get('https://dhis2.github.io/dhis2-api-specification/spec/metadata_openapi.json', { transformResponse: [data => { let tempData = JSON.parse(data); @@ -659,8 +631,7 @@ function discover(httpMethod, endpoint) { }).then(result => { var _result$data$descript; - _Utils.Log.info(`\t=======================================================================================\n\tQuery Parameters for ${httpMethod} on ${endpoint} [${(_result$data$descript = result.data.description) !== null && _result$data$descript !== void 0 ? _result$data$descript : ''}]\n\t=======================================================================================`); - + console.log(`\t=======================================================================================\n\tQuery Parameters for ${httpMethod} on ${endpoint} [${(_result$data$descript = result.data.description) !== null && _result$data$descript !== void 0 ? _result$data$descript : ''}]\n\t=======================================================================================`); console.table(result.data.parameters, ['in', 'required', 'description']); console.table(result.data.parameters, ['schema']); console.log(`=========================================Responses===============================\n${(0, _Utils.prettyJson)(result.data.responses)}\n=======================================================================================`); @@ -689,6 +660,7 @@ function discover(httpMethod, endpoint) { * name: 'New Name', * }); */ +// TODO: @Elias, can this be deleted in favor of update? How does DHIS2 handle PATCH vs PUT? function patch(resourceType, path, data, params, options, callback) { @@ -717,10 +689,6 @@ function patch(resourceType, path, data, params, options, callback) { const headers = { Accept: (_CONTENT_TYPES$respon = _Utils.CONTENT_TYPES[responseType]) !== null && _CONTENT_TYPES$respon !== void 0 ? _CONTENT_TYPES$respon : 'application/json' }; - (0, _Utils.logOperation)(operationName); - (0, _Utils.logApiVersion)(apiVersion); - (0, _Utils.logWaitingForServer)(url, queryParams); - (0, _Utils.warnExpectLargeResult)(resourceType, url); return _axios.default.request({ method: 'PATCH', url, @@ -737,7 +705,7 @@ function patch(resourceType, path, data, params, options, callback) { statusText: result.statusText }; - _Utils.Log.info(`${operationName} succeeded. Updated ${resourceType}.\nSummary:\n${(0, _Utils.prettyJson)(resultObject)}`); + _Utils.Log.success(`${operationName} succeeded. Updated ${resourceType}.\nSummary:\n${(0, _Utils.prettyJson)(resultObject)}`); if (callback) return callback((0, _languageCommon.composeNextState)(state, resultObject)); return (0, _languageCommon.composeNextState)(state, resultObject); @@ -758,6 +726,7 @@ function patch(resourceType, path, data, params, options, callback) { * @example Example`deleting` a `tracked entity instance` * del('trackedEntityInstances', 'LcRd6Nyaq7T'); */ +// TODO: @Elias, can this be implemented using the same pattern as update but without data? function del(resourceType, path, data, params, options, callback) { @@ -786,10 +755,6 @@ function del(resourceType, path, data, params, options, callback) { Accept: (_CONTENT_TYPES$respon2 = _Utils.CONTENT_TYPES[responseType]) !== null && _CONTENT_TYPES$respon2 !== void 0 ? _CONTENT_TYPES$respon2 : 'application/json' }; const url = (0, _Utils.buildUrl)('/' + resourceType + '/' + path, hostUrl, apiVersion); - (0, _Utils.logOperation)(operationName); - (0, _Utils.logApiVersion)(apiVersion); - (0, _Utils.logWaitingForServer)(url, queryParams); - (0, _Utils.warnExpectLargeResult)(resourceType, url); return _axios.default.request({ method: 'DELETE', url, @@ -801,7 +766,7 @@ function del(resourceType, path, data, params, options, callback) { data: body, headers }).then(result => { - _Utils.Log.info(`${operationName} succeeded. DELETED ${resourceType}.\nSummary:\n${(0, _Utils.prettyJson)(result.data)}`); + _Utils.Log.success(`${operationName} succeeded. DELETED ${resourceType}.\nSummary:\n${(0, _Utils.prettyJson)(result.data)}`); if (callback) return callback((0, _languageCommon.composeNextState)(state, result.data)); return (0, _languageCommon.composeNextState)(state, result.data); @@ -824,4 +789,11 @@ function attrVal(tei, attributeName) { var _tei$attributes, _tei$attributes$find; return tei === null || tei === void 0 ? void 0 : (_tei$attributes = tei.attributes) === null || _tei$attributes === void 0 ? void 0 : (_tei$attributes$find = _tei$attributes.find(a => (a === null || a === void 0 ? void 0 : a.displayName.toLowerCase()) == attributeName.toLowerCase())) === null || _tei$attributes$find === void 0 ? void 0 : _tei$attributes$find.value; +} + +function attribute(attributeId, attributeValue) { + return { + attribute: attributeId, + value: attributeValue + }; } \ No newline at end of file diff --git a/lib/Client.js b/lib/Client.js index db468be..543f0dd 100644 --- a/lib/Client.js +++ b/lib/Client.js @@ -10,26 +10,23 @@ var _axios = _interopRequireDefault(require("axios")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function request({ - method, - url, - data, - options -}) { - let headers = { - 'Content-Type': 'application/json' - }; - let req = { + username, + password +}, axiosRequest) { + const { method, - url, - headers, - ...options - }; - - if (method !== 'get') { - req = { ...req, - data - }; - } - - return _axios.default.request(req); + url + } = axiosRequest; + console.log(`Sending ${method} request to ${url}`); + return _axios.default.request({ + headers: { + 'Content-Type': 'application/json' + }, + auth: { + username, + password + }, + // Note that providing headers or auth in the request object will overwrite. + ...axiosRequest + }); } \ No newline at end of file diff --git a/lib/Utils.js b/lib/Utils.js index a5bbb06..30fbf44 100644 --- a/lib/Utils.js +++ b/lib/Utils.js @@ -3,197 +3,126 @@ Object.defineProperty(exports, "__esModule", { value: true }); -exports.composeSuccessMessage = composeSuccessMessage; -exports.warnExpectLargeResult = warnExpectLargeResult; -exports.logWaitingForServer = logWaitingForServer; -exports.logApiVersion = logApiVersion; -exports.logOperation = logOperation; exports.buildUrl = buildUrl; -exports.attribute = attribute; -exports.requestHttpHead = requestHttpHead; -exports.validateMetadataPayload = validateMetadataPayload; exports.handleResponse = handleResponse; exports.prettyJson = prettyJson; -exports.getIndicesOf = getIndicesOf; -exports.isLike = isLike; -exports.applyFilter = applyFilter; -exports.parseFilter = parseFilter; -exports.expandAndSetOperation = expandAndSetOperation; exports.nestArray = nestArray; -exports.expandExtractAndLog = expandExtractAndLog; -exports.CONTENT_TYPES = exports.dhis2OperatorMap = exports.Log = void 0; - -var _lodash = require("lodash"); +exports.generateUrl = generateUrl; +exports.buildUrlParams = buildUrlParams; +exports.Log = exports.CONTENT_TYPES = void 0; var _axios = _interopRequireDefault(require("axios")); +var _lodash = require("lodash"); + var _languageCommon = require("@openfn/language-common"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } -function composeSuccessMessage(operation) { - return `${operation} succeeded. The body of this result will be available in state.data or in your callback.`; -} - -function warnExpectLargeResult(paramOrResourceType, endpointUrl) { - if (!paramOrResourceType) Log.warn(` Missing params or resourceType. This may take a while. This endpoint(${endpointUrl}) may return a large collection of records, since 'params' or 'resourceType' is not specified. We recommend you specify 'params' or 'resourceType' or use 'filter' parameter to limit the content of the result.`); -} - -function logWaitingForServer(url, params) { - if (params) { - console.info('Request params: ', typeof params === 'object' && !(params instanceof URLSearchParams) ? prettyJson(params) : params); - } - - console.info(`Waiting for response from ${url}`); -} - -function logApiVersion(apiVersion) { - const message = apiVersion && apiVersion ? `Using DHIS2 api version ${apiVersion}` : ' Attempting to use apiVersion without providing it in state.configuration or in options parameter. You may encounter errors. api_version_missing.'; - if (apiVersion) console.warn(message);else console.warn(`Using latest version of DHIS2 api.`); -} - -function logOperation(operation) { - console.info(`Executing ${operation} ...`); -} - -function buildUrl(path, hostUrl, apiVersion) { - const pathSuffix = apiVersion ? `/${apiVersion}${path}` : `${path}`; - const url = hostUrl + '/api' + pathSuffix; - return url; -} - -function attribute(attributeId, attributeValue) { - return { - attribute: attributeId, - value: attributeValue - }; -} - -function requestHttpHead(endpointUrl, { - username, - password -}) { - return _axios.default.request({ - method: 'HEAD', - url: endpointUrl, - auth: { - username, - password - } - }).then(result => result.headers['content-length']); -} - -function validateMetadataPayload(payload, resourceType) { - return _axios.default.request({ - method: 'POST', - url: `https://play.dhis2.org/dev/api/schemas/${resourceType}`, - auth: { - username: 'admin', - password: 'distict' - }, - data: payload - }).then(result => result.data); -} - -function handleResponse(result, state, callback) { - if (callback) return callback(composeNextState(state, result)); - return composeNextState(state, result); -} - -function prettyJson(data) { - return JSON.stringify(data, null, 2); -} - -function getIndicesOf(string, regex) { - var match, - indexes = {}; - regex = new RegExp(regex); - - while (match = regex.exec(string)) { - let schemaRef; - - if (!indexes[match[0]]) { - indexes[match[0]] = {}; - } - - let hrefString = string.slice(match.index, (0, _lodash.indexOf)(string, '}', match.index) - 1); - let lastIndex = (0, _lodash.lastIndexOf)(hrefString, '/') + 1; - schemaRef = (0, _lodash.trim)(hrefString.slice(lastIndex)); - indexes[match[0]][match.index] = schemaRef; - } - - return indexes; -} +const CONTENT_TYPES = { + xml: 'application/xml', + json: 'application/json', + pdf: 'application/pdf', + csv: 'application/csv', + xls: 'application/vnd.ms-excel' +}; +exports.CONTENT_TYPES = CONTENT_TYPES; class Log { - static info(message) { - return console.info('(info)', new Date(), `\n${message}`); + static success(message) { + return console.info(`✓ ${message} @ ${new Date()}`); } static warn(message) { - return console.warn('⚠ WARNING', new Date(), `\n${message}`); + return console.warn(`⚠ Warning: ${message} @ ${new Date()}`); } static error(message) { - return console.error('✗ ERROR', new Date(), `\n${message}`); + return console.error(`✗ Error: ${message} @ ${new Date()}`); } } exports.Log = Log; -function isLike(string, words) { - var _words$match; - - const wordsArrary = words === null || words === void 0 ? void 0 : (_words$match = words.match(/([^\W]+[^\s,]*)/)) === null || _words$match === void 0 ? void 0 : _words$match.splice(0, 1); - - const isFound = word => { - var _RegExp; - - return (_RegExp = RegExp(word, 'i')) === null || _RegExp === void 0 ? void 0 : _RegExp.test(string); - }; - - return (0, _lodash.some)(wordsArrary, isFound); +function buildUrl(path, hostUrl, apiVersion) { + const pathSuffix = apiVersion ? `/${apiVersion}${path}` : `${path}`; + return hostUrl + '/api' + pathSuffix; } -const dhis2OperatorMap = { - eq: _lodash.eq, - like: isLike -}; -exports.dhis2OperatorMap = dhis2OperatorMap; - -function applyFilter(arrObject, targetProperty, operator, valueToCompareWith) { - if (targetProperty && operator && valueToCompareWith) { - try { - return (0, _lodash.filter)(arrObject, obj => Reflect.apply(operator, obj, [obj[targetProperty], valueToCompareWith])); - } catch (error) { - Log.warn(`Returned unfiltered data. Failed to apply custom filter(${prettyJson({ - targetProperty: targetProperty !== null && targetProperty !== void 0 ? targetProperty : null, - operator: operator !== null && operator !== void 0 ? operator : null, - value: valueToCompareWith !== null && valueToCompareWith !== void 0 ? valueToCompareWith : null - })}) on this collection. The operator you supplied maybe unsupported on this resource at the moment.`); - return arrObject; - } - } - - Log.info(`No filters applied, returned all records on this resource.`); - return arrObject; +function handleResponse(result, state, callback) { + // TODO: @Elias, should composeNextState get passed result OR result.data? + if (callback) return callback((0, _languageCommon.composeNextState)(state, result.data)); + return (0, _languageCommon.composeNextState)(state, result.data); } -function parseFilter(filterExpression) { - var _filterTokens$; - - const filterTokens = filterExpression === null || filterExpression === void 0 ? void 0 : filterExpression.split(':'); - filterTokens ? filterTokens[1] = dhis2OperatorMap[(_filterTokens$ = filterTokens[1]) !== null && _filterTokens$ !== void 0 ? _filterTokens$ : null] : null; - return filterTokens; -} +function prettyJson(data) { + return JSON.stringify(data, null, 2); +} // ============================================================================= +// TODO: @Elias... what are these functions doing and do they have a place in the new implementation? +// export function getIndicesOf(string, regex) { +// var match, +// indexes = {}; +// regex = new RegExp(regex); +// while ((match = regex.exec(string))) { +// let schemaRef; +// if (!indexes[match[0]]) { +// indexes[match[0]] = {}; +// } +// let hrefString = string.slice( +// match.index, +// indexOf(string, '}', match.index) - 1 +// ); +// let lastIndex = lastIndexOf(hrefString, '/') + 1; +// schemaRef = trim(hrefString.slice(lastIndex)); +// indexes[match[0]][match.index] = schemaRef; +// } +// return indexes; +// } +// export function isLike(string, words) { +// const wordsArrary = words?.match(/([^\W]+[^\s,]*)/)?.splice(0, 1); +// const isFound = word => RegExp(word, 'i')?.test(string); +// return some(wordsArrary, isFound); +// } +// export const dhis2OperatorMap = { +// eq: eq, +// like: isLike, +// }; +// export function applyFilter( +// arrObject, +// targetProperty, +// operator, +// valueToCompareWith +// ) { +// if (targetProperty && operator && valueToCompareWith) { +// try { +// return filter(arrObject, obj => +// Reflect.apply(operator, obj, [obj[targetProperty], valueToCompareWith]) +// ); +// } catch (error) { +// Log.warn( +// `Returned unfiltered data. Failed to apply custom filter(${prettyJson({ +// targetProperty: targetProperty ?? null, +// operator: operator ?? null, +// value: valueToCompareWith ?? null, +// })}) on this collection. The operator you supplied maybe unsupported on this resource at the moment.` +// ); +// return arrObject; +// } +// } +// console.log('No filters applied; returned all records for this resource.'); +// return arrObject; +// } +// export function parseFilter(filterExpression) { +// const filterTokens = filterExpression?.split(':'); +// filterTokens +// ? (filterTokens[1] = dhis2OperatorMap[filterTokens[1] ?? null]) +// : null; +// return filterTokens; +// } +// // TODO: @Elias, end of the investigation block! +// ============================================================================= -function expandAndSetOperation(options, state, operationName) { - return { - operationName, - ...(0, _languageCommon.expandReferences)(options)(state) - }; -} const isArray = variable => !!variable && variable.constructor === Array; @@ -203,92 +132,31 @@ function nestArray(data, key) { } : data; } -function log(operationName, apiVersion, url, resourceType, params) { - logOperation(operationName); - logApiVersion(apiVersion); - logWaitingForServer(url, params); - warnExpectLargeResult(resourceType, url); -} - -function extractValuesForAxios(operationName, values) { - return state => { - var _values$options$apiVe, _values$options; - - const apiVersion = (_values$options$apiVe = (_values$options = values.options) === null || _values$options === void 0 ? void 0 : _values$options.apiVersion) !== null && _values$options$apiVe !== void 0 ? _values$options$apiVe : state.configuration.apiVersion; - const { - username, - password, - hostUrl - } = state.configuration; - const auth = { - username, - password - }; - let urlString = '/' + values.resourceType; +function generateUrl(configuration, options, resourceType) { + let { + hostUrl, + apiVersion + } = configuration; + const urlString = '/' + resourceType; // Note that users can override the apiVersion from configuration with args - if (operationName === 'update') { - urlString += '/' + values.path; - } + if (options === null || options === void 0 ? void 0 : options.apiVersion) apiVersion = options.apiVersion; // TODO: discuss how this actually works on DHIS2. I'm not sure I'm following. - const url = buildUrl(urlString, hostUrl, apiVersion); - let urlParams = null; + const apiMessage = apiVersion ? `Using DHIS2 api version ${apiVersion}` : 'Using latest available version of the DHIS2 api on this server.'; + console.log(apiMessage); + return buildUrl(urlString, hostUrl, apiVersion); +} - if (operationName === 'get' || operationName === 'upsert') { - var _values$options2, _values$options2$para, _values$options3, _values$options3$para, _values$options4, _values$options4$para, _values$options5, _values$options5$para, _values$options6; +function buildUrlParams(options) { + var _options$params, _options$params2, _options$params3, _options$params4; - const filters = (_values$options2 = values.options) === null || _values$options2 === void 0 ? void 0 : (_values$options2$para = _values$options2.params) === null || _values$options2$para === void 0 ? void 0 : _values$options2$para.filters; - const dimensions = (_values$options3 = values.options) === null || _values$options3 === void 0 ? void 0 : (_values$options3$para = _values$options3.params) === null || _values$options3$para === void 0 ? void 0 : _values$options3$para.dimensions; - (_values$options4 = values.options) === null || _values$options4 === void 0 ? true : (_values$options4$para = _values$options4.params) === null || _values$options4$para === void 0 ? true : delete _values$options4$para.filters; - (_values$options5 = values.options) === null || _values$options5 === void 0 ? true : (_values$options5$para = _values$options5.params) === null || _values$options5$para === void 0 ? true : delete _values$options5$para.dimensions; - urlParams = new URLSearchParams((_values$options6 = values.options) === null || _values$options6 === void 0 ? void 0 : _values$options6.params); - filters === null || filters === void 0 ? void 0 : filters.map(f => urlParams.append('filter', f)); - dimensions === null || dimensions === void 0 ? void 0 : dimensions.map(d => urlParams.append('dimension', d)); - } + const filters = options === null || options === void 0 ? void 0 : (_options$params = options.params) === null || _options$params === void 0 ? void 0 : _options$params.filters; + const dimensions = options === null || options === void 0 ? void 0 : (_options$params2 = options.params) === null || _options$params2 === void 0 ? void 0 : _options$params2.dimensions; // We remove filters and dimensions before building standard search params. - const resourceType = values.resourceType; - const data = values.data; - const callback = values.callback; - const extractedValues = { - resourceType, - data, - apiVersion, - auth, - url, - urlParams, - callback - }; - return extractedValues; - }; -} + options === null || options === void 0 ? true : (_options$params3 = options.params) === null || _options$params3 === void 0 ? true : delete _options$params3.filters; + options === null || options === void 0 ? true : (_options$params4 = options.params) === null || _options$params4 === void 0 ? true : delete _options$params4.dimensions; + const urlParams = new URLSearchParams(options === null || options === void 0 ? void 0 : options.params); // Then we re-apply the filters and dimensions in this dhis2-specific way. -function expandExtractAndLog(operationName, initialParams) { - return state => { - const { - resourceType, - data, - apiVersion, - auth, - url, - urlParams, - callback - } = extractValuesForAxios(operationName, (0, _languageCommon.expandReferences)(initialParams)(state))(state); - log(operationName, apiVersion, url, resourceType, urlParams); - return { - url, - data, - resourceType, - auth, - urlParams, - callback - }; - }; -} - -const CONTENT_TYPES = { - xml: 'application/xml', - json: 'application/json', - pdf: 'application/pdf', - csv: 'application/csv', - xls: 'application/vnd.ms-excel' -}; -exports.CONTENT_TYPES = CONTENT_TYPES; \ No newline at end of file + filters === null || filters === void 0 ? void 0 : filters.map(f => urlParams.append('filter', f)); + dimensions === null || dimensions === void 0 ? void 0 : dimensions.map(d => urlParams.append('dimension', d)); + return urlParams; +} \ No newline at end of file diff --git a/src/Adaptor.js b/src/Adaptor.js index 8fbf33a..cd07b3e 100644 --- a/src/Adaptor.js +++ b/src/Adaptor.js @@ -6,21 +6,15 @@ import { expandReferences, } from '@openfn/language-common'; import { indexOf } from 'lodash'; - import { - Log, - warnExpectLargeResult, - logWaitingForServer, buildUrl, - logApiVersion, + buildUrlParams, CONTENT_TYPES, - applyFilter, - parseFilter, - logOperation, - prettyJson, - expandExtractAndLog, + generateUrl, + handleResponse, + Log, nestArray, - expandAndSetOperation, + prettyJson, } from './Utils'; import { request } from './Client'; @@ -71,6 +65,8 @@ function configMigrationHelper(state) { return state; } +// NOTE: In order to prevent unintended exposure of authentication information +// in the logs, we make use of an axios interceptor. axios.interceptors.response.use( function (response) { const contentType = response.headers['content-type']?.split(';')[0]; @@ -83,7 +79,7 @@ axios.interceptors.response.use( if (indexOf(acceptHeaders, contentType) === -1) { const newError = { status: 404, - message: 'Unexpected content,returned', + message: 'Unexpected content returned', responseData: response.data, }; @@ -100,14 +96,25 @@ axios.interceptors.response.use( try { response = { ...response, data: JSON.parse(response.data) }; } catch (error) { - /* Keep quiet */ + Log.warn('Non-JSON response detected, unable to parse.'); } } return response; }, function (error) { - Log.error(`${error?.message}`); - return Promise.reject(error); + try { + const details = error.toJSON(); + if (details?.config?.auth) details.config.auth = '--REDACTED--'; + if (details?.config?.data) details.config.data = '--REDACTED--'; + + Log.error(details.message); + return Promise.reject(details); + } catch (e) { + // TODO: @Elias, why does this error sometimes already appear to be JSONified? + // console.log(e) // "not JSONABLE TypeError: error.toJSON is not a function" + Log.error(error.message); + return Promise.reject(error); + } } ); @@ -223,36 +230,23 @@ axios.interceptors.response.use( * }); */ export function create(resourceType, data, options, callback) { - const initialParams = { resourceType, data, options, callback }; return state => { - const { - url, - data, - resourceType, - auth, - urlParams, - callback, - } = expandExtractAndLog('create', initialParams)(state); + console.log(`Preparing create operation...`); - return request({ + resourceType = expandReferences(resourceType)(state); + data = expandReferences(data)(state); + options = expandReferences(options)(state); + + const { configuration } = state; + + return request(configuration, { method: 'post', - url, + url: generateUrl(configuration, options, resourceType), data: nestArray(data, resourceType), - options: { - auth, - params: urlParams, - }, - }) - .then(result => { - Log.info( - `\nOperation succeeded. Created ${resourceType}: ${result.headers.location}.\n` - ); - if (callback) return callback(composeNextState(state, result.data)); - return composeNextState(state, result.data); - }) - .catch(error => { - throw error; - }); + }).then(result => { + Log.success(`Created ${resourceType}: ${result.headers.location}`); + return handleResponse(result, state, callback); + }); }; } @@ -400,22 +394,25 @@ export function create(resourceType, data, options, callback) { * }); */ export function update(resourceType, path, data, options, callback) { - const initialParams = { resourceType, path, data, options, callback }; return state => { - const { url, data, resourceType, auth, _, callback } = expandExtractAndLog( - 'update', - initialParams - )(state); + console.log(`Preparing update operation...`); - return request({ method: 'put', url, data, options: { auth } }) - .then(result => { - Log.info(`\nOperation succeeded. Updated ${resourceType} ${path}.\n`); - if (callback) return callback(composeNextState(state, result.data)); - return composeNextState(state, result.data); - }) - .catch(error => { - throw error; - }); + resourceType = expandReferences(resourceType)(state); + path = expandReferences(path)(state); + data = expandReferences(data)(state); + options = expandReferences(options)(state); + + const { configuration } = state; + + return request(configuration, { + method: 'put', + url: `${generateUrl(configuration, options, resourceType)}/${path}`, + // TODO: @Elias, why no "nestArray" here? + data, + }).then(result => { + Log.success(`Updated ${resourceType} at ${path}`); + return handleResponse(result, state, callback); + }); }; } @@ -436,33 +433,28 @@ export function update(resourceType, path, data, options, callback) { * trackedEntityInstance: 'dNpxRu1mWG5', * }); */ +// TODO: @Elias, I'm not sure "options" is the right name for the second arg here. +// Isn't this more like the filters that you're using to find the right resources? export function get(resourceType, options, callback) { - const initialParams = { resourceType, options, callback }; return state => { - const { - url: url, - _data, - _resourceType, - auth, - urlParams, - callback, - } = expandExtractAndLog('get', initialParams)(state); - - return request({ + console.log(`Preparing get operation...`); + + resourceType = expandReferences(resourceType)(state); + options = expandReferences(options)(state); + + const { configuration } = state; + + return request(configuration, { method: 'get', - url, - options: { auth, params: urlParams, responseType: 'json' }, - }) - .then(result => { - Log.info( - `\nOperation succeeded. Retrieved ${result.data[resourceType].length} ${resourceType}.\n` - ); - if (callback) return callback(composeNextState(state, result.data)); - return composeNextState(state, result.data); - }) - .catch(error => { - throw error; - }); + url: generateUrl(configuration, options, resourceType), + params: buildUrlParams(options), + responseType: 'json', + }).then(result => { + Log.success( + `Retrieved ${result.data[resourceType].length} ${resourceType}.` + ); + return handleResponse(result, state, callback); + }); }; } @@ -495,30 +487,28 @@ export function get(resourceType, options, callback) { */ export function upsert(resourceType, data, options, callback) { return state => { + console.log(`Preparing upsert via 'get' then 'create' OR 'update'...`); + return get( resourceType, options - )(state) - .then(res => { - const resources = res.data[resourceType]; - if (resources.length > 1) { - throw new RangeError( - `Cannot upsert on Non-unique attribute. The operation found more than one records for your request.` - ); - } else if (resources.length <= 0) { - return create(resourceType, data, options, callback)(state); - } else { - const pathName = - resourceType === 'trackedEntityInstances' - ? 'trackedEntityInstance' - : 'id'; - const path = resources[0][pathName]; - return update(resourceType, path, data, options, callback)(state); - } - }) - .catch(err => { - throw err; - }); + )(state).then(resp => { + const resources = resp.data[resourceType]; + if (resources.length > 1) { + throw new RangeError( + `Cannot upsert on Non-unique attribute. The operation found more than one records for your request.` + ); + } else if (resources.length <= 0) { + return create(resourceType, data, options, callback)(state); + } else { + const pathName = + resourceType === 'trackedEntityInstances' + ? 'trackedEntityInstance' + : 'id'; + const path = resources[0][pathName]; + return update(resourceType, path, data, options, callback)(state); + } + }); }; } @@ -534,7 +524,7 @@ export function upsert(resourceType, data, options, callback) { */ export function discover(httpMethod, endpoint) { return state => { - Log.info( + console.log( `Discovering query/import parameters for ${httpMethod} on ${endpoint}` ); return axios @@ -596,7 +586,7 @@ export function discover(httpMethod, endpoint) { } ) .then(result => { - Log.info( + console.log( `\t=======================================================================================\n\tQuery Parameters for ${httpMethod} on ${endpoint} [${ result.data.description ?? '' }]\n\t=======================================================================================` @@ -636,6 +626,7 @@ export function discover(httpMethod, endpoint) { * name: 'New Name', * }); */ +// TODO: @Elias, can this be deleted in favor of update? How does DHIS2 handle PATCH vs PUT? export function patch(resourceType, path, data, params, options, callback) { return state => { resourceType = expandReferences(resourceType)(state); @@ -672,14 +663,6 @@ export function patch(resourceType, path, data, params, options, callback) { Accept: CONTENT_TYPES[responseType] ?? 'application/json', }; - logOperation(operationName); - - logApiVersion(apiVersion); - - logWaitingForServer(url, queryParams); - - warnExpectLargeResult(resourceType, url); - return axios .request({ method: 'PATCH', @@ -697,7 +680,7 @@ export function patch(resourceType, path, data, params, options, callback) { status: result.status, statusText: result.statusText, }; - Log.info( + Log.success( `${operationName} succeeded. Updated ${resourceType}.\nSummary:\n${prettyJson( resultObject )}` @@ -722,6 +705,7 @@ export function patch(resourceType, path, data, params, options, callback) { * @example Example`deleting` a `tracked entity instance` * del('trackedEntityInstances', 'LcRd6Nyaq7T'); */ +// TODO: @Elias, can this be implemented using the same pattern as update but without data? export function del(resourceType, path, data, params, options, callback) { return state => { resourceType = expandReferences(resourceType)(state); @@ -758,14 +742,6 @@ export function del(resourceType, path, data, params, options, callback) { const url = buildUrl('/' + resourceType + '/' + path, hostUrl, apiVersion); - logOperation(operationName); - - logApiVersion(apiVersion); - - logWaitingForServer(url, queryParams); - - warnExpectLargeResult(resourceType, url); - return axios .request({ method: 'DELETE', @@ -779,7 +755,7 @@ export function del(resourceType, path, data, params, options, callback) { headers, }) .then(result => { - Log.info( + Log.success( `${operationName} succeeded. DELETED ${resourceType}.\nSummary:\n${prettyJson( result.data )}` @@ -806,7 +782,12 @@ export function attrVal(tei, attributeName) { )?.value; } -export { attribute } from './Utils'; +export function attribute(attributeId, attributeValue) { + return { + attribute: attributeId, + value: attributeValue, + }; +} export { field, diff --git a/src/Client.js b/src/Client.js index cd050de..309a8cf 100644 --- a/src/Client.js +++ b/src/Client.js @@ -1,10 +1,14 @@ import axios from 'axios'; -export function request({ method, url, data, options }) { - let headers = { 'Content-Type': 'application/json' }; - let req = { method, url, headers, ...options }; - if (method !== 'get') { - req = { ...req, data }; - } - return axios.request(req); +export function request({ username, password }, axiosRequest) { + const { method, url } = axiosRequest; + + console.log(`Sending ${method} request to ${url}`); + + return axios.request({ + headers: { 'Content-Type': 'application/json' }, + auth: { username, password }, + // Note that providing headers or auth in the request object will overwrite. + ...axiosRequest, + }); } diff --git a/src/Utils.js b/src/Utils.js index b2563e1..6876d78 100644 --- a/src/Utils.js +++ b/src/Utils.js @@ -1,184 +1,115 @@ -import { eq, filter, some, indexOf, lastIndexOf, trim } from 'lodash'; import axios from 'axios'; -import { expandReferences } from '@openfn/language-common'; - -export function composeSuccessMessage(operation) { - return `${operation} succeeded. The body of this result will be available in state.data or in your callback.`; -} - -export function warnExpectLargeResult(paramOrResourceType, endpointUrl) { - if (!paramOrResourceType) - Log.warn( - ` Missing params or resourceType. This may take a while. This endpoint(${endpointUrl}) may return a large collection of records, since 'params' or 'resourceType' is not specified. We recommend you specify 'params' or 'resourceType' or use 'filter' parameter to limit the content of the result.` - ); -} - -export function logWaitingForServer(url, params) { - if (params) { - console.info( - 'Request params: ', - typeof params === 'object' && !(params instanceof URLSearchParams) - ? prettyJson(params) - : params - ); - } - - console.info(`Waiting for response from ${url}`); -} - -export function logApiVersion(apiVersion) { - const message = - apiVersion && apiVersion - ? `Using DHIS2 api version ${apiVersion}` - : ' Attempting to use apiVersion without providing it in state.configuration or in options parameter. You may encounter errors. api_version_missing.'; - - if (apiVersion) console.warn(message); - else console.warn(`Using latest version of DHIS2 api.`); -} - -export function logOperation(operation) { - console.info(`Executing ${operation} ...`); -} - -export function buildUrl(path, hostUrl, apiVersion) { - const pathSuffix = apiVersion ? `/${apiVersion}${path}` : `${path}`; - const url = hostUrl + '/api' + pathSuffix; - - return url; -} - -export function attribute(attributeId, attributeValue) { - return { - attribute: attributeId, - value: attributeValue, - }; -} - -export function requestHttpHead(endpointUrl, { username, password }) { - return axios - .request({ - method: 'HEAD', - url: endpointUrl, - auth: { - username, - password, - }, - }) - .then(result => result.headers['content-length']); -} - -export function validateMetadataPayload(payload, resourceType) { - return axios - .request({ - method: 'POST', - url: `https://play.dhis2.org/dev/api/schemas/${resourceType}`, - auth: { - username: 'admin', - password: 'distict', - }, - data: payload, - }) - .then(result => result.data); -} - -export function handleResponse(result, state, callback) { - if (callback) return callback(composeNextState(state, result)); - - return composeNextState(state, result); -} - -export function prettyJson(data) { - return JSON.stringify(data, null, 2); -} - -export function getIndicesOf(string, regex) { - var match, - indexes = {}; - - regex = new RegExp(regex); - - while ((match = regex.exec(string))) { - let schemaRef; - if (!indexes[match[0]]) { - indexes[match[0]] = {}; - } - let hrefString = string.slice( - match.index, - indexOf(string, '}', match.index) - 1 - ); - let lastIndex = lastIndexOf(hrefString, '/') + 1; - schemaRef = trim(hrefString.slice(lastIndex)); - indexes[match[0]][match.index] = schemaRef; - } +import { eq, filter, some, indexOf, lastIndexOf, trim } from 'lodash'; +import { composeNextState } from '@openfn/language-common'; - return indexes; -} +export const CONTENT_TYPES = { + xml: 'application/xml', + json: 'application/json', + pdf: 'application/pdf', + csv: 'application/csv', + xls: 'application/vnd.ms-excel', +}; export class Log { - static info(message) { - return console.info('(info)', new Date(), `\n${message}`); + static success(message) { + return console.info(`✓ ${message} @ ${new Date()}`); } static warn(message) { - return console.warn('⚠ WARNING', new Date(), `\n${message}`); + return console.warn(`⚠ Warning: ${message} @ ${new Date()}`); } static error(message) { - return console.error('✗ ERROR', new Date(), `\n${message}`); + return console.error(`✗ Error: ${message} @ ${new Date()}`); } } -export function isLike(string, words) { - const wordsArrary = words?.match(/([^\W]+[^\s,]*)/)?.splice(0, 1); - const isFound = word => RegExp(word, 'i')?.test(string); - return some(wordsArrary, isFound); +export function buildUrl(path, hostUrl, apiVersion) { + const pathSuffix = apiVersion ? `/${apiVersion}${path}` : `${path}`; + return hostUrl + '/api' + pathSuffix; } -export const dhis2OperatorMap = { - eq: eq, - like: isLike, -}; - -export function applyFilter( - arrObject, - targetProperty, - operator, - valueToCompareWith -) { - if (targetProperty && operator && valueToCompareWith) { - try { - return filter(arrObject, obj => - Reflect.apply(operator, obj, [obj[targetProperty], valueToCompareWith]) - ); - } catch (error) { - Log.warn( - `Returned unfiltered data. Failed to apply custom filter(${prettyJson({ - targetProperty: targetProperty ?? null, - operator: operator ?? null, - value: valueToCompareWith ?? null, - })}) on this collection. The operator you supplied maybe unsupported on this resource at the moment.` - ); - return arrObject; - } - } - Log.info(`No filters applied, returned all records on this resource.`); - return arrObject; +export function handleResponse(result, state, callback) { + // TODO: @Elias, should composeNextState get passed result OR result.data? + if (callback) return callback(composeNextState(state, result.data)); + return composeNextState(state, result.data); } -export function parseFilter(filterExpression) { - const filterTokens = filterExpression?.split(':'); - filterTokens - ? (filterTokens[1] = dhis2OperatorMap[filterTokens[1] ?? null]) - : null; - return filterTokens; +export function prettyJson(data) { + return JSON.stringify(data, null, 2); } -export function expandAndSetOperation(options, state, operationName) { - return { - operationName, - ...expandReferences(options)(state), - }; -} +// ============================================================================= +// TODO: @Elias... what are these functions doing and do they have a place in the new implementation? +// export function getIndicesOf(string, regex) { +// var match, +// indexes = {}; + +// regex = new RegExp(regex); + +// while ((match = regex.exec(string))) { +// let schemaRef; +// if (!indexes[match[0]]) { +// indexes[match[0]] = {}; +// } +// let hrefString = string.slice( +// match.index, +// indexOf(string, '}', match.index) - 1 +// ); +// let lastIndex = lastIndexOf(hrefString, '/') + 1; +// schemaRef = trim(hrefString.slice(lastIndex)); +// indexes[match[0]][match.index] = schemaRef; +// } + +// return indexes; +// } + +// export function isLike(string, words) { +// const wordsArrary = words?.match(/([^\W]+[^\s,]*)/)?.splice(0, 1); +// const isFound = word => RegExp(word, 'i')?.test(string); +// return some(wordsArrary, isFound); +// } + +// export const dhis2OperatorMap = { +// eq: eq, +// like: isLike, +// }; + +// export function applyFilter( +// arrObject, +// targetProperty, +// operator, +// valueToCompareWith +// ) { +// if (targetProperty && operator && valueToCompareWith) { +// try { +// return filter(arrObject, obj => +// Reflect.apply(operator, obj, [obj[targetProperty], valueToCompareWith]) +// ); +// } catch (error) { +// Log.warn( +// `Returned unfiltered data. Failed to apply custom filter(${prettyJson({ +// targetProperty: targetProperty ?? null, +// operator: operator ?? null, +// value: valueToCompareWith ?? null, +// })}) on this collection. The operator you supplied maybe unsupported on this resource at the moment.` +// ); +// return arrObject; +// } +// } +// console.log('No filters applied; returned all records for this resource.'); +// return arrObject; +// } + +// export function parseFilter(filterExpression) { +// const filterTokens = filterExpression?.split(':'); +// filterTokens +// ? (filterTokens[1] = dhis2OperatorMap[filterTokens[1] ?? null]) +// : null; +// return filterTokens; +// } +// // TODO: @Elias, end of the investigation block! +// ============================================================================= const isArray = variable => !!variable && variable.constructor === Array; @@ -186,80 +117,36 @@ export function nestArray(data, key) { return isArray(data) ? { [key]: data } : data; } -function log(operationName, apiVersion, url, resourceType, params) { - logOperation(operationName); - logApiVersion(apiVersion); - logWaitingForServer(url, params); - warnExpectLargeResult(resourceType, url); -} +export function generateUrl(configuration, options, resourceType) { + let { hostUrl, apiVersion } = configuration; + const urlString = '/' + resourceType; -function extractValuesForAxios(operationName, values) { - return state => { - const apiVersion = - values.options?.apiVersion ?? state.configuration.apiVersion; - const { username, password, hostUrl } = state.configuration; - const auth = { username, password }; + // Note that users can override the apiVersion from configuration with args + if (options?.apiVersion) apiVersion = options.apiVersion; - let urlString = '/' + values.resourceType; - if (operationName === 'update') { - urlString += '/' + values.path; - } - const url = buildUrl(urlString, hostUrl, apiVersion); + // TODO: discuss how this actually works on DHIS2. I'm not sure I'm following. + const apiMessage = apiVersion + ? `Using DHIS2 api version ${apiVersion}` + : 'Using latest available version of the DHIS2 api on this server.'; - let urlParams = null; - if (operationName === 'get' || operationName === 'upsert') { - const filters = values.options?.params?.filters; - const dimensions = values.options?.params?.dimensions; - delete values.options?.params?.filters; - delete values.options?.params?.dimensions; - urlParams = new URLSearchParams(values.options?.params); - filters?.map(f => urlParams.append('filter', f)); - dimensions?.map(d => urlParams.append('dimension', d)); - } + console.log(apiMessage); - const resourceType = values.resourceType; - const data = values.data; - const callback = values.callback; + return buildUrl(urlString, hostUrl, apiVersion); +} - const extractedValues = { - resourceType, - data, - apiVersion, - auth, - url, - urlParams, - callback, - }; +export function buildUrlParams(options) { + const filters = options?.params?.filters; + const dimensions = options?.params?.dimensions; - return extractedValues; - }; -} + // We remove filters and dimensions before building standard search params. + delete options?.params?.filters; + delete options?.params?.dimensions; -export function expandExtractAndLog(operationName, initialParams) { - return state => { - const { - resourceType, - data, - apiVersion, - auth, - url, - urlParams, - callback, - } = extractValuesForAxios( - operationName, - expandReferences(initialParams)(state) - )(state); + const urlParams = new URLSearchParams(options?.params); - log(operationName, apiVersion, url, resourceType, urlParams); + // Then we re-apply the filters and dimensions in this dhis2-specific way. + filters?.map(f => urlParams.append('filter', f)); + dimensions?.map(d => urlParams.append('dimension', d)); - return { url, data, resourceType, auth, urlParams, callback }; - }; + return urlParams; } - -export const CONTENT_TYPES = { - xml: 'application/xml', - json: 'application/json', - pdf: 'application/pdf', - csv: 'application/csv', - xls: 'application/vnd.ms-excel', -}; diff --git a/test/index.js b/test/index.js index 8050b3d..d1376b1 100644 --- a/test/index.js +++ b/test/index.js @@ -84,9 +84,8 @@ describe('CREATE', () => { message: 'the response', }); - const response = await execute(create('events', state => state.data))( - state - ); + const response = await execute(create('events', state.data))(state); + expect(response.data).to.eql({ httpStatus: 'OK', message: 'the response' }); }); From 7ddd4ac29a85c7350c48caf50de3390d6809b1f1 Mon Sep 17 00:00:00 2001 From: Taylor Downs Date: Sat, 18 Dec 2021 14:37:38 +0000 Subject: [PATCH 07/26] add docs for attribute and update attrVal --- ast.json | 55 +++++++++++++++++++++++++++++++++++++++++++++++++- lib/Adaptor.js | 13 +++++++++++- src/Adaptor.js | 17 ++++++++++++---- 3 files changed, 79 insertions(+), 6 deletions(-) diff --git a/ast.json b/ast.json index aa9cffa..43128b0 100644 --- a/ast.json +++ b/ast.json @@ -856,7 +856,7 @@ }, { "title": "example", - "description": "valByName(tei.attributes, 'first name')" + "description": "attrVal(tei.attributes, 'first name')" }, { "title": "function", @@ -892,6 +892,59 @@ ] }, "valid": true + }, + { + "name": "attribute", + "params": [ + "attributeId", + "attributeValue" + ], + "docs": { + "description": "Converts an attribute ID and value into a DSHI2 attribute object", + "tags": [ + { + "title": "public", + "description": null, + "type": null + }, + { + "title": "example", + "description": "attribute('w75KJ2mc4zz', 'Elias')" + }, + { + "title": "function", + "description": null, + "name": null + }, + { + "title": "param", + "description": "A tracked entity instance (TEI) attribute ID.", + "type": { + "type": "NameExpression", + "name": "string" + }, + "name": "attributeId" + }, + { + "title": "param", + "description": "The value for that attribute.", + "type": { + "type": "NameExpression", + "name": "string" + }, + "name": "attributeValue" + }, + { + "title": "returns", + "description": null, + "type": { + "type": "NameExpression", + "name": "object" + } + } + ] + }, + "valid": true } ], "exports": [], diff --git a/lib/Adaptor.js b/lib/Adaptor.js index fb93c16..2a3bf5e 100644 --- a/lib/Adaptor.js +++ b/lib/Adaptor.js @@ -777,7 +777,7 @@ function del(resourceType, path, data, params, options, callback) { * Gets an attribute value by its case-insensitive display name * @public * @example - * valByName(tei.attributes, 'first name') + * attrVal(tei.attributes, 'first name') * @function * @param {Object} tei - A tracked entity instance (TEI) object * @param {string} attributeName - The 'displayName' to search for in the TEI's attributes @@ -790,6 +790,17 @@ function attrVal(tei, attributeName) { return tei === null || tei === void 0 ? void 0 : (_tei$attributes = tei.attributes) === null || _tei$attributes === void 0 ? void 0 : (_tei$attributes$find = _tei$attributes.find(a => (a === null || a === void 0 ? void 0 : a.displayName.toLowerCase()) == attributeName.toLowerCase())) === null || _tei$attributes$find === void 0 ? void 0 : _tei$attributes$find.value; } +/** + * Converts an attribute ID and value into a DSHI2 attribute object + * @public + * @example + * attribute('w75KJ2mc4zz', 'Elias') + * @function + * @param {string} attributeId - A tracked entity instance (TEI) attribute ID. + * @param {string} attributeValue - The value for that attribute. + * @returns {object} + */ + function attribute(attributeId, attributeValue) { return { diff --git a/src/Adaptor.js b/src/Adaptor.js index cd07b3e..3adc120 100644 --- a/src/Adaptor.js +++ b/src/Adaptor.js @@ -546,9 +546,8 @@ export function discover(httpMethod, endpoint) { if (param.schema['$ref']) { let schemaRefIndex = param.schema['$ref'].lastIndexOf('/') + 1; - let schemaRef = param.schema['$ref'].slice( - schemaRefIndex - ); + let schemaRef = + param.schema['$ref'].slice(schemaRefIndex); param.schema = tempData.components.schemas[schemaRef]; } @@ -770,7 +769,7 @@ export function del(resourceType, path, data, params, options, callback) { * Gets an attribute value by its case-insensitive display name * @public * @example - * valByName(tei.attributes, 'first name') + * attrVal(tei.attributes, 'first name') * @function * @param {Object} tei - A tracked entity instance (TEI) object * @param {string} attributeName - The 'displayName' to search for in the TEI's attributes @@ -782,6 +781,16 @@ export function attrVal(tei, attributeName) { )?.value; } +/** + * Converts an attribute ID and value into a DSHI2 attribute object + * @public + * @example + * attribute('w75KJ2mc4zz', 'Elias') + * @function + * @param {string} attributeId - A tracked entity instance (TEI) attribute ID. + * @param {string} attributeValue - The value for that attribute. + * @returns {object} + */ export function attribute(attributeId, attributeValue) { return { attribute: attributeId, From 60cd5f1c7a5687c04d297df0580b1b188c708f24 Mon Sep 17 00:00:00 2001 From: Taylor Downs Date: Sat, 18 Dec 2021 14:39:02 +0000 Subject: [PATCH 08/26] premajor version bump --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 660c548..c50dfcf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@openfn/language-dhis2", - "version": "2.0.11", + "version": "3.0.0-0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 31671a8..43f67b6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@openfn/language-dhis2", - "version": "2.0.11", + "version": "3.0.0-0", "description": "DHIS2 Language Pack for OpenFn", "homepage": "https://docs.openfn.org", "repository": { From aac2f766555ab3f517c0dc0458ed15221d88066d Mon Sep 17 00:00:00 2001 From: "Elias W. BA" Date: Tue, 21 Dec 2021 14:54:57 +0000 Subject: [PATCH 09/26] Review and address todos --- ast.json | 82 +++++-------------------------------------- lib/Adaptor.js | 62 ++++++++++++++------------------ lib/Client.js | 12 ++++++- lib/Utils.js | 95 +++++++------------------------------------------- src/Adaptor.js | 52 +++++++++------------------ src/Client.js | 12 ++++++- src/Utils.js | 94 ++++++------------------------------------------- test/index.js | 27 +++++++++++++- 8 files changed, 123 insertions(+), 313 deletions(-) diff --git a/ast.json b/ast.json index 43128b0..6ef1bf1 100644 --- a/ast.json +++ b/ast.json @@ -41,7 +41,7 @@ }, { "title": "param", - "description": "Optional `options` to control the behavior of the `create` operation and to pass `import parameters` E.g. `{dryRun: true, importStrategy: CREATE}` See {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html DHIS2 API documentation} or {@link discover}..` Defaults to `{operationName: 'create', apiVersion: null, responseType: 'json'}`", + "description": "Optional `options` to define URL parameters (E.g. `filters`, `dimensions` and `import parameters`), axios configurations (E.g. `auth`) and DHIS 2 api version to use.", "type": { "type": "OptionalType", "expression": { @@ -181,37 +181,12 @@ }, { "title": "param", - "description": "Optional options for update method. Defaults to `{operationName: 'update', apiVersion: state.configuration.apiVersion, responseType: 'json'}`", + "description": "Optional `options` to define URL parameters (E.g. `filters`, `dimensions` and `import parameters`), axios configurations (E.g. `auth`) and DHIS 2 api version to use.", "type": { "type": "OptionalType", "expression": { - "type": "RecordType", - "fields": [ - { - "type": "FieldType", - "key": "apiVersion", - "value": { - "type": "NameExpression", - "name": "number" - } - }, - { - "type": "FieldType", - "key": "operationName", - "value": { - "type": "NameExpression", - "name": "string" - } - }, - { - "type": "FieldType", - "key": "resourceType", - "value": { - "type": "NameExpression", - "name": "string" - } - } - ] + "type": "NameExpression", + "name": "Object" } }, "name": "options" @@ -326,37 +301,12 @@ }, { "title": "param", - "description": "`Optional` options for `getData` operation. Defaults to `{operationName: 'getData', apiVersion: state.configuration.apiVersion, responseType: 'json'}`.", + "description": "Optional `options` to define URL parameters (E.g. `filters`, `dimensions` and `import parameters`), axios configurations (E.g. `auth`) and DHIS 2 api version to use.", "type": { "type": "OptionalType", "expression": { - "type": "RecordType", - "fields": [ - { - "type": "FieldType", - "key": "apiVersion", - "value": { - "type": "NameExpression", - "name": "number" - } - }, - { - "type": "FieldType", - "key": "operationName", - "value": { - "type": "NameExpression", - "name": "string" - } - }, - { - "type": "FieldType", - "key": "responseType", - "value": { - "type": "NameExpression", - "name": "string" - } - } - ] + "type": "NameExpression", + "name": "Object" } }, "name": "options" @@ -383,7 +333,7 @@ }, { "title": "example", - "description": "getData('trackedEntityInstances', {\n fields: '*',\n ou: 'DiszpKrYNg8',\n entityType: 'nEenWmSyUEp',\n trackedEntityInstance: 'dNpxRu1mWG5',\n});", + "description": "get('trackedEntityInstances', {\n fields: '*',\n ou: 'DiszpKrYNg8',\n entityType: 'nEenWmSyUEp',\n trackedEntityInstance: 'dNpxRu1mWG5',\n});", "caption": "Example getting one `trackedEntityInstance` with `Id` 'dNpxRu1mWG5' for a given `orgUnit(DiszpKrYNg8)`" } ] @@ -506,22 +456,6 @@ "title": "example", "description": "upsert(\n 'trackedEntityInstances',\n {\n attributeId: 'lZGmxYbs97q',\n attributeValue: state =>\n state.data.attributes.find(obj => obj.attribute === 'lZGmxYbs97q')\n .value,\n },\n state.data,\n { ou: 'TSyzvBiovKh' }\n);", "caption": "Example `expression.js` of upsert" - }, - { - "title": "todo", - "description": "Tweak/refine to mimic implementation based on the following inspiration: {@link https://sqlite.org/lang_upsert.html sqlite upsert} and {@link https://wiki.postgresql.org/wiki/UPSERT postgresql upsert}" - }, - { - "title": "todo", - "description": "Test implementation for upserting metadata" - }, - { - "title": "todo", - "description": "Test implementation for upserting data values" - }, - { - "title": "todo", - "description": "Implement the updateCondition" } ] }, diff --git a/lib/Adaptor.js b/lib/Adaptor.js index 2a3bf5e..0bfea6f 100644 --- a/lib/Adaptor.js +++ b/lib/Adaptor.js @@ -202,24 +202,21 @@ _axios.default.interceptors.response.use(function (response) { * @function * @param {string} resourceType - Type of resource to create. E.g. `trackedEntityInstances`, `programs`, `events`, ... * @param {Object} data - 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 control the behavior of the `create` operation and to pass `import parameters` E.g. `{dryRun: true, importStrategy: CREATE}` See {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html DHIS2 API documentation} or {@link discover}..` Defaults to `{operationName: 'create', apiVersion: null, responseType: 'json'}` + * @param {Object} [options] - Optional `options` to define URL parameters (E.g. `filters`, `dimensions` and `import parameters`), axios configurations (E.g. `auth`) and DHIS 2 api version to use. * @param {function} [callback] - Optional callback to handle the response * @returns {Operation} - * * @example -a `program` * create('programs', { * name: 'name 20', * shortName: 'n20', * programType: 'WITHOUT_REGISTRATION', * }); - * * @example -an `event` * create('events', { * program: 'eBAyeGv0exc', * orgUnit: 'DiszpKrYNg8', * status: 'COMPLETED', * }); - * * @example -a `trackedEntityInstance` * create('trackedEntityInstances', { * orgUnit: 'TSyzvBiovKh', @@ -231,10 +228,8 @@ _axios.default.interceptors.response.use(function (response) { * }, * ] * }); - * * @example -a `dataSet` * create('dataSets', { name: 'OpenFn Data Set', periodType: 'Monthly' }); - * * @example -a `dataSetNotification` * create('dataSetNotificationTemplates', { * dataSetNotificationTrigger: 'DATA_SET_COMPLETION', @@ -244,7 +239,6 @@ _axios.default.interceptors.response.use(function (response) { * deliveryChannels: ['SMS'], * dataSets: [], * }); - * * @example -a `dataElement` * create('dataElements', { * aggregationType: 'SUM', @@ -253,13 +247,11 @@ _axios.default.interceptors.response.use(function (response) { * name: 'Paracetamol', * shortName: 'Para', * }); - * * @example -a `dataElementGroup` * create('dataElementGroups', { * name: 'Data Element Group 1', * dataElements: [], * }); - * * @example -a `dataElementGroupSet` * create('dataElementGroupSets', { * name: 'Data Element Group Set 4', @@ -267,7 +259,6 @@ _axios.default.interceptors.response.use(function (response) { * shortName: 'DEGS4', * dataElementGroups: [], * }); - * * @example -a `dataValueSet` * create('dataValueSets', { * dataElement: 'f7n9E0hX8qk', @@ -275,7 +266,6 @@ _axios.default.interceptors.response.use(function (response) { * orgUnit: 'DiszpKrYNg8', * value: '12', * }); - * * @example -a `dataValueSet` with related `dataValues` * create('dataValueSets', { * dataSet: 'pBOMPrpg1QX', @@ -297,7 +287,6 @@ _axios.default.interceptors.response.use(function (response) { * }, * ], * }); - * * @example -an `enrollment` * create('enrollments', { * trackedEntityInstance: 'bmshzEacgxa', @@ -315,13 +304,19 @@ function create(resourceType, data, options, callback) { resourceType = (0, _languageCommon.expandReferences)(resourceType)(state); data = (0, _languageCommon.expandReferences)(data)(state); options = (0, _languageCommon.expandReferences)(options)(state); + const { + params, + requestConfig + } = options || {}; const { configuration } = state; return (0, _Client.request)(configuration, { method: 'post', url: (0, _Utils.generateUrl)(configuration, options, resourceType), - data: (0, _Utils.nestArray)(data, resourceType) + params: params && (0, _Utils.buildUrlParams)(params), + data: (0, _Utils.nestArray)(data, resourceType), + ...requestConfig }).then(result => { _Utils.Log.success(`Created ${resourceType}: ${result.headers.location}`); @@ -337,7 +332,7 @@ function create(resourceType, data, options, callback) { * @param {string} resourceType - The type of resource to be updated. E.g. `dataElements`, `organisationUnits`, etc. * @param {string} path - The `id` or `path` to the `object` to be updated. E.g. `FTRrcoaog83` or `FTRrcoaog83/{collection-name}/{object-id}` * @param {Object} data - Data to update. It requires to send `all required fields` or the `full body`. If you want `partial updates`, use `patch` operation. - * @param {{apiVersion: number,operationName: string,resourceType: string}} [options] - Optional options for update method. Defaults to `{operationName: 'update', apiVersion: state.configuration.apiVersion, responseType: 'json'}` + * @param {Object} [options] - Optional `options` to define URL parameters (E.g. `filters`, `dimensions` and `import parameters`), axios configurations (E.g. `auth`) and DHIS 2 api version to use. * @param {function} [callback] - Optional callback to handle the response * @returns {Operation} * @example -a program @@ -346,7 +341,6 @@ function create(resourceType, data, options, callback) { * shortName: '14e1aa02', * programType: 'WITHOUT_REGISTRATION', * }); - * * @example an `event` * update('events', 'PVqUD2hvU4E', { * program: 'eBAyeGv0exc', @@ -355,7 +349,6 @@ function create(resourceType, data, options, callback) { * storedBy: 'admin', * dataValues: [], * }); - * * @example a `trackedEntityInstance` * update('trackedEntityInstances', 'IeQfgUtGPq2', { * created: '2015-08-06T21:12:37.256', @@ -396,10 +389,8 @@ function create(resourceType, data, options, callback) { * }, * ], * }); - * * @example -a `dataSet` * update('dataSets', 'lyLU2wR22tC', { name: 'OpenFN Data Set', periodType: 'Weekly' }); - * * @example -a `dataSetNotification` * update('dataSetNotificationTemplates', 'VbQBwdm1wVP', { * dataSetNotificationTrigger: 'DATA_SET_COMPLETION', @@ -409,7 +400,6 @@ function create(resourceType, data, options, callback) { * deliveryChannels: ['SMS'], * dataSets: [], * }); - * * @example -a `dataElement` * update('dataElements', 'FTRrcoaog83', { * aggregationType: 'SUM', @@ -418,13 +408,11 @@ function create(resourceType, data, options, callback) { * name: 'Paracetamol', * shortName: 'Para', * }); - * * @example -a `dataElementGroup` * update('dataElementGroups', 'QrprHT61XFk', { * name: 'Data Element Group 1', * dataElements: [], * }); - * * @example -a `dataElementGroupSet` * update('dataElementGroupSets', 'VxWloRvAze8', { * name: 'Data Element Group Set 4', @@ -432,7 +420,6 @@ function create(resourceType, data, options, callback) { * shortName: 'DEGS4', * dataElementGroups: [], * }); - * * @example -a `dataValueSet` * update('dataValueSets', 'AsQj6cDsUq4', { * dataElement: 'f7n9E0hX8qk', @@ -440,7 +427,6 @@ function create(resourceType, data, options, callback) { * orgUnit: 'DiszpKrYNg8', * value: '12', * }); - * * @example -a `dataValueSet` with related `dataValues` * update('dataValueSets', 'Ix2HsbDMLea', { * dataSet: 'pBOMPrpg1QX', @@ -462,7 +448,6 @@ function create(resourceType, data, options, callback) { * }, * ], * }); - * * @example a single enrollment * update('enrollments', 'CmsHzercTBa' { * trackedEntityInstance: 'bmshzEacgxa', @@ -481,14 +466,19 @@ function update(resourceType, path, data, options, callback) { path = (0, _languageCommon.expandReferences)(path)(state); data = (0, _languageCommon.expandReferences)(data)(state); options = (0, _languageCommon.expandReferences)(options)(state); + const { + params, + requestConfig + } = options || {}; const { configuration } = state; return (0, _Client.request)(configuration, { method: 'put', url: `${(0, _Utils.generateUrl)(configuration, options, resourceType)}/${path}`, - // TODO: @Elias, why no "nestArray" here? - data + params: params && (0, _Utils.buildUrlParams)(params), + data, + ...requestConfig }).then(result => { _Utils.Log.success(`Updated ${resourceType} at ${path}`); @@ -502,19 +492,17 @@ function update(resourceType, path, data, options, callback) { * @public * @function * @param {string} resourceType - The type of resource to get(use its `plural` name). E.g. `dataElements`, `trackedEntityInstances`,`organisationUnits`, etc. - * @param {{apiVersion: number,operationName: string,responseType: string}}[options] - `Optional` options for `getData` operation. Defaults to `{operationName: 'getData', apiVersion: state.configuration.apiVersion, responseType: 'json'}`. + * @param {Object} [options] - Optional `options` to define URL parameters (E.g. `filters`, `dimensions` and `import parameters`), axios configurations (E.g. `auth`) and DHIS 2 api version to use. * @param {function} [callback] - Optional callback to handle the response * @returns {Operation} state * @example Example getting one `trackedEntityInstance` with `Id` 'dNpxRu1mWG5' for a given `orgUnit(DiszpKrYNg8)` - * getData('trackedEntityInstances', { + * get('trackedEntityInstances', { * fields: '*', * ou: 'DiszpKrYNg8', * entityType: 'nEenWmSyUEp', * trackedEntityInstance: 'dNpxRu1mWG5', * }); */ -// TODO: @Elias, I'm not sure "options" is the right name for the second arg here. -// Isn't this more like the filters that you're using to find the right resources? function get(resourceType, options, callback) { @@ -522,14 +510,19 @@ function get(resourceType, options, callback) { console.log(`Preparing get operation...`); resourceType = (0, _languageCommon.expandReferences)(resourceType)(state); options = (0, _languageCommon.expandReferences)(options)(state); + const { + params, + requestConfig + } = options || {}; const { configuration } = state; return (0, _Client.request)(configuration, { method: 'get', url: (0, _Utils.generateUrl)(configuration, options, resourceType), - params: (0, _Utils.buildUrlParams)(options), - responseType: 'json' + params: params && (0, _Utils.buildUrlParams)(params), + responseType: 'json', + ...requestConfig }).then(result => { _Utils.Log.success(`Retrieved ${result.data[resourceType].length} ${resourceType}.`); @@ -559,10 +552,6 @@ function get(resourceType, options, callback) { * state.data, * { ou: 'TSyzvBiovKh' } * ); - * @todo Tweak/refine to mimic implementation based on the following inspiration: {@link https://sqlite.org/lang_upsert.html sqlite upsert} and {@link https://wiki.postgresql.org/wiki/UPSERT postgresql upsert} - * @todo Test implementation for upserting metadata - * @todo Test implementation for upserting data values - * @todo Implement the updateCondition */ @@ -661,6 +650,7 @@ function discover(httpMethod, endpoint) { * }); */ // TODO: @Elias, can this be deleted in favor of update? How does DHIS2 handle PATCH vs PUT? +// I need to investigate on this. But I think DHIS 2 forces to send all properties back when we do an update. If that's confirmed then this may be needed. function patch(resourceType, path, data, params, options, callback) { diff --git a/lib/Client.js b/lib/Client.js index 543f0dd..c40bd7b 100644 --- a/lib/Client.js +++ b/lib/Client.js @@ -9,6 +9,16 @@ var _axios = _interopRequireDefault(require("axios")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } +/** + * The request client takes configuration from state and an axios request object + * then (1) logs the method and URL, (2) applies standard headers and auth + * before spreading the rest of the axios configuration, and (3) executes an + * axios request. + * @function + * @param {object} configuration - configuration must have a username and password + * @param {object} axiosRequest - the axiosRequest contains valid axios params: https://axios-http.com/docs/req_config + * @returns {Promise} a promise that will resolve to either a response object or an error object. + */ function request({ username, password @@ -18,7 +28,7 @@ function request({ url } = axiosRequest; console.log(`Sending ${method} request to ${url}`); - return _axios.default.request({ + return (0, _axios.default)({ headers: { 'Content-Type': 'application/json' }, diff --git a/lib/Utils.js b/lib/Utils.js index 30fbf44..66ce6d4 100644 --- a/lib/Utils.js +++ b/lib/Utils.js @@ -11,14 +11,8 @@ exports.generateUrl = generateUrl; exports.buildUrlParams = buildUrlParams; exports.Log = exports.CONTENT_TYPES = void 0; -var _axios = _interopRequireDefault(require("axios")); - -var _lodash = require("lodash"); - var _languageCommon = require("@openfn/language-common"); -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - const CONTENT_TYPES = { xml: 'application/xml', json: 'application/json', @@ -51,78 +45,16 @@ function buildUrl(path, hostUrl, apiVersion) { } function handleResponse(result, state, callback) { - // TODO: @Elias, should composeNextState get passed result OR result.data? - if (callback) return callback((0, _languageCommon.composeNextState)(state, result.data)); - return (0, _languageCommon.composeNextState)(state, result.data); + const { + data + } = result; + if (callback) return callback((0, _languageCommon.composeNextState)(state, data)); + return (0, _languageCommon.composeNextState)(state, data); } function prettyJson(data) { return JSON.stringify(data, null, 2); -} // ============================================================================= -// TODO: @Elias... what are these functions doing and do they have a place in the new implementation? -// export function getIndicesOf(string, regex) { -// var match, -// indexes = {}; -// regex = new RegExp(regex); -// while ((match = regex.exec(string))) { -// let schemaRef; -// if (!indexes[match[0]]) { -// indexes[match[0]] = {}; -// } -// let hrefString = string.slice( -// match.index, -// indexOf(string, '}', match.index) - 1 -// ); -// let lastIndex = lastIndexOf(hrefString, '/') + 1; -// schemaRef = trim(hrefString.slice(lastIndex)); -// indexes[match[0]][match.index] = schemaRef; -// } -// return indexes; -// } -// export function isLike(string, words) { -// const wordsArrary = words?.match(/([^\W]+[^\s,]*)/)?.splice(0, 1); -// const isFound = word => RegExp(word, 'i')?.test(string); -// return some(wordsArrary, isFound); -// } -// export const dhis2OperatorMap = { -// eq: eq, -// like: isLike, -// }; -// export function applyFilter( -// arrObject, -// targetProperty, -// operator, -// valueToCompareWith -// ) { -// if (targetProperty && operator && valueToCompareWith) { -// try { -// return filter(arrObject, obj => -// Reflect.apply(operator, obj, [obj[targetProperty], valueToCompareWith]) -// ); -// } catch (error) { -// Log.warn( -// `Returned unfiltered data. Failed to apply custom filter(${prettyJson({ -// targetProperty: targetProperty ?? null, -// operator: operator ?? null, -// value: valueToCompareWith ?? null, -// })}) on this collection. The operator you supplied maybe unsupported on this resource at the moment.` -// ); -// return arrObject; -// } -// } -// console.log('No filters applied; returned all records for this resource.'); -// return arrObject; -// } -// export function parseFilter(filterExpression) { -// const filterTokens = filterExpression?.split(':'); -// filterTokens -// ? (filterTokens[1] = dhis2OperatorMap[filterTokens[1] ?? null]) -// : null; -// return filterTokens; -// } -// // TODO: @Elias, end of the investigation block! -// ============================================================================= - +} const isArray = variable => !!variable && variable.constructor === Array; @@ -146,17 +78,16 @@ function generateUrl(configuration, options, resourceType) { return buildUrl(urlString, hostUrl, apiVersion); } -function buildUrlParams(options) { - var _options$params, _options$params2, _options$params3, _options$params4; - - const filters = options === null || options === void 0 ? void 0 : (_options$params = options.params) === null || _options$params === void 0 ? void 0 : _options$params.filters; - const dimensions = options === null || options === void 0 ? void 0 : (_options$params2 = options.params) === null || _options$params2 === void 0 ? void 0 : _options$params2.dimensions; // We remove filters and dimensions before building standard search params. +function buildUrlParams(params) { + const filters = params === null || params === void 0 ? void 0 : params.filters; + const dimensions = params === null || params === void 0 ? void 0 : params.dimensions; // We remove filters and dimensions before building standard search params. - options === null || options === void 0 ? true : (_options$params3 = options.params) === null || _options$params3 === void 0 ? true : delete _options$params3.filters; - options === null || options === void 0 ? true : (_options$params4 = options.params) === null || _options$params4 === void 0 ? true : delete _options$params4.dimensions; - const urlParams = new URLSearchParams(options === null || options === void 0 ? void 0 : options.params); // Then we re-apply the filters and dimensions in this dhis2-specific way. + params === null || params === void 0 ? true : delete params.filters; + params === null || params === void 0 ? true : delete params.dimensions; + const urlParams = new URLSearchParams(params); // Then we re-apply the filters and dimensions in this dhis2-specific way. filters === null || filters === void 0 ? void 0 : filters.map(f => urlParams.append('filter', f)); dimensions === null || dimensions === void 0 ? void 0 : dimensions.map(d => urlParams.append('dimension', d)); + console.log('after mapping', urlParams); return urlParams; } \ No newline at end of file diff --git a/src/Adaptor.js b/src/Adaptor.js index 3adc120..28383d8 100644 --- a/src/Adaptor.js +++ b/src/Adaptor.js @@ -124,24 +124,21 @@ axios.interceptors.response.use( * @function * @param {string} resourceType - Type of resource to create. E.g. `trackedEntityInstances`, `programs`, `events`, ... * @param {Object} data - 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 control the behavior of the `create` operation and to pass `import parameters` E.g. `{dryRun: true, importStrategy: CREATE}` See {@link https://docs.dhis2.org/2.34/en/dhis2_developer_manual/web-api.html DHIS2 API documentation} or {@link discover}..` Defaults to `{operationName: 'create', apiVersion: null, responseType: 'json'}` + * @param {Object} [options] - Optional `options` to define URL parameters (E.g. `filters`, `dimensions` and `import parameters`), axios configurations (E.g. `auth`) and DHIS 2 api version to use. * @param {function} [callback] - Optional callback to handle the response * @returns {Operation} - * * @example -a `program` * create('programs', { * name: 'name 20', * shortName: 'n20', * programType: 'WITHOUT_REGISTRATION', * }); - * * @example -an `event` * create('events', { * program: 'eBAyeGv0exc', * orgUnit: 'DiszpKrYNg8', * status: 'COMPLETED', * }); - * * @example -a `trackedEntityInstance` * create('trackedEntityInstances', { * orgUnit: 'TSyzvBiovKh', @@ -153,10 +150,8 @@ axios.interceptors.response.use( * }, * ] * }); - * * @example -a `dataSet` * create('dataSets', { name: 'OpenFn Data Set', periodType: 'Monthly' }); - * * @example -a `dataSetNotification` * create('dataSetNotificationTemplates', { * dataSetNotificationTrigger: 'DATA_SET_COMPLETION', @@ -166,7 +161,6 @@ axios.interceptors.response.use( * deliveryChannels: ['SMS'], * dataSets: [], * }); - * * @example -a `dataElement` * create('dataElements', { * aggregationType: 'SUM', @@ -175,13 +169,11 @@ axios.interceptors.response.use( * name: 'Paracetamol', * shortName: 'Para', * }); - * * @example -a `dataElementGroup` * create('dataElementGroups', { * name: 'Data Element Group 1', * dataElements: [], * }); - * * @example -a `dataElementGroupSet` * create('dataElementGroupSets', { * name: 'Data Element Group Set 4', @@ -189,7 +181,6 @@ axios.interceptors.response.use( * shortName: 'DEGS4', * dataElementGroups: [], * }); - * * @example -a `dataValueSet` * create('dataValueSets', { * dataElement: 'f7n9E0hX8qk', @@ -197,7 +188,6 @@ axios.interceptors.response.use( * orgUnit: 'DiszpKrYNg8', * value: '12', * }); - * * @example -a `dataValueSet` with related `dataValues` * create('dataValueSets', { * dataSet: 'pBOMPrpg1QX', @@ -219,7 +209,6 @@ axios.interceptors.response.use( * }, * ], * }); - * * @example -an `enrollment` * create('enrollments', { * trackedEntityInstance: 'bmshzEacgxa', @@ -237,12 +226,15 @@ export function create(resourceType, data, options, callback) { data = expandReferences(data)(state); options = expandReferences(options)(state); + const { params, requestConfig } = options || {}; const { configuration } = state; return request(configuration, { method: 'post', url: generateUrl(configuration, options, resourceType), + params: params && buildUrlParams(params), data: nestArray(data, resourceType), + ...requestConfig, }).then(result => { Log.success(`Created ${resourceType}: ${result.headers.location}`); return handleResponse(result, state, callback); @@ -258,7 +250,7 @@ export function create(resourceType, data, options, callback) { * @param {string} resourceType - The type of resource to be updated. E.g. `dataElements`, `organisationUnits`, etc. * @param {string} path - The `id` or `path` to the `object` to be updated. E.g. `FTRrcoaog83` or `FTRrcoaog83/{collection-name}/{object-id}` * @param {Object} data - Data to update. It requires to send `all required fields` or the `full body`. If you want `partial updates`, use `patch` operation. - * @param {{apiVersion: number,operationName: string,resourceType: string}} [options] - Optional options for update method. Defaults to `{operationName: 'update', apiVersion: state.configuration.apiVersion, responseType: 'json'}` + * @param {Object} [options] - Optional `options` to define URL parameters (E.g. `filters`, `dimensions` and `import parameters`), axios configurations (E.g. `auth`) and DHIS 2 api version to use. * @param {function} [callback] - Optional callback to handle the response * @returns {Operation} * @example -a program @@ -267,7 +259,6 @@ export function create(resourceType, data, options, callback) { * shortName: '14e1aa02', * programType: 'WITHOUT_REGISTRATION', * }); - * * @example an `event` * update('events', 'PVqUD2hvU4E', { * program: 'eBAyeGv0exc', @@ -276,7 +267,6 @@ export function create(resourceType, data, options, callback) { * storedBy: 'admin', * dataValues: [], * }); - * * @example a `trackedEntityInstance` * update('trackedEntityInstances', 'IeQfgUtGPq2', { * created: '2015-08-06T21:12:37.256', @@ -317,10 +307,8 @@ export function create(resourceType, data, options, callback) { * }, * ], * }); - * * @example -a `dataSet` * update('dataSets', 'lyLU2wR22tC', { name: 'OpenFN Data Set', periodType: 'Weekly' }); - * * @example -a `dataSetNotification` * update('dataSetNotificationTemplates', 'VbQBwdm1wVP', { * dataSetNotificationTrigger: 'DATA_SET_COMPLETION', @@ -330,7 +318,6 @@ export function create(resourceType, data, options, callback) { * deliveryChannels: ['SMS'], * dataSets: [], * }); - * * @example -a `dataElement` * update('dataElements', 'FTRrcoaog83', { * aggregationType: 'SUM', @@ -339,13 +326,11 @@ export function create(resourceType, data, options, callback) { * name: 'Paracetamol', * shortName: 'Para', * }); - * * @example -a `dataElementGroup` * update('dataElementGroups', 'QrprHT61XFk', { * name: 'Data Element Group 1', * dataElements: [], * }); - * * @example -a `dataElementGroupSet` * update('dataElementGroupSets', 'VxWloRvAze8', { * name: 'Data Element Group Set 4', @@ -353,7 +338,6 @@ export function create(resourceType, data, options, callback) { * shortName: 'DEGS4', * dataElementGroups: [], * }); - * * @example -a `dataValueSet` * update('dataValueSets', 'AsQj6cDsUq4', { * dataElement: 'f7n9E0hX8qk', @@ -361,7 +345,6 @@ export function create(resourceType, data, options, callback) { * orgUnit: 'DiszpKrYNg8', * value: '12', * }); - * * @example -a `dataValueSet` with related `dataValues` * update('dataValueSets', 'Ix2HsbDMLea', { * dataSet: 'pBOMPrpg1QX', @@ -383,7 +366,6 @@ export function create(resourceType, data, options, callback) { * }, * ], * }); - * * @example a single enrollment * update('enrollments', 'CmsHzercTBa' { * trackedEntityInstance: 'bmshzEacgxa', @@ -402,13 +384,15 @@ export function update(resourceType, path, data, options, callback) { data = expandReferences(data)(state); options = expandReferences(options)(state); + const { params, requestConfig } = options || {}; const { configuration } = state; return request(configuration, { method: 'put', url: `${generateUrl(configuration, options, resourceType)}/${path}`, - // TODO: @Elias, why no "nestArray" here? + params: params && buildUrlParams(params), data, + ...requestConfig, }).then(result => { Log.success(`Updated ${resourceType} at ${path}`); return handleResponse(result, state, callback); @@ -422,19 +406,17 @@ export function update(resourceType, path, data, options, callback) { * @public * @function * @param {string} resourceType - The type of resource to get(use its `plural` name). E.g. `dataElements`, `trackedEntityInstances`,`organisationUnits`, etc. - * @param {{apiVersion: number,operationName: string,responseType: string}}[options] - `Optional` options for `getData` operation. Defaults to `{operationName: 'getData', apiVersion: state.configuration.apiVersion, responseType: 'json'}`. + * @param {Object} [options] - Optional `options` to define URL parameters (E.g. `filters`, `dimensions` and `import parameters`), axios configurations (E.g. `auth`) and DHIS 2 api version to use. * @param {function} [callback] - Optional callback to handle the response * @returns {Operation} state * @example Example getting one `trackedEntityInstance` with `Id` 'dNpxRu1mWG5' for a given `orgUnit(DiszpKrYNg8)` - * getData('trackedEntityInstances', { + * get('trackedEntityInstances', { * fields: '*', * ou: 'DiszpKrYNg8', * entityType: 'nEenWmSyUEp', * trackedEntityInstance: 'dNpxRu1mWG5', * }); */ -// TODO: @Elias, I'm not sure "options" is the right name for the second arg here. -// Isn't this more like the filters that you're using to find the right resources? export function get(resourceType, options, callback) { return state => { console.log(`Preparing get operation...`); @@ -442,13 +424,15 @@ export function get(resourceType, options, callback) { resourceType = expandReferences(resourceType)(state); options = expandReferences(options)(state); + const { params, requestConfig } = options || {}; const { configuration } = state; return request(configuration, { method: 'get', url: generateUrl(configuration, options, resourceType), - params: buildUrlParams(options), + params: params && buildUrlParams(params), responseType: 'json', + ...requestConfig, }).then(result => { Log.success( `Retrieved ${result.data[resourceType].length} ${resourceType}.` @@ -480,10 +464,6 @@ export function get(resourceType, options, callback) { * state.data, * { ou: 'TSyzvBiovKh' } * ); - * @todo Tweak/refine to mimic implementation based on the following inspiration: {@link https://sqlite.org/lang_upsert.html sqlite upsert} and {@link https://wiki.postgresql.org/wiki/UPSERT postgresql upsert} - * @todo Test implementation for upserting metadata - * @todo Test implementation for upserting data values - * @todo Implement the updateCondition */ export function upsert(resourceType, data, options, callback) { return state => { @@ -546,8 +526,9 @@ export function discover(httpMethod, endpoint) { if (param.schema['$ref']) { let schemaRefIndex = param.schema['$ref'].lastIndexOf('/') + 1; - let schemaRef = - param.schema['$ref'].slice(schemaRefIndex); + let schemaRef = param.schema['$ref'].slice( + schemaRefIndex + ); param.schema = tempData.components.schemas[schemaRef]; } @@ -626,6 +607,7 @@ export function discover(httpMethod, endpoint) { * }); */ // TODO: @Elias, can this be deleted in favor of update? How does DHIS2 handle PATCH vs PUT? +// I need to investigate on this. But I think DHIS 2 forces to send all properties back when we do an update. If that's confirmed then this may be needed. export function patch(resourceType, path, data, params, options, callback) { return state => { resourceType = expandReferences(resourceType)(state); diff --git a/src/Client.js b/src/Client.js index 309a8cf..8decb35 100644 --- a/src/Client.js +++ b/src/Client.js @@ -1,11 +1,21 @@ import axios from 'axios'; +/** + * The request client takes configuration from state and an axios request object + * then (1) logs the method and URL, (2) applies standard headers and auth + * before spreading the rest of the axios configuration, and (3) executes an + * axios request. + * @function + * @param {object} configuration - configuration must have a username and password + * @param {object} axiosRequest - the axiosRequest contains valid axios params: https://axios-http.com/docs/req_config + * @returns {Promise} a promise that will resolve to either a response object or an error object. + */ export function request({ username, password }, axiosRequest) { const { method, url } = axiosRequest; console.log(`Sending ${method} request to ${url}`); - return axios.request({ + return axios({ headers: { 'Content-Type': 'application/json' }, auth: { username, password }, // Note that providing headers or auth in the request object will overwrite. diff --git a/src/Utils.js b/src/Utils.js index 6876d78..b99e5eb 100644 --- a/src/Utils.js +++ b/src/Utils.js @@ -1,5 +1,3 @@ -import axios from 'axios'; -import { eq, filter, some, indexOf, lastIndexOf, trim } from 'lodash'; import { composeNextState } from '@openfn/language-common'; export const CONTENT_TYPES = { @@ -30,87 +28,15 @@ export function buildUrl(path, hostUrl, apiVersion) { } export function handleResponse(result, state, callback) { - // TODO: @Elias, should composeNextState get passed result OR result.data? - if (callback) return callback(composeNextState(state, result.data)); - return composeNextState(state, result.data); + const { data } = result; + if (callback) return callback(composeNextState(state, data)); + return composeNextState(state, data); } export function prettyJson(data) { return JSON.stringify(data, null, 2); } -// ============================================================================= -// TODO: @Elias... what are these functions doing and do they have a place in the new implementation? -// export function getIndicesOf(string, regex) { -// var match, -// indexes = {}; - -// regex = new RegExp(regex); - -// while ((match = regex.exec(string))) { -// let schemaRef; -// if (!indexes[match[0]]) { -// indexes[match[0]] = {}; -// } -// let hrefString = string.slice( -// match.index, -// indexOf(string, '}', match.index) - 1 -// ); -// let lastIndex = lastIndexOf(hrefString, '/') + 1; -// schemaRef = trim(hrefString.slice(lastIndex)); -// indexes[match[0]][match.index] = schemaRef; -// } - -// return indexes; -// } - -// export function isLike(string, words) { -// const wordsArrary = words?.match(/([^\W]+[^\s,]*)/)?.splice(0, 1); -// const isFound = word => RegExp(word, 'i')?.test(string); -// return some(wordsArrary, isFound); -// } - -// export const dhis2OperatorMap = { -// eq: eq, -// like: isLike, -// }; - -// export function applyFilter( -// arrObject, -// targetProperty, -// operator, -// valueToCompareWith -// ) { -// if (targetProperty && operator && valueToCompareWith) { -// try { -// return filter(arrObject, obj => -// Reflect.apply(operator, obj, [obj[targetProperty], valueToCompareWith]) -// ); -// } catch (error) { -// Log.warn( -// `Returned unfiltered data. Failed to apply custom filter(${prettyJson({ -// targetProperty: targetProperty ?? null, -// operator: operator ?? null, -// value: valueToCompareWith ?? null, -// })}) on this collection. The operator you supplied maybe unsupported on this resource at the moment.` -// ); -// return arrObject; -// } -// } -// console.log('No filters applied; returned all records for this resource.'); -// return arrObject; -// } - -// export function parseFilter(filterExpression) { -// const filterTokens = filterExpression?.split(':'); -// filterTokens -// ? (filterTokens[1] = dhis2OperatorMap[filterTokens[1] ?? null]) -// : null; -// return filterTokens; -// } -// // TODO: @Elias, end of the investigation block! -// ============================================================================= - const isArray = variable => !!variable && variable.constructor === Array; export function nestArray(data, key) { @@ -134,19 +60,21 @@ export function generateUrl(configuration, options, resourceType) { return buildUrl(urlString, hostUrl, apiVersion); } -export function buildUrlParams(options) { - const filters = options?.params?.filters; - const dimensions = options?.params?.dimensions; +export function buildUrlParams(params) { + const filters = params?.filters; + const dimensions = params?.dimensions; // We remove filters and dimensions before building standard search params. - delete options?.params?.filters; - delete options?.params?.dimensions; + delete params?.filters; + delete params?.dimensions; - const urlParams = new URLSearchParams(options?.params); + const urlParams = new URLSearchParams(params); // Then we re-apply the filters and dimensions in this dhis2-specific way. filters?.map(f => urlParams.append('filter', f)); dimensions?.map(d => urlParams.append('dimension', d)); + console.log('after mapping', urlParams); + return urlParams; } diff --git a/test/index.js b/test/index.js index d1376b1..d896be7 100644 --- a/test/index.js +++ b/test/index.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { execute, create, update } from '../lib/Adaptor'; import { dataValue } from '@openfn/language-common'; -import { buildUrl, nestArray } from '../lib/Utils'; +import { buildUrl, buildUrlParams, nestArray } from '../lib/Utils'; import nock from 'nock'; const testServer = nock('https://play.dhis2.org/2.36.4'); @@ -184,6 +184,31 @@ describe('buildUrl', () => { }); }); +describe('generateURL', () => { + it('should generate a URL properly given ________________'), + () => { + expect(1).to.eql(2); + }; +}); + +describe.only('buildURLParams', () => { + it.only('should handle special filter and dimensions params and build the rest per usual', () => { + const params = { + dryRun: true, + filters: ['sex:eq:male', 'origin:eq:senegal'], + someNonesense: 'other', + dimensions: ['dx:fbfJHSPpUQD', 'ou:O6uvpzGd5pu;lc3eMKXaEfw'], + }; + + const finalParams = buildUrlParams(params).toString(); + + const expected = + 'dryRun=true&someNonesense=other&filter=sex%3Aeq%3Amale&filter=origin%3Aeq%3Asenegal&dimension=dx%3AfbfJHSPpUQD&dimension=ou%3AO6uvpzGd5pu%3Blc3eMKXaEfw'; + + expect(finalParams).to.eql(expected); + }); +}); + describe('nestArray', () => { it('when an array is passed it gets nested inside that "entity" key', async () => { const state = { From fc6673e5e2396bce315a3b63b25f8d2e97875db6 Mon Sep 17 00:00:00 2001 From: "Elias W. BA" Date: Tue, 21 Dec 2021 15:41:45 +0000 Subject: [PATCH 10/26] Test buildURL, generateURL and buildURLParams --- lib/Utils.js | 1 - src/Utils.js | 2 - test/index.js | 135 +++++++++++++++++++++++++++++++++++++------------- 3 files changed, 100 insertions(+), 38 deletions(-) diff --git a/lib/Utils.js b/lib/Utils.js index 66ce6d4..7eb089c 100644 --- a/lib/Utils.js +++ b/lib/Utils.js @@ -88,6 +88,5 @@ function buildUrlParams(params) { filters === null || filters === void 0 ? void 0 : filters.map(f => urlParams.append('filter', f)); dimensions === null || dimensions === void 0 ? void 0 : dimensions.map(d => urlParams.append('dimension', d)); - console.log('after mapping', urlParams); return urlParams; } \ No newline at end of file diff --git a/src/Utils.js b/src/Utils.js index b99e5eb..3d42016 100644 --- a/src/Utils.js +++ b/src/Utils.js @@ -74,7 +74,5 @@ export function buildUrlParams(params) { filters?.map(f => urlParams.append('filter', f)); dimensions?.map(d => urlParams.append('dimension', d)); - console.log('after mapping', urlParams); - return urlParams; } diff --git a/test/index.js b/test/index.js index d896be7..fdf5ab7 100644 --- a/test/index.js +++ b/test/index.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { execute, create, update } from '../lib/Adaptor'; import { dataValue } from '@openfn/language-common'; -import { buildUrl, buildUrlParams, nestArray } from '../lib/Utils'; +import { buildUrl, buildUrlParams, generateUrl, nestArray } from '../lib/Utils'; import nock from 'nock'; const testServer = nock('https://play.dhis2.org/2.36.4'); @@ -163,49 +163,114 @@ describe('UPDATE', () => { }); }); -describe('buildUrl', () => { - it('the proper URL gets built from the "entity" string and the config', async () => { - const state = { - configuration: { - username: 'admin', - password: 'district', - hostUrl: 'https://dhis2.moh.gov', - apiVersion: '2.36.4', - }, +describe.only('URL builders', () => { + const fixture = {}; + + before(done => { + fixture.configuration = { + username: 'admin', + password: 'district', + hostUrl: 'https://play.dhis2.org/2.36.4', }; + fixture.options = {}; + fixture.resourceType = 'dataValueSets'; + done(); + }); + + describe.only('buildUrl', () => { + it.only('the proper URL gets built from the "entity" string and the config', done => { + const configuration = { ...fixture.configuration, apiVersion: 33 }; + + const finalURL = buildUrl( + '/' + 'events', + configuration.hostUrl, + configuration.apiVersion + ); + + const expectedURL = 'https://play.dhis2.org/2.36.4/api/33/events'; - const url = buildUrl( - '/' + 'events', - state.configuration.hostUrl, - state.configuration.apiVersion - ); + expect(finalURL).to.eq(expectedURL); - expect(url).to.eql('https://dhis2.moh.gov/api/2.36.4/events'); + done(); + }); }); -}); -describe('generateURL', () => { - it('should generate a URL properly given ________________'), - () => { - expect(1).to.eql(2); - }; -}); + describe.only('generateURL', () => { + it.only('should generate basic URL', done => { + const finalURL = generateUrl( + fixture.configuration, + fixture.options, + fixture.resourceType + ); + const expectedURL = 'https://play.dhis2.org/2.36.4/api/dataValueSets'; -describe.only('buildURLParams', () => { - it.only('should handle special filter and dimensions params and build the rest per usual', () => { - const params = { - dryRun: true, - filters: ['sex:eq:male', 'origin:eq:senegal'], - someNonesense: 'other', - dimensions: ['dx:fbfJHSPpUQD', 'ou:O6uvpzGd5pu;lc3eMKXaEfw'], - }; + expect(finalURL).to.eq(expectedURL); + done(); + }); + + it.only('should generate URL with specific api version from configuration', done => { + const configuration = { ...fixture.configuration, apiVersion: 33 }; - const finalParams = buildUrlParams(params).toString(); + const finalURL = generateUrl( + configuration, + fixture.options, + fixture.resourceType + ); + const expectedURL = `https://play.dhis2.org/2.36.4/api/${configuration.apiVersion}/dataValueSets`; + + expect(finalURL).to.eq(expectedURL); + done(); + }); + + it.only('should generate URL with specific api version from options', done => { + const options = { ...fixture.options, apiVersion: 33 }; + + const finalURL = generateUrl( + fixture.configuration, + options, + fixture.resourceType + ); + const expectedURL = 'https://play.dhis2.org/2.36.4/api/33/dataValueSets'; + + expect(finalURL).to.eq(expectedURL); + done(); + }); - const expected = - 'dryRun=true&someNonesense=other&filter=sex%3Aeq%3Amale&filter=origin%3Aeq%3Asenegal&dimension=dx%3AfbfJHSPpUQD&dimension=ou%3AO6uvpzGd5pu%3Blc3eMKXaEfw'; + it.only('should generate URL without caring about other options', done => { + const options = { + ...fixture.options, + apiVersion: 33, + params: { filters: ['a:eq:b', 'c:ge:d'] }, + }; - expect(finalParams).to.eql(expected); + const finalURL = generateUrl( + fixture.configuration, + options, + fixture.resourceType + ); + const expectedURL = 'https://play.dhis2.org/2.36.4/api/33/dataValueSets'; + + expect(finalURL).to.eq(expectedURL); + done(); + }); + }); + + describe.only('buildURLParams', () => { + it.only('should handle special filter and dimensions params and build the rest per usual', () => { + const params = { + dryRun: true, + filters: ['sex:eq:male', 'origin:eq:senegal'], + someNonesense: 'other', + dimensions: ['dx:fbfJHSPpUQD', 'ou:O6uvpzGd5pu;lc3eMKXaEfw'], + }; + + const finalParams = buildUrlParams(params).toString(); + + const expected = + 'dryRun=true&someNonesense=other&filter=sex%3Aeq%3Amale&filter=origin%3Aeq%3Asenegal&dimension=dx%3AfbfJHSPpUQD&dimension=ou%3AO6uvpzGd5pu%3Blc3eMKXaEfw'; + + expect(finalParams).to.eq(expected); + }); }); }); From 2e3e3585096050ab77e75d53083897abef3b1e20 Mon Sep 17 00:00:00 2001 From: Taylor Downs Date: Tue, 21 Dec 2021 09:23:56 -0700 Subject: [PATCH 11/26] include params in the client --- lib/Client.js | 4 +++- src/Client.js | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/Client.js b/lib/Client.js index c40bd7b..f7ade87 100644 --- a/lib/Client.js +++ b/lib/Client.js @@ -25,9 +25,11 @@ function request({ }, axiosRequest) { const { method, - url + url, + params } = axiosRequest; console.log(`Sending ${method} request to ${url}`); + if (params) console.log(` with params: ${params}`); return (0, _axios.default)({ headers: { 'Content-Type': 'application/json' diff --git a/src/Client.js b/src/Client.js index 8decb35..50b137b 100644 --- a/src/Client.js +++ b/src/Client.js @@ -11,9 +11,10 @@ import axios from 'axios'; * @returns {Promise} a promise that will resolve to either a response object or an error object. */ export function request({ username, password }, axiosRequest) { - const { method, url } = axiosRequest; + const { method, url, params } = axiosRequest; console.log(`Sending ${method} request to ${url}`); + if (params) console.log(` with params: ${params}`); return axios({ headers: { 'Content-Type': 'application/json' }, From 0e7a4930099fbdf609325c84f0c113b3187b7b7c Mon Sep 17 00:00:00 2001 From: Taylor Downs Date: Tue, 21 Dec 2021 11:44:30 -0700 Subject: [PATCH 12/26] update client for redirects --- lib/Client.js | 6 +++++- src/Client.js | 7 +++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/Client.js b/lib/Client.js index f7ade87..d99c1aa 100644 --- a/lib/Client.js +++ b/lib/Client.js @@ -29,11 +29,15 @@ function request({ params } = axiosRequest; console.log(`Sending ${method} request to ${url}`); - if (params) console.log(` with params: ${params}`); + if (params) console.log(` with params: ${params}`); // NOTE: We don't follow redirects for unsafe methods: https://github.com/axios/axios/issues/2460 + + const safeRedirect = ['get', 'head', 'options', 'trace'].includes(method.toLowerCase()); return (0, _axios.default)({ headers: { 'Content-Type': 'application/json' }, + responseType: 'json', + maxRedirects: safeRedirect ? 5 : 0, auth: { username, password diff --git a/src/Client.js b/src/Client.js index 50b137b..dcbee31 100644 --- a/src/Client.js +++ b/src/Client.js @@ -16,8 +16,15 @@ export function request({ username, password }, axiosRequest) { console.log(`Sending ${method} request to ${url}`); if (params) console.log(` with params: ${params}`); + // NOTE: We don't follow redirects for unsafe methods: https://github.com/axios/axios/issues/2460 + const safeRedirect = ['get', 'head', 'options', 'trace'].includes( + method.toLowerCase() + ); + return axios({ headers: { 'Content-Type': 'application/json' }, + responseType: 'json', + maxRedirects: safeRedirect ? 5 : 0, auth: { username, password }, // Note that providing headers or auth in the request object will overwrite. ...axiosRequest, From f5fb8176c1b71260a321ac16f46fadb48247b7ff Mon Sep 17 00:00:00 2001 From: Taylor Downs Date: Tue, 21 Dec 2021 19:21:03 +0000 Subject: [PATCH 13/26] proposal for get/upsert --- ast.json | 12 +++++++++++- lib/Adaptor.js | 12 ++++++++---- src/Adaptor.js | 15 ++++++++------- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/ast.json b/ast.json index 6ef1bf1..66ebb6d 100644 --- a/ast.json +++ b/ast.json @@ -274,6 +274,7 @@ "name": "get", "params": [ "resourceType", + "filters", "options", "callback" ], @@ -301,7 +302,16 @@ }, { "title": "param", - "description": "Optional `options` to define URL parameters (E.g. `filters`, `dimensions` and `import parameters`), axios configurations (E.g. `auth`) and DHIS 2 api version to use.", + "description": "Filters to limit what resources are retrieved.", + "type": { + "type": "NameExpression", + "name": "Object" + }, + "name": "filters" + }, + { + "title": "param", + "description": "Optional `options` to define URL parameters beyond filters, request configuration (e.g. `auth`) and DHIS2 api version to use.", "type": { "type": "OptionalType", "expression": { diff --git a/lib/Adaptor.js b/lib/Adaptor.js index 0bfea6f..a8257df 100644 --- a/lib/Adaptor.js +++ b/lib/Adaptor.js @@ -492,7 +492,8 @@ function update(resourceType, path, data, options, callback) { * @public * @function * @param {string} resourceType - The type of resource to get(use its `plural` name). E.g. `dataElements`, `trackedEntityInstances`,`organisationUnits`, etc. - * @param {Object} [options] - Optional `options` to define URL parameters (E.g. `filters`, `dimensions` and `import parameters`), axios configurations (E.g. `auth`) and DHIS 2 api version to use. + * @param {Object} filters - Filters to limit what resources are retrieved. + * @param {Object} [options] - Optional `options` to define URL parameters 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 Example getting one `trackedEntityInstance` with `Id` 'dNpxRu1mWG5' for a given `orgUnit(DiszpKrYNg8)` @@ -505,10 +506,11 @@ function update(resourceType, path, data, options, callback) { */ -function get(resourceType, options, callback) { +function get(resourceType, filters, options, callback) { return state => { console.log(`Preparing get operation...`); resourceType = (0, _languageCommon.expandReferences)(resourceType)(state); + filters = (0, _languageCommon.expandReferences)(filters)(state); options = (0, _languageCommon.expandReferences)(options)(state); const { params, @@ -520,7 +522,9 @@ function get(resourceType, options, callback) { return (0, _Client.request)(configuration, { method: 'get', url: (0, _Utils.generateUrl)(configuration, options, resourceType), - params: params && (0, _Utils.buildUrlParams)(params), + params: (0, _Utils.buildUrlParams)({ ...filters, + ...params + }), responseType: 'json', ...requestConfig }).then(result => { @@ -558,7 +562,7 @@ function get(resourceType, options, callback) { function upsert(resourceType, data, options, callback) { return state => { console.log(`Preparing upsert via 'get' then 'create' OR 'update'...`); - return get(resourceType, options)(state).then(resp => { + return get(resourceType, data)(state).then(resp => { const resources = resp.data[resourceType]; if (resources.length > 1) { diff --git a/src/Adaptor.js b/src/Adaptor.js index 28383d8..f42eb5e 100644 --- a/src/Adaptor.js +++ b/src/Adaptor.js @@ -406,7 +406,8 @@ export function update(resourceType, path, data, options, callback) { * @public * @function * @param {string} resourceType - The type of resource to get(use its `plural` name). E.g. `dataElements`, `trackedEntityInstances`,`organisationUnits`, etc. - * @param {Object} [options] - Optional `options` to define URL parameters (E.g. `filters`, `dimensions` and `import parameters`), axios configurations (E.g. `auth`) and DHIS 2 api version to use. + * @param {Object} filters - Filters to limit what resources are retrieved. + * @param {Object} [options] - Optional `options` to define URL parameters 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 Example getting one `trackedEntityInstance` with `Id` 'dNpxRu1mWG5' for a given `orgUnit(DiszpKrYNg8)` @@ -417,11 +418,12 @@ export function update(resourceType, path, data, options, callback) { * trackedEntityInstance: 'dNpxRu1mWG5', * }); */ -export function get(resourceType, options, callback) { +export function get(resourceType, filters, options, callback) { return state => { console.log(`Preparing get operation...`); resourceType = expandReferences(resourceType)(state); + filters = expandReferences(filters)(state); options = expandReferences(options)(state); const { params, requestConfig } = options || {}; @@ -430,7 +432,7 @@ export function get(resourceType, options, callback) { return request(configuration, { method: 'get', url: generateUrl(configuration, options, resourceType), - params: params && buildUrlParams(params), + params: buildUrlParams({ ...filters, ...params }), responseType: 'json', ...requestConfig, }).then(result => { @@ -471,7 +473,7 @@ export function upsert(resourceType, data, options, callback) { return get( resourceType, - options + data )(state).then(resp => { const resources = resp.data[resourceType]; if (resources.length > 1) { @@ -526,9 +528,8 @@ export function discover(httpMethod, endpoint) { if (param.schema['$ref']) { let schemaRefIndex = param.schema['$ref'].lastIndexOf('/') + 1; - let schemaRef = param.schema['$ref'].slice( - schemaRefIndex - ); + let schemaRef = + param.schema['$ref'].slice(schemaRefIndex); param.schema = tempData.components.schemas[schemaRef]; } From 2f4eeda4bf0dc4f85862ddbf77221be9e148706b Mon Sep 17 00:00:00 2001 From: Taylor Downs Date: Tue, 21 Dec 2021 19:21:57 +0000 Subject: [PATCH 14/26] prerelease version bump --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index c50dfcf..fbb21a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@openfn/language-dhis2", - "version": "3.0.0-0", + "version": "3.0.0-1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 43f67b6..0bf2186 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@openfn/language-dhis2", - "version": "3.0.0-0", + "version": "3.0.0-1", "description": "DHIS2 Language Pack for OpenFn", "homepage": "https://docs.openfn.org", "repository": { From 7ec750811595af60e65f678dec8a5e9ccddf7dd2 Mon Sep 17 00:00:00 2001 From: "Elias W. BA" Date: Tue, 21 Dec 2021 21:11:58 +0000 Subject: [PATCH 15/26] Integration tests - part 1 --- lib/Adaptor.js | 97 +++--- lib/Utils.js | 6 +- src/Utils.js | 2 +- test/integration.js | 795 ++++++++++++++++++++------------------------ 4 files changed, 412 insertions(+), 488 deletions(-) diff --git a/lib/Adaptor.js b/lib/Adaptor.js index 0bfea6f..bc845fc 100644 --- a/lib/Adaptor.js +++ b/lib/Adaptor.js @@ -144,58 +144,51 @@ function configMigrationHelper(state) { return state; } // NOTE: In order to prevent unintended exposure of authentication information // in the logs, we make use of an axios interceptor. +// axios.interceptors.response.use( +// function (response) { +// const contentType = response.headers['content-type']?.split(';')[0]; +// const acceptHeaders = response.config.headers['Accept'] +// .split(';')[0] +// .split(','); +// if (response.config.method === 'get') { +// if (indexOf(acceptHeaders, contentType) === -1) { +// const newError = { +// status: 404, +// message: 'Unexpected content returned', +// responseData: response.data, +// }; +// Log.error(newError.message); +// return Promise.reject(newError); +// } +// } +// if ( +// typeof response?.data === 'string' && +// contentType === CONTENT_TYPES?.json +// ) { +// try { +// response = { ...response, data: JSON.parse(response.data) }; +// } catch (error) { +// Log.warn('Non-JSON response detected, unable to parse.'); +// } +// } +// return response; +// }, +// function (error) { +// try { +// const details = error.toJSON(); +// if (details?.config?.auth) details.config.auth = '--REDACTED--'; +// if (details?.config?.data) details.config.data = '--REDACTED--'; +// Log.error(details.message); +// return Promise.reject(details); +// } catch (e) { +// // TODO: @Elias, why does this error sometimes already appear to be JSONified? +// // console.log(e) // "not JSONABLE TypeError: error.toJSON is not a function" +// Log.error(error.message); +// return Promise.reject(error); +// } +// } +// ); - -_axios.default.interceptors.response.use(function (response) { - var _response$headers$con, _response; - - const contentType = (_response$headers$con = response.headers['content-type']) === null || _response$headers$con === void 0 ? void 0 : _response$headers$con.split(';')[0]; - const acceptHeaders = response.config.headers['Accept'].split(';')[0].split(','); - - if (response.config.method === 'get') { - if ((0, _lodash.indexOf)(acceptHeaders, contentType) === -1) { - const newError = { - status: 404, - message: 'Unexpected content returned', - responseData: response.data - }; - - _Utils.Log.error(newError.message); - - return Promise.reject(newError); - } - } - - if (typeof ((_response = response) === null || _response === void 0 ? void 0 : _response.data) === 'string' && contentType === (_Utils.CONTENT_TYPES === null || _Utils.CONTENT_TYPES === void 0 ? void 0 : _Utils.CONTENT_TYPES.json)) { - try { - response = { ...response, - data: JSON.parse(response.data) - }; - } catch (error) { - _Utils.Log.warn('Non-JSON response detected, unable to parse.'); - } - } - - return response; -}, function (error) { - try { - var _details$config, _details$config2; - - const details = error.toJSON(); - if (details === null || details === void 0 ? void 0 : (_details$config = details.config) === null || _details$config === void 0 ? void 0 : _details$config.auth) details.config.auth = '--REDACTED--'; - if (details === null || details === void 0 ? void 0 : (_details$config2 = details.config) === null || _details$config2 === void 0 ? void 0 : _details$config2.data) details.config.data = '--REDACTED--'; - - _Utils.Log.error(details.message); - - return Promise.reject(details); - } catch (e) { - // TODO: @Elias, why does this error sometimes already appear to be JSONified? - // console.log(e) // "not JSONABLE TypeError: error.toJSON is not a function" - _Utils.Log.error(error.message); - - return Promise.reject(error); - } -}); /** * Create a record * @public @@ -321,6 +314,8 @@ function create(resourceType, data, options, callback) { _Utils.Log.success(`Created ${resourceType}: ${result.headers.location}`); return (0, _Utils.handleResponse)(result, state, callback); + }).catch(error => { + console.log('ERROR', error); }); }; } diff --git a/lib/Utils.js b/lib/Utils.js index 7eb089c..c9f0d0f 100644 --- a/lib/Utils.js +++ b/lib/Utils.js @@ -42,7 +42,8 @@ exports.Log = Log; function buildUrl(path, hostUrl, apiVersion) { const pathSuffix = apiVersion ? `/${apiVersion}${path}` : `${path}`; return hostUrl + '/api' + pathSuffix; -} +} // Write a unit test for this one + function handleResponse(result, state, callback) { const { @@ -71,8 +72,7 @@ function generateUrl(configuration, options, resourceType) { } = configuration; const urlString = '/' + resourceType; // Note that users can override the apiVersion from configuration with args - if (options === null || options === void 0 ? void 0 : options.apiVersion) apiVersion = options.apiVersion; // TODO: discuss how this actually works on DHIS2. I'm not sure I'm following. - + if (options === null || options === void 0 ? void 0 : options.apiVersion) apiVersion = options.apiVersion; const apiMessage = apiVersion ? `Using DHIS2 api version ${apiVersion}` : 'Using latest available version of the DHIS2 api on this server.'; console.log(apiMessage); return buildUrl(urlString, hostUrl, apiVersion); diff --git a/src/Utils.js b/src/Utils.js index 3d42016..fd97c81 100644 --- a/src/Utils.js +++ b/src/Utils.js @@ -27,6 +27,7 @@ export function buildUrl(path, hostUrl, apiVersion) { return hostUrl + '/api' + pathSuffix; } +// Write a unit test for this one export function handleResponse(result, state, callback) { const { data } = result; if (callback) return callback(composeNextState(state, data)); @@ -50,7 +51,6 @@ export function generateUrl(configuration, options, resourceType) { // Note that users can override the apiVersion from configuration with args if (options?.apiVersion) apiVersion = options.apiVersion; - // TODO: discuss how this actually works on DHIS2. I'm not sure I'm following. const apiMessage = apiVersion ? `Using DHIS2 api version ${apiVersion}` : 'Using latest available version of the DHIS2 api on this server.'; diff --git a/test/integration.js b/test/integration.js index 9ff4e48..03493b7 100644 --- a/test/integration.js +++ b/test/integration.js @@ -3,13 +3,19 @@ const { create, execute, get, update } = require('../src/Adaptor'); const crypto = require('crypto'); const { upsert } = require('../lib/Adaptor'); -const getRandomOrganisationUnitPayload = user => { - const name = crypto.randomBytes(16).toString('hex'); - const shortName = name.substring(0, 5); - const displayName = name; - const openingDate = new Date().toISOString(); - return { name, shortName, displayName, openingDate, users: [user] }; -}; +// const getRandomOrganisationUnitPayload = user => { +// const name = crypto.randomBytes(16).toString('hex'); +// const shortName = name.substring(0, 5); +// const displayName = name; +// const openingDate = new Date().toISOString(); +// return { name, shortName, displayName, openingDate, users: [user] }; +// }; + +// const getRandomProgramStagePayload = program => { +// const name = crypto.randomBytes(16).toString('hex'); +// const displayName = name; +// return { name, displayName, program }; +// }; const getRandomProgramPayload = () => { const name = crypto.randomBytes(16).toString('hex'); @@ -18,468 +24,391 @@ const getRandomProgramPayload = () => { return { name, shortName, programType }; }; -const getRandomProgramStagePayload = program => { - const name = crypto.randomBytes(16).toString('hex'); - const displayName = name; - return { name, displayName, program }; -}; - -const globalState = { - configuration: { - username: 'admin', - password: 'district', - hostUrl: 'https://play.dhis2.org/2.36.4', - }, - program: 'IpHINAT79UW', - organisationUnit: 'DiszpKrYNg8', - dataSet: 'pBOMPrpg1QX', - trackedEntityInstance: 'bmshzEacgxa', - programStage: 'A03MvHHogjR', - dataElement: 'Ix2HsbDMLea', - enrollment: 'CmsHzercTBa', -}; - -describe('create', () => { - it('should create an event program', async () => { - const state = { - ...globalState, - data: { program: getRandomProgramPayload() }, - }; +describe('Integration tests', () => { + const fixture = {}; - const response = await execute( - create('programs', state => state.data.program) - )(state); - expect({ - httpStatus: response.data.httpStatus, - httpStatusCode: response.data.httpStatusCode, - status: response.data.status, - }).to.eql({ - httpStatus: 'Created', - httpStatusCode: 201, - status: 'OK', - }); - }); - - it('should create a single event', async () => { - const state = { - ...globalState, - data: { - program: 'eBAyeGv0exc', - orgUnit: 'DiszpKrYNg8', - status: 'COMPLETED', + before(done => { + fixture.initialState = { + configuration: { + username: 'admin', + password: 'district', + hostUrl: 'https://play.dhis2.org/2.36.6', }, }; - const response = await execute(create('events', state => state.data))( - state - ); - globalState.event = response.data.response.uid; - expect({ - httpStatus: response.data.httpStatus, - httpStatusCode: response.data.httpStatusCode, - status: response.data.status, - }).to.eql({ - httpStatus: 'OK', - httpStatusCode: 200, - status: 'OK', - }); + done(); }); - it('should create a single tracked entity instance', async () => { - const state = { - ...globalState, - data: { - orgUnit: globalState.organisationUnit, - trackedEntityType: 'nEenWmSyUEp', - attributes: [ - { - attribute: 'w75KJ2mc4zz', - value: 'Gigiwe', - }, - ], - }, - }; - const response = await execute( - create('trackedEntityInstances', state => state.data) - )(state); - globalState.trackedEntityInstance = - response.data.response.importSummaries[0].reference; - expect({ - httpStatus: response.data.httpStatus, - httpStatusCode: response.data.httpStatusCode, - status: response.data.status, - }).to.eql({ - httpStatus: 'OK', - httpStatusCode: 200, - status: 'OK', + describe('create', () => { + it('should create an event program', async () => { + const state = { + ...fixture.initialState, + data: { program: getRandomProgramPayload() }, + }; + + const finalState = await execute( + create('programs', state => state.data.program) + )(state); + + expect(finalState.data.status).to.eq('OK'); }); - }); - it('should create a single dataValueSet', async () => { - const state = { - ...globalState, - data: { - dataElement: 'f7n9E0hX8qk', - period: '201401', - orgUnit: globalState.organisationUnit, - value: '12', - }, - }; + it('should create a single event', async () => { + const state = { + ...fixture.initialState, + data: { + program: 'eBAyeGv0exc', + orgUnit: 'DiszpKrYNg8', + status: 'COMPLETED', + }, + }; - const response = await execute( - create('dataValueSets', state => state.data) - )(state); - expect({ status: response.data.status }).to.eql({ status: 'SUCCESS' }); - }); + const finalState = await execute(create('events', state => state.data))( + state + ); - it('should create a set of related data values sharing the same period and organisation unit', async () => { - const state = { - ...globalState, - data: { - dataSet: globalState.dataSet, - completeDate: '2014-02-03', - period: '201401', - orgUnit: globalState.organisationUnit, - dataValues: [ - { - dataElement: 'f7n9E0hX8qk', - value: '1', - }, - { - dataElement: 'Ix2HsbDMLea', - value: '2', - }, - { - dataElement: 'eY5ehpbEsB7', - value: '3', - }, - ], - }, - }; + console.log('FINAL STATE', finalState); - const response = await execute( - create('dataValueSets', state => state.data) - )(state); - expect({ status: response.data.status }).to.eql({ status: 'SUCCESS' }); - }); + expect(finalState.data.status).to.eq('OK'); + }); - // it('should create a single enrollment of a trackedEntityInstance into a given program', async () => { - // const state = { - // ...globalState, - // data: { - // trackedEntityInstance: globalState.trackedEntityInstance, - // orgUnit: globalState.organisationUnit, - // program: globalState.program, - // enrollmentDate: new Date().toISOString().split('T')[0], - // incidentDate: new Date().toISOString().split('T')[0], - // }, - // }; - - // const response = await execute(create('enrollments', state => state.data))( - // state - // ); - // expect({ status: response.data.status }).to.eql({ status: 'SUCCESS' }); - // }); -}); + it('should create a single tracked entity instance', async () => { + const state = { + ...fixture.initialState, + data: { + orgUnit: 'DiszpKrYNg8', + trackedEntityType: 'nEenWmSyUEp', + attributes: [ + { + attribute: 'w75KJ2mc4zz', + value: 'Gigiwe', + }, + ], + }, + }; -describe('update', () => { - it('should update an event program', async () => { - const state = { - ...globalState, - data: { program: getRandomProgramPayload() }, - }; + const finalState = await execute( + create('trackedEntityInstances', state => state.data) + )(state); - const response = await execute( - update( - 'programs', - state => state.program, - state => state.data.program - ) - )(state); - expect({ - httpStatus: response.data.httpStatus, - httpStatusCode: response.data.httpStatusCode, - status: response.data.status, - }).to.eql({ - httpStatus: 'OK', - httpStatusCode: 200, - status: 'OK', + expect(finalState.data.status).to.eq('OK'); }); - }); - it('should update a single event', async () => { - const state = { - ...globalState, - event: 'OZ3mVgaIAqw', - data: { - program: 'eBAyeGv0exc', - orgUnit: 'DiszpKrYNg8', - status: 'COMPLETED', - }, - }; - const response = await execute( - update( - 'events', - state => state.event, - state => state.data - ) - )(state); - expect({ - httpStatus: response.data.httpStatus, - httpStatusCode: response.data.httpStatusCode, - status: response.data.status, - }).to.eql({ - httpStatus: 'OK', - httpStatusCode: 200, - status: 'OK', - }); - }); + it('should create a single dataValueSet', async () => { + const state = { + ...fixture.initialState, + data: { + dataElement: 'f7n9E0hX8qk', + period: '201401', + orgUnit: 'DiszpKrYNg8', + value: '12', + }, + }; - it('should update a single tracked entity instance', async () => { - const state = { - ...globalState, - data: { - orgUnit: globalState.organisationUnit, - trackedEntityType: 'nEenWmSyUEp', - attributes: [ - { - attribute: 'w75KJ2mc4zz', - value: 'Gigiwe', - }, - ], - }, - }; + const finalState = await execute( + create('dataValueSets', state => state.data) + )(state); - const response = await execute( - update( - 'trackedEntityInstances', - state => state.trackedEntityInstance, - state => state.data - ) - )(state); - - expect({ - httpStatus: response.data.httpStatus, - httpStatusCode: response.data.httpStatusCode, - status: response.data.status, - }).to.eql({ - httpStatus: 'OK', - httpStatusCode: 200, - status: 'OK', + expect(finalState.data.status).to.eq('SUCCESS'); }); - }); - it('should update a single dataValueSet', async () => { - const state = { - ...globalState, - data: { - dataElement: 'f7n9E0hX8qk', - period: '201401', - orgUnit: globalState.organisationUnit, - value: '12', - }, - }; - const response = await execute( - update( - 'dataValueSets', - state => state.dataSet, - state => state.data - ) - )(state); - expect({ status: response.data.status }).to.eql({ status: 'SUCCESS' }); - }); + it('should create a set of related data values sharing the same period and organisation unit', async () => { + const state = { + ...fixture.initialState, + data: { + dataSet: 'pBOMPrpg1QX', + completeDate: '2014-02-03', + period: '201401', + orgUnit: 'DiszpKrYNg8', + dataValues: [ + { + dataElement: 'f7n9E0hX8qk', + value: '1', + }, + { + dataElement: 'Ix2HsbDMLea', + value: '2', + }, + { + dataElement: 'eY5ehpbEsB7', + value: '3', + }, + ], + }, + }; - it('should update a set of related data values sharing the same period and organisation unit', async () => { - const state = { - ...globalState, - data: { - dataSet: globalState.dataSet, - completeDate: '2014-02-03', - period: '201401', - orgUnit: globalState.organisationUnit, - dataValues: [ - { - dataElement: 'f7n9E0hX8qk', - value: '1', - }, - { - dataElement: 'Ix2HsbDMLea', - value: '2', - }, - { - dataElement: 'eY5ehpbEsB7', - value: '3', - }, - ], - }, - }; + const finalState = await execute( + create('dataValueSets', state => state.data) + )(state); - const response = await execute( - update( - 'dataValueSets', - state => state.dataSet, - state => state.data - ) - )(state); - expect({ status: response.data.status }).to.eql({ status: 'SUCCESS' }); + expect(finalState.data.status).to.eq('SUCCESS'); + }); }); -}); -describe('get', () => { - const state = { - configuration: { - username: 'admin', - password: 'district', - hostUrl: 'https://play.dhis2.org/2.36.4', - }, - data: {}, - }; - - it('should get trackedEntityInstances matching the URL parameters specified', async () => { - const response = await execute( - get('trackedEntityInstances', { - params: { - fields: '*', - ou: 'DiszpKrYNg8', - entityType: 'nEenWmSyUEp', - trackedEntityInstance: 'dNpxRu1mWG5', - }, - }) - )(state); - expect(response.data.trackedEntityInstances.length).to.gte(1); - }); + describe('update', () => { + it('should update an event program', async () => { + const state = { + ...fixture.initialState, + program: 'eBAyeGv0exc', + data: { program: getRandomProgramPayload() }, + }; - it('should get all programs in the organisation unit TSyzvBiovKh', async () => { - const response = await execute( - get('programs', { - params: { orgUnit: 'TSyzvBiovKh', fields: '*' }, - }) - )(state); - expect(response.data.programs.length).to.gte(1); - }); -}); + const response = await execute( + update( + 'programs', + state => state.program, + state => state.data.program + ) + )(state); + expect(response.data.status).to.eq('OK'); + }); + + it('should update a single event', async () => { + const state = { + ...fixture.initialState, + event: 'OZ3mVgaIAqw', + data: { + program: 'eBAyeGv0exc', + orgUnit: 'DiszpKrYNg8', + status: 'COMPLETED', + }, + }; + const finalState = await execute( + update( + 'events', + state => state.event, + state => state.data + ) + )(state); + expect(finalState.data.status).to.eql('OK'); + }); -describe('upsert', () => { - const state = { - configuration: { - username: 'admin', - password: 'district', - hostUrl: 'https://play.dhis2.org/2.36.4', - }, - data: {}, - }; - - it('should upsert a trackedEntityInstance matching the URL parameters', async () => { - const response = await execute( - upsert( - 'trackedEntityInstances', - { - created: '2019-08-21T13:27:51.119', + it('should update a single tracked entity instance', async () => { + const state = { + ...fixture.initialState, + data: { orgUnit: 'DiszpKrYNg8', - createdAtClient: '2019-03-19T01:11:03.924', - trackedEntityInstance: 'dNpxRu1mWG5', - lastUpdated: '2019-09-27T00:02:11.604', - trackedEntityType: 'We9I19a3vO1', - lastUpdatedAtClient: '2019-03-19T01:11:03.924', - coordinates: - '[[[-11.8049,8.3374],[-11.8032,8.3436],[-11.8076,8.3441],[-11.8096,8.3387],[-11.8049,8.3374]]]', - inactive: false, - deleted: false, - featureType: 'POLYGON', - geometry: { - type: 'Polygon', - coordinates: [ - [ - [-11.8049, 8.3374], - [-11.8032, 8.3436], - [-11.8076, 8.3441], - [-11.8096, 8.3387], - [-11.8049, 8.3374], - ], - ], - }, - programOwners: [ + trackedEntityType: 'nEenWmSyUEp', + attributes: [ { - ownerOrgUnit: 'DiszpKrYNg8', - program: 'M3xtLkYBlKI', - trackedEntityInstance: 'dNpxRu1mWG5', + attribute: 'w75KJ2mc4zz', + value: 'Gigiwe', }, ], - enrollments: [], - relationships: [ + }, + }; + + const finalState = await execute( + update('trackedEntityInstances', 'bmshzEacgxa', state => state.data) + )(state); + + expect(finalState.data.status).to.eq('OK'); + }); + + it('should update a single dataValueSet', async () => { + const state = { + ...fixture.initialState, + data: { + dataElement: 'f7n9E0hX8qk', + period: '201401', + orgUnit: 'DiszpKrYNg8', + value: '12', + }, + }; + const finalState = await execute( + update('dataValueSets', 'pBOMPrpg1QX', state => state.data) + )(state); + expect(finalState.data.status).to.eql('SUCCESS'); + }); + + it('should update a set of related data values sharing the same period and organisation unit', async () => { + const state = { + ...fixture.initialState, + data: { + dataSet: 'pBOMPrpg1QX', + completeDate: '2014-02-03', + period: '201401', + orgUnit: 'DiszpKrYNg8', + dataValues: [ { - lastUpdated: '2019-08-21T00:00:00.000', - created: '2019-08-21T00:00:00.000', - relationshipName: 'Focus to Case', - bidirectional: false, - relationshipType: 'Mv8R4MPcNcX', - relationship: 'EDfZpCLcEVN', - from: { - trackedEntityInstance: { - trackedEntityInstance: 'dNpxRu1mWG5', - programOwners: [], - }, - }, - to: { - trackedEntityInstance: { - trackedEntityInstance: 'Fbru4rg4dYV', - programOwners: [], - }, - }, + dataElement: 'f7n9E0hX8qk', + value: '1', }, { - lastUpdated: '2019-08-21T00:00:00.000', - created: '2019-08-21T00:00:00.000', - relationshipName: 'Focus to Case', - bidirectional: false, - relationshipType: 'Mv8R4MPcNcX', - relationship: 'z4ItJx8ul3Z', - from: { - trackedEntityInstance: { - trackedEntityInstance: 'dNpxRu1mWG5', - programOwners: [], - }, - }, - to: { - trackedEntityInstance: { - trackedEntityInstance: 'RHA9RWNvAnC', - programOwners: [], - }, - }, + dataElement: 'Ix2HsbDMLea', + value: '2', }, { - lastUpdated: '2019-08-21T00:00:00.000', - created: '2019-08-21T00:00:00.000', - relationshipName: 'Focus to Case', - bidirectional: false, - relationshipType: 'Mv8R4MPcNcX', - relationship: 'XIfv95ZiM4H', - from: { - trackedEntityInstance: { - trackedEntityInstance: 'dNpxRu1mWG5', - programOwners: [], - }, - }, - to: { - trackedEntityInstance: { - trackedEntityInstance: 'jZRaFaYkAtE', - programOwners: [], - }, - }, + dataElement: 'eY5ehpbEsB7', + value: '3', }, ], - attributes: [], }, - { - params: { - fields: '*', - ou: 'DiszpKrYNg8', - entityType: 'nEenWmSyUEp', - trackedEntityInstance: 'dNpxRu1mWG5', - }, - } - ) - )(state); - expect(response.data.httpStatusCode).to.eq(200); - expect(response.data.httpStatus).to.eq('OK'); + }; + + const finalState = await execute( + update('dataValueSets', 'pBOMPrpg1QX', state => state.data) + )(state); + expect(finalState.data.status).to.eq('SUCCESS'); + }); }); }); + +// describe('get', () => { +// const state = { +// configuration: { +// username: 'admin', +// password: 'district', +// hostUrl: 'https://play.dhis2.org/2.36.4', +// }, +// data: {}, +// }; + +// it('should get trackedEntityInstances matching the URL parameters specified', async () => { +// const response = await execute( +// get('trackedEntityInstances', { +// params: { +// fields: '*', +// ou: 'DiszpKrYNg8', +// entityType: 'nEenWmSyUEp', +// trackedEntityInstance: 'dNpxRu1mWG5', +// }, +// }) +// )(state); +// expect(response.data.trackedEntityInstances.length).to.gte(1); +// }); + +// it('should get all programs in the organisation unit TSyzvBiovKh', async () => { +// const response = await execute( +// get('programs', { +// params: { orgUnit: 'TSyzvBiovKh', fields: '*' }, +// }) +// )(state); +// expect(response.data.programs.length).to.gte(1); +// }); +// }); + +// describe('upsert', () => { +// const state = { +// configuration: { +// username: 'admin', +// password: 'district', +// hostUrl: 'https://play.dhis2.org/2.36.4', +// }, +// data: {}, +// }; + +// it('should upsert a trackedEntityInstance matching the URL parameters', async () => { +// const response = await execute( +// upsert( +// 'trackedEntityInstances', +// { +// created: '2019-08-21T13:27:51.119', +// orgUnit: 'DiszpKrYNg8', +// createdAtClient: '2019-03-19T01:11:03.924', +// trackedEntityInstance: 'dNpxRu1mWG5', +// lastUpdated: '2019-09-27T00:02:11.604', +// trackedEntityType: 'We9I19a3vO1', +// lastUpdatedAtClient: '2019-03-19T01:11:03.924', +// coordinates: +// '[[[-11.8049,8.3374],[-11.8032,8.3436],[-11.8076,8.3441],[-11.8096,8.3387],[-11.8049,8.3374]]]', +// inactive: false, +// deleted: false, +// featureType: 'POLYGON', +// geometry: { +// type: 'Polygon', +// coordinates: [ +// [ +// [-11.8049, 8.3374], +// [-11.8032, 8.3436], +// [-11.8076, 8.3441], +// [-11.8096, 8.3387], +// [-11.8049, 8.3374], +// ], +// ], +// }, +// programOwners: [ +// { +// ownerOrgUnit: 'DiszpKrYNg8', +// program: 'M3xtLkYBlKI', +// trackedEntityInstance: 'dNpxRu1mWG5', +// }, +// ], +// enrollments: [], +// relationships: [ +// { +// lastUpdated: '2019-08-21T00:00:00.000', +// created: '2019-08-21T00:00:00.000', +// relationshipName: 'Focus to Case', +// bidirectional: false, +// relationshipType: 'Mv8R4MPcNcX', +// relationship: 'EDfZpCLcEVN', +// from: { +// trackedEntityInstance: { +// trackedEntityInstance: 'dNpxRu1mWG5', +// programOwners: [], +// }, +// }, +// to: { +// trackedEntityInstance: { +// trackedEntityInstance: 'Fbru4rg4dYV', +// programOwners: [], +// }, +// }, +// }, +// { +// lastUpdated: '2019-08-21T00:00:00.000', +// created: '2019-08-21T00:00:00.000', +// relationshipName: 'Focus to Case', +// bidirectional: false, +// relationshipType: 'Mv8R4MPcNcX', +// relationship: 'z4ItJx8ul3Z', +// from: { +// trackedEntityInstance: { +// trackedEntityInstance: 'dNpxRu1mWG5', +// programOwners: [], +// }, +// }, +// to: { +// trackedEntityInstance: { +// trackedEntityInstance: 'RHA9RWNvAnC', +// programOwners: [], +// }, +// }, +// }, +// { +// lastUpdated: '2019-08-21T00:00:00.000', +// created: '2019-08-21T00:00:00.000', +// relationshipName: 'Focus to Case', +// bidirectional: false, +// relationshipType: 'Mv8R4MPcNcX', +// relationship: 'XIfv95ZiM4H', +// from: { +// trackedEntityInstance: { +// trackedEntityInstance: 'dNpxRu1mWG5', +// programOwners: [], +// }, +// }, +// to: { +// trackedEntityInstance: { +// trackedEntityInstance: 'jZRaFaYkAtE', +// programOwners: [], +// }, +// }, +// }, +// ], +// attributes: [], +// }, +// { +// params: { +// fields: '*', +// ou: 'DiszpKrYNg8', +// entityType: 'nEenWmSyUEp', +// trackedEntityInstance: 'dNpxRu1mWG5', +// }, +// } +// ) +// )(state); +// expect(response.data.httpStatusCode).to.eq(200); +// expect(response.data.httpStatus).to.eq('OK'); +// }); +// }); From 69a49b897f161d83fd6259960ef877f44f08fe58 Mon Sep 17 00:00:00 2001 From: Taylor Downs Date: Tue, 21 Dec 2021 14:18:40 -0700 Subject: [PATCH 16/26] fix capitalization on tests --- test/index.js | 50 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/test/index.js b/test/index.js index fdf5ab7..a7a8917 100644 --- a/test/index.js +++ b/test/index.js @@ -54,7 +54,33 @@ describe('execute', () => { }); }); -describe('CREATE', () => { +describe('get', () => { + const state = { + configuration: { + username: 'admin', + password: 'district', + hostUrl: 'https://play.dhis2.org/2.36.4', + }, + data: {}, + }; + + it('should make an authenticated GET to the right url', async () => { + const params = new URLSearchParams({ foo: 'bar' }); + + testServer + .get('/api/events/qAZJCrNJK8H') + .matchHeader('authorization', 'Basic YWRtaW46ZGlzdHJpY3Q=') + .reply(200, { + httpStatus: 'OK', + message: 'the response', + }); + + const response = await execute(update('dataValueSets', {}))(state); + expect(response.data).to.eql({ httpStatus: 'OK', message: 'the response' }); + }); +}); + +describe('create', () => { const state = { configuration: { username: 'admin', @@ -107,7 +133,7 @@ describe('CREATE', () => { }); }); -describe('UPDATE', () => { +describe('update', () => { const state = { configuration: { username: 'admin', @@ -163,7 +189,7 @@ describe('UPDATE', () => { }); }); -describe.only('URL builders', () => { +describe('URL builders', () => { const fixture = {}; before(done => { @@ -177,8 +203,8 @@ describe.only('URL builders', () => { done(); }); - describe.only('buildUrl', () => { - it.only('the proper URL gets built from the "entity" string and the config', done => { + describe('buildUrl', () => { + it('the proper URL gets built from the "entity" string and the config', done => { const configuration = { ...fixture.configuration, apiVersion: 33 }; const finalURL = buildUrl( @@ -195,8 +221,8 @@ describe.only('URL builders', () => { }); }); - describe.only('generateURL', () => { - it.only('should generate basic URL', done => { + describe('generateURL', () => { + it('should generate basic URL', done => { const finalURL = generateUrl( fixture.configuration, fixture.options, @@ -208,7 +234,7 @@ describe.only('URL builders', () => { done(); }); - it.only('should generate URL with specific api version from configuration', done => { + it('should generate URL with specific api version from configuration', done => { const configuration = { ...fixture.configuration, apiVersion: 33 }; const finalURL = generateUrl( @@ -222,7 +248,7 @@ describe.only('URL builders', () => { done(); }); - it.only('should generate URL with specific api version from options', done => { + it('should generate URL with specific api version from options', done => { const options = { ...fixture.options, apiVersion: 33 }; const finalURL = generateUrl( @@ -236,7 +262,7 @@ describe.only('URL builders', () => { done(); }); - it.only('should generate URL without caring about other options', done => { + it('should generate URL without caring about other options', done => { const options = { ...fixture.options, apiVersion: 33, @@ -255,8 +281,8 @@ describe.only('URL builders', () => { }); }); - describe.only('buildURLParams', () => { - it.only('should handle special filter and dimensions params and build the rest per usual', () => { + describe('buildURLParams', () => { + it('should handle special filter and dimensions params and build the rest per usual', () => { const params = { dryRun: true, filters: ['sex:eq:male', 'origin:eq:senegal'], From 446f88138941ed95316dc7ca32d8afb24294f25b Mon Sep 17 00:00:00 2001 From: Taylor Downs Date: Tue, 21 Dec 2021 16:04:20 -0700 Subject: [PATCH 17/26] update GET, add tests --- ast.json | 9 +++- lib/Adaptor.js | 116 ++++++++++++++++++++++++-------------------- src/Adaptor.js | 30 +++++++----- test/index.js | 29 ++++++----- test/integration.js | 61 +++++++++++------------ 5 files changed, 132 insertions(+), 113 deletions(-) diff --git a/ast.json b/ast.json index 66ebb6d..8358cd9 100644 --- a/ast.json +++ b/ast.json @@ -343,8 +343,13 @@ }, { "title": "example", - "description": "get('trackedEntityInstances', {\n fields: '*',\n ou: 'DiszpKrYNg8',\n entityType: 'nEenWmSyUEp',\n trackedEntityInstance: 'dNpxRu1mWG5',\n});", - "caption": "Example getting one `trackedEntityInstance` with `Id` 'dNpxRu1mWG5' for a given `orgUnit(DiszpKrYNg8)`" + "description": "get('dataValueSets', {\n dataSet: 'pBOMPrpg1QX',\n orgUnit: 'DiszpKrYNg8',\n period: '201401',\n fields: '*',\n});", + "caption": "Get all data values for the 'pBOMPrpg1QX' dataset." + }, + { + "title": "example", + "description": "get('programs', { orgUnit: 'TSyzvBiovKh', fields: '*' });", + "caption": "get all programs for an organization unit" } ] }, diff --git a/lib/Adaptor.js b/lib/Adaptor.js index b242d09..e8bfdfe 100644 --- a/lib/Adaptor.js +++ b/lib/Adaptor.js @@ -144,51 +144,61 @@ function configMigrationHelper(state) { return state; } // NOTE: In order to prevent unintended exposure of authentication information // in the logs, we make use of an axios interceptor. -// axios.interceptors.response.use( -// function (response) { -// const contentType = response.headers['content-type']?.split(';')[0]; -// const acceptHeaders = response.config.headers['Accept'] -// .split(';')[0] -// .split(','); -// if (response.config.method === 'get') { -// if (indexOf(acceptHeaders, contentType) === -1) { -// const newError = { -// status: 404, -// message: 'Unexpected content returned', -// responseData: response.data, -// }; -// Log.error(newError.message); -// return Promise.reject(newError); -// } -// } -// if ( -// typeof response?.data === 'string' && -// contentType === CONTENT_TYPES?.json -// ) { -// try { -// response = { ...response, data: JSON.parse(response.data) }; -// } catch (error) { -// Log.warn('Non-JSON response detected, unable to parse.'); -// } -// } -// return response; -// }, -// function (error) { -// try { -// const details = error.toJSON(); -// if (details?.config?.auth) details.config.auth = '--REDACTED--'; -// if (details?.config?.data) details.config.data = '--REDACTED--'; -// Log.error(details.message); -// return Promise.reject(details); -// } catch (e) { -// // TODO: @Elias, why does this error sometimes already appear to be JSONified? -// // console.log(e) // "not JSONABLE TypeError: error.toJSON is not a function" -// Log.error(error.message); -// return Promise.reject(error); -// } -// } -// ); + +_axios.default.interceptors.response.use(function (response) { + var _response$headers$con, _response; + + const contentType = (_response$headers$con = response.headers['content-type']) === null || _response$headers$con === void 0 ? void 0 : _response$headers$con.split(';')[0]; + const acceptHeaders = response.config.headers['Accept'].split(';')[0].split(','); + + if (response.config.method === 'get') { + if ((0, _lodash.indexOf)(acceptHeaders, contentType) === -1) { + const newError = { + status: 404, + message: 'Unexpected content returned', + responseData: response.data + }; + + _Utils.Log.error(newError.message); + + return Promise.reject(newError); + } + } + + if (typeof ((_response = response) === null || _response === void 0 ? void 0 : _response.data) === 'string' && contentType === (_Utils.CONTENT_TYPES === null || _Utils.CONTENT_TYPES === void 0 ? void 0 : _Utils.CONTENT_TYPES.json)) { + try { + response = { ...response, + data: JSON.parse(response.data) + }; + } catch (error) { + _Utils.Log.warn('Non-JSON response detected, unable to parse.'); + } + } + + return response; +}, function (error) { + try { + var _details$config, _details$config2; + + const details = error.toJSON(); + if (details === null || details === void 0 ? void 0 : (_details$config = details.config) === null || _details$config === void 0 ? void 0 : _details$config.auth) details.config.auth = '--REDACTED--'; + if (details === null || details === void 0 ? void 0 : (_details$config2 = details.config) === null || _details$config2 === void 0 ? void 0 : _details$config2.data) details.config.data = '--REDACTED--'; + + _Utils.Log.error(JSON.stringify(details, null, 2)); + + return Promise.reject({ + error: error.message, + data: error.response.data + }); + } catch (e) { + // TODO: @Elias, why does this error sometimes already appear to be JSONified? + // console.log(e) // "not JSONABLE TypeError: error.toJSON is not a function" + _Utils.Log.error(error.message); + + return Promise.reject(error); + } +}); /** * Create a record * @public @@ -314,8 +324,6 @@ function create(resourceType, data, options, callback) { _Utils.Log.success(`Created ${resourceType}: ${result.headers.location}`); return (0, _Utils.handleResponse)(result, state, callback); - }).catch(error => { - console.log('ERROR', error); }); }; } @@ -491,13 +499,15 @@ function update(resourceType, path, data, options, callback) { * @param {Object} [options] - Optional `options` to define URL parameters 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 Example getting one `trackedEntityInstance` with `Id` 'dNpxRu1mWG5' for a given `orgUnit(DiszpKrYNg8)` - * get('trackedEntityInstances', { - * fields: '*', - * ou: 'DiszpKrYNg8', - * entityType: 'nEenWmSyUEp', - * trackedEntityInstance: 'dNpxRu1mWG5', + * @example Get all data values for the 'pBOMPrpg1QX' dataset. + * get('dataValueSets', { + * dataSet: 'pBOMPrpg1QX', + * orgUnit: 'DiszpKrYNg8', + * period: '201401', + * fields: '*', * }); + * @example get all programs for an organization unit + * get('programs', { orgUnit: 'TSyzvBiovKh', fields: '*' }); */ @@ -523,7 +533,7 @@ function get(resourceType, filters, options, callback) { responseType: 'json', ...requestConfig }).then(result => { - _Utils.Log.success(`Retrieved ${result.data[resourceType].length} ${resourceType}.`); + _Utils.Log.success(`Retrieved ${resourceType}`); return (0, _Utils.handleResponse)(result, state, callback); }); diff --git a/src/Adaptor.js b/src/Adaptor.js index f42eb5e..bf960a5 100644 --- a/src/Adaptor.js +++ b/src/Adaptor.js @@ -107,8 +107,11 @@ axios.interceptors.response.use( if (details?.config?.auth) details.config.auth = '--REDACTED--'; if (details?.config?.data) details.config.data = '--REDACTED--'; - Log.error(details.message); - return Promise.reject(details); + Log.error(JSON.stringify(details, null, 2)); + return Promise.reject({ + error: error.message, + data: error.response.data, + }); } catch (e) { // TODO: @Elias, why does this error sometimes already appear to be JSONified? // console.log(e) // "not JSONABLE TypeError: error.toJSON is not a function" @@ -410,13 +413,15 @@ export function update(resourceType, path, data, options, callback) { * @param {Object} [options] - Optional `options` to define URL parameters 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 Example getting one `trackedEntityInstance` with `Id` 'dNpxRu1mWG5' for a given `orgUnit(DiszpKrYNg8)` - * get('trackedEntityInstances', { - * fields: '*', - * ou: 'DiszpKrYNg8', - * entityType: 'nEenWmSyUEp', - * trackedEntityInstance: 'dNpxRu1mWG5', + * @example Get all data values for the 'pBOMPrpg1QX' dataset. + * get('dataValueSets', { + * dataSet: 'pBOMPrpg1QX', + * orgUnit: 'DiszpKrYNg8', + * period: '201401', + * fields: '*', * }); + * @example get all programs for an organization unit + * get('programs', { orgUnit: 'TSyzvBiovKh', fields: '*' }); */ export function get(resourceType, filters, options, callback) { return state => { @@ -436,9 +441,7 @@ export function get(resourceType, filters, options, callback) { responseType: 'json', ...requestConfig, }).then(result => { - Log.success( - `Retrieved ${result.data[resourceType].length} ${resourceType}.` - ); + Log.success(`Retrieved ${resourceType}`); return handleResponse(result, state, callback); }); }; @@ -528,8 +531,9 @@ export function discover(httpMethod, endpoint) { if (param.schema['$ref']) { let schemaRefIndex = param.schema['$ref'].lastIndexOf('/') + 1; - let schemaRef = - param.schema['$ref'].slice(schemaRefIndex); + let schemaRef = param.schema['$ref'].slice( + schemaRefIndex + ); param.schema = tempData.components.schemas[schemaRef]; } diff --git a/test/index.js b/test/index.js index a7a8917..f608e08 100644 --- a/test/index.js +++ b/test/index.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { execute, create, update } from '../lib/Adaptor'; +import { execute, create, update, get, upsert } from '../lib/Adaptor'; import { dataValue } from '@openfn/language-common'; import { buildUrl, buildUrlParams, generateUrl, nestArray } from '../lib/Utils'; import nock from 'nock'; @@ -14,15 +14,9 @@ describe('execute', () => { }, }; let operations = [ - state => { - return { counter: 1 }; - }, - state => { - return { counter: 2 }; - }, - state => { - return { counter: 3 }; - }, + () => ({ counter: 1 }), + () => ({ counter: 2 }), + () => ({ counter: 3 }), ]; execute(...operations)(state) @@ -65,17 +59,26 @@ describe('get', () => { }; it('should make an authenticated GET to the right url', async () => { - const params = new URLSearchParams({ foo: 'bar' }); + const filter = { + dataSet: 'pBOMPrpg1QX', + period: 201401, + orgUnit: 'DiszpKrYNg8', + }; + + const params = new URLSearchParams({ ...filter, fields: '*' }); testServer - .get('/api/events/qAZJCrNJK8H') + .get('/api/dataValueSets') + .query(params) .matchHeader('authorization', 'Basic YWRtaW46ZGlzdHJpY3Q=') .reply(200, { httpStatus: 'OK', message: 'the response', }); - const response = await execute(update('dataValueSets', {}))(state); + const response = await execute( + get('dataValueSets', filter, { params: { fields: '*' } }) + )(state); expect(response.data).to.eql({ httpStatus: 'OK', message: 'the response' }); }); }); diff --git a/test/integration.js b/test/integration.js index 03493b7..5bf2720 100644 --- a/test/integration.js +++ b/test/integration.js @@ -253,39 +253,36 @@ describe('Integration tests', () => { }); }); -// describe('get', () => { -// const state = { -// configuration: { -// username: 'admin', -// password: 'district', -// hostUrl: 'https://play.dhis2.org/2.36.4', -// }, -// data: {}, -// }; - -// it('should get trackedEntityInstances matching the URL parameters specified', async () => { -// const response = await execute( -// get('trackedEntityInstances', { -// params: { -// fields: '*', -// ou: 'DiszpKrYNg8', -// entityType: 'nEenWmSyUEp', -// trackedEntityInstance: 'dNpxRu1mWG5', -// }, -// }) -// )(state); -// expect(response.data.trackedEntityInstances.length).to.gte(1); -// }); +describe('get', () => { + const state = { + configuration: { + username: 'admin', + password: 'district', + hostUrl: 'https://play.dhis2.org/2.36.4', + }, + data: {}, + }; + + it('should get dataValueSets matching the filters specified', async () => { + const finalState = await execute( + get('dataValueSets', { + dataSet: 'pBOMPrpg1QX', + orgUnit: 'DiszpKrYNg8', + period: '201401', + fields: '*', + }) + )(state); + + expect(finalState.data.dataValues.length).to.gte(1); + }); -// it('should get all programs in the organisation unit TSyzvBiovKh', async () => { -// const response = await execute( -// get('programs', { -// params: { orgUnit: 'TSyzvBiovKh', fields: '*' }, -// }) -// )(state); -// expect(response.data.programs.length).to.gte(1); -// }); -// }); + it('should get all programs in the organisation unit TSyzvBiovKh', async () => { + const response = await execute( + get('programs', { orgUnit: 'TSyzvBiovKh', fields: '*' }) + )(state); + expect(response.data.programs.length).to.gte(1); + }); +}); // describe('upsert', () => { // const state = { From 220dee88f27e34200a60ec451e86257e7bf93836 Mon Sep 17 00:00:00 2001 From: Taylor Downs Date: Tue, 21 Dec 2021 16:46:21 -0700 Subject: [PATCH 18/26] tests, interceptor, remove upsert --- ast.json | 121 ---------------------------------------------- lib/Adaptor.js | 120 +++++++++++++++++++++++----------------------- lib/Utils.js | 6 +-- src/Adaptor.js | 128 ++++++++++++++++++++++++------------------------- src/Utils.js | 6 +-- test/index.js | 58 ++++++++++++++++++++++ 6 files changed, 186 insertions(+), 253 deletions(-) diff --git a/ast.json b/ast.json index 8358cd9..de33138 100644 --- a/ast.json +++ b/ast.json @@ -355,127 +355,6 @@ }, "valid": true }, - { - "name": "upsert", - "params": [ - "resourceType", - "data", - "options", - "callback" - ], - "docs": { - "description": "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.", - "tags": [ - { - "title": "public", - "description": null, - "type": null - }, - { - "title": "function", - "description": null, - "name": null - }, - { - "title": "param", - "description": "The type of a resource to `insert` or `update`. E.g. `trackedEntityInstances`", - "type": { - "type": "NameExpression", - "name": "string" - }, - "name": "resourceType" - }, - { - "title": "param", - "description": "The update data containing new values", - "type": { - "type": "NameExpression", - "name": "Object" - }, - "name": "data" - }, - { - "title": "param", - "description": "`Optional` options for `upsertTEI` operation. Defaults to `{replace: false, apiVersion: state.configuration.apiVersion,strict: true,responseType: 'json'}`.", - "type": { - "type": "OptionalType", - "expression": { - "type": "RecordType", - "fields": [ - { - "type": "FieldType", - "key": "replace", - "value": { - "type": "NameExpression", - "name": "boolean" - } - }, - { - "type": "FieldType", - "key": "apiVersion", - "value": { - "type": "NameExpression", - "name": "number" - } - }, - { - "type": "FieldType", - "key": "strict", - "value": { - "type": "NameExpression", - "name": "boolean" - } - }, - { - "type": "FieldType", - "key": "responseType", - "value": { - "type": "NameExpression", - "name": "string" - } - } - ] - } - }, - "name": "options" - }, - { - "title": "param", - "description": "Optional callback to handle the response", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "function" - } - }, - "name": "callback" - }, - { - "title": "throws", - "description": "Throws range error", - "type": { - "type": "NameExpression", - "name": "RangeError" - } - }, - { - "title": "returns", - "description": null, - "type": { - "type": "NameExpression", - "name": "Operation" - } - }, - { - "title": "example", - "description": "upsert(\n 'trackedEntityInstances',\n {\n attributeId: 'lZGmxYbs97q',\n attributeValue: state =>\n state.data.attributes.find(obj => obj.attribute === 'lZGmxYbs97q')\n .value,\n },\n state.data,\n { ou: 'TSyzvBiovKh' }\n);", - "caption": "Example `expression.js` of upsert" - } - ] - }, - "valid": true - }, { "name": "discover", "params": [ diff --git a/lib/Adaptor.js b/lib/Adaptor.js index e8bfdfe..ed93012 100644 --- a/lib/Adaptor.js +++ b/lib/Adaptor.js @@ -7,7 +7,6 @@ exports.execute = execute; exports.create = create; exports.update = update; exports.get = get; -exports.upsert = upsert; exports.discover = discover; exports.patch = patch; exports.del = del; @@ -178,26 +177,20 @@ _axios.default.interceptors.response.use(function (response) { return response; }, function (error) { - try { - var _details$config, _details$config2; + var _error$config, _error$config2, _error$response, _error$response$data, _error$response2; - const details = error.toJSON(); - if (details === null || details === void 0 ? void 0 : (_details$config = details.config) === null || _details$config === void 0 ? void 0 : _details$config.auth) details.config.auth = '--REDACTED--'; - if (details === null || details === void 0 ? void 0 : (_details$config2 = details.config) === null || _details$config2 === void 0 ? void 0 : _details$config2.data) details.config.data = '--REDACTED--'; + if ((_error$config = error.config) === null || _error$config === void 0 ? void 0 : _error$config.auth) error.config.auth = '--REDACTED--'; + if ((_error$config2 = error.config) === null || _error$config2 === void 0 ? void 0 : _error$config2.data) error.config.data = '--REDACTED--'; + const details = (_error$response = error.response) === null || _error$response === void 0 ? void 0 : (_error$response$data = _error$response.data) === null || _error$response$data === void 0 ? void 0 : _error$response$data.response; - _Utils.Log.error(JSON.stringify(details, null, 2)); + _Utils.Log.error(error.message || "That didn't work."); - return Promise.reject({ - error: error.message, - data: error.response.data - }); - } catch (e) { - // TODO: @Elias, why does this error sometimes already appear to be JSONified? - // console.log(e) // "not JSONABLE TypeError: error.toJSON is not a function" - _Utils.Log.error(error.message); - - return Promise.reject(error); - } + if (details) console.log(JSON.stringify(details, null, 2)); + return Promise.reject({ + request: error.config, + error: error.message, + response: (_error$response2 = error.response) === null || _error$response2 === void 0 ? void 0 : _error$response2.data + }); }); /** * Create a record @@ -538,50 +531,55 @@ function get(resourceType, filters, options, callback) { return (0, _Utils.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 `insert` or `update`. E.g. `trackedEntityInstances` - * @param {Object} data - The update data containing new values - * @param {{replace:boolean, apiVersion: number,strict: boolean,responseType: string}} [options] - `Optional` options for `upsertTEI` operation. Defaults to `{replace: false, apiVersion: state.configuration.apiVersion,strict: true,responseType: 'json'}`. - * @param {function} [callback] - Optional callback to handle the response - * @throws {RangeError} - Throws range error - * @returns {Operation} - * @example Example `expression.js` of upsert - * upsert( - * 'trackedEntityInstances', - * { - * attributeId: 'lZGmxYbs97q', - * attributeValue: state => - * state.data.attributes.find(obj => obj.attribute === 'lZGmxYbs97q') - * .value, - * }, - * state.data, - * { ou: 'TSyzvBiovKh' } - * ); - */ +} // /** +// * 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 `insert` or `update`. E.g. `trackedEntityInstances` +// * @param {Object} data - The update data containing new values +// * @param {{replace:boolean, apiVersion: number,strict: boolean,responseType: string}} [options] - `Optional` options for `upsertTEI` operation. Defaults to `{replace: false, apiVersion: state.configuration.apiVersion,strict: true,responseType: 'json'}`. +// * @param {function} [callback] - Optional callback to handle the response +// * @throws {RangeError} - Throws range error +// * @returns {Operation} +// * @example Example `expression.js` of upsert +// * upsert( +// * 'trackedEntityInstances', +// * { +// * attributeId: 'lZGmxYbs97q', +// * attributeValue: state => +// * state.data.attributes.find(obj => obj.attribute === 'lZGmxYbs97q') +// * .value, +// * }, +// * state.data, +// * { ou: 'TSyzvBiovKh' } +// * ); +// */ +// export function upsert(resourceType, data, options, callback) { +// return state => { +// console.log(`Preparing upsert via 'get' then 'create' OR 'update'...`); +// return get( +// resourceType, +// data +// )(state).then(resp => { +// const resources = resp.data[resourceType]; +// if (resources.length > 1) { +// throw new RangeError( +// `Cannot upsert on Non-unique attribute. The operation found more than one records for your request.` +// ); +// } else if (resources.length <= 0) { +// return create(resourceType, data, options, callback)(state); +// } else { +// const pathName = +// resourceType === 'trackedEntityInstances' +// ? 'trackedEntityInstance' +// : 'id'; +// const path = resources[0][pathName]; +// return update(resourceType, path, data, options, callback)(state); +// } +// }); +// }; +// } - -function upsert(resourceType, data, options, callback) { - return state => { - console.log(`Preparing upsert via 'get' then 'create' OR 'update'...`); - return get(resourceType, data)(state).then(resp => { - const resources = resp.data[resourceType]; - - if (resources.length > 1) { - throw new RangeError(`Cannot upsert on Non-unique attribute. The operation found more than one records for your request.`); - } else if (resources.length <= 0) { - return create(resourceType, data, options, callback)(state); - } else { - const pathName = resourceType === 'trackedEntityInstances' ? 'trackedEntityInstance' : 'id'; - const path = resources[0][pathName]; - return update(resourceType, path, data, options, callback)(state); - } - }); - }; -} /** * Discover `DHIS2` `api` `endpoint` `query parameters` and allowed `operators` for a given resource's endpoint. * @public diff --git a/lib/Utils.js b/lib/Utils.js index c9f0d0f..408c0a2 100644 --- a/lib/Utils.js +++ b/lib/Utils.js @@ -24,15 +24,15 @@ exports.CONTENT_TYPES = CONTENT_TYPES; class Log { static success(message) { - return console.info(`✓ ${message} @ ${new Date()}`); + return console.info(`✓ Success at ${new Date()}:\n`, message); } static warn(message) { - return console.warn(`⚠ Warning: ${message} @ ${new Date()}`); + return console.warn(`⚠ Warning at ${new Date()}:\n`, message); } static error(message) { - return console.error(`✗ Error: ${message} @ ${new Date()}`); + return console.error(`✗ Error at ${new Date()}:\n`, message); } } diff --git a/src/Adaptor.js b/src/Adaptor.js index bf960a5..b35e385 100644 --- a/src/Adaptor.js +++ b/src/Adaptor.js @@ -102,22 +102,20 @@ axios.interceptors.response.use( return response; }, function (error) { - try { - const details = error.toJSON(); - if (details?.config?.auth) details.config.auth = '--REDACTED--'; - if (details?.config?.data) details.config.data = '--REDACTED--'; - - Log.error(JSON.stringify(details, null, 2)); - return Promise.reject({ - error: error.message, - data: error.response.data, - }); - } catch (e) { - // TODO: @Elias, why does this error sometimes already appear to be JSONified? - // console.log(e) // "not JSONABLE TypeError: error.toJSON is not a function" - Log.error(error.message); - return Promise.reject(error); - } + if (error.config?.auth) error.config.auth = '--REDACTED--'; + if (error.config?.data) error.config.data = '--REDACTED--'; + + const details = error.response?.data?.response; + + Log.error(error.message || "That didn't work."); + + if (details) console.log(JSON.stringify(details, null, 2)); + + return Promise.reject({ + request: error.config, + error: error.message, + response: error.response?.data, + }); } ); @@ -447,55 +445,55 @@ export function get(resourceType, filters, options, 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 `insert` or `update`. E.g. `trackedEntityInstances` - * @param {Object} data - The update data containing new values - * @param {{replace:boolean, apiVersion: number,strict: boolean,responseType: string}} [options] - `Optional` options for `upsertTEI` operation. Defaults to `{replace: false, apiVersion: state.configuration.apiVersion,strict: true,responseType: 'json'}`. - * @param {function} [callback] - Optional callback to handle the response - * @throws {RangeError} - Throws range error - * @returns {Operation} - * @example Example `expression.js` of upsert - * upsert( - * 'trackedEntityInstances', - * { - * attributeId: 'lZGmxYbs97q', - * attributeValue: state => - * state.data.attributes.find(obj => obj.attribute === 'lZGmxYbs97q') - * .value, - * }, - * state.data, - * { ou: 'TSyzvBiovKh' } - * ); - */ -export function upsert(resourceType, data, options, callback) { - return state => { - console.log(`Preparing upsert via 'get' then 'create' OR 'update'...`); - - return get( - resourceType, - data - )(state).then(resp => { - const resources = resp.data[resourceType]; - if (resources.length > 1) { - throw new RangeError( - `Cannot upsert on Non-unique attribute. The operation found more than one records for your request.` - ); - } else if (resources.length <= 0) { - return create(resourceType, data, options, callback)(state); - } else { - const pathName = - resourceType === 'trackedEntityInstances' - ? 'trackedEntityInstance' - : 'id'; - const path = resources[0][pathName]; - return update(resourceType, path, data, options, callback)(state); - } - }); - }; -} +// /** +// * 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 `insert` or `update`. E.g. `trackedEntityInstances` +// * @param {Object} data - The update data containing new values +// * @param {{replace:boolean, apiVersion: number,strict: boolean,responseType: string}} [options] - `Optional` options for `upsertTEI` operation. Defaults to `{replace: false, apiVersion: state.configuration.apiVersion,strict: true,responseType: 'json'}`. +// * @param {function} [callback] - Optional callback to handle the response +// * @throws {RangeError} - Throws range error +// * @returns {Operation} +// * @example Example `expression.js` of upsert +// * upsert( +// * 'trackedEntityInstances', +// * { +// * attributeId: 'lZGmxYbs97q', +// * attributeValue: state => +// * state.data.attributes.find(obj => obj.attribute === 'lZGmxYbs97q') +// * .value, +// * }, +// * state.data, +// * { ou: 'TSyzvBiovKh' } +// * ); +// */ +// export function upsert(resourceType, data, options, callback) { +// return state => { +// console.log(`Preparing upsert via 'get' then 'create' OR 'update'...`); + +// return get( +// resourceType, +// data +// )(state).then(resp => { +// const resources = resp.data[resourceType]; +// if (resources.length > 1) { +// throw new RangeError( +// `Cannot upsert on Non-unique attribute. The operation found more than one records for your request.` +// ); +// } else if (resources.length <= 0) { +// return create(resourceType, data, options, callback)(state); +// } else { +// const pathName = +// resourceType === 'trackedEntityInstances' +// ? 'trackedEntityInstance' +// : 'id'; +// const path = resources[0][pathName]; +// return update(resourceType, path, data, options, callback)(state); +// } +// }); +// }; +// } /** * Discover `DHIS2` `api` `endpoint` `query parameters` and allowed `operators` for a given resource's endpoint. diff --git a/src/Utils.js b/src/Utils.js index fd97c81..efd7085 100644 --- a/src/Utils.js +++ b/src/Utils.js @@ -10,15 +10,15 @@ export const CONTENT_TYPES = { export class Log { static success(message) { - return console.info(`✓ ${message} @ ${new Date()}`); + return console.info(`✓ Success at ${new Date()}:\n`, message); } static warn(message) { - return console.warn(`⚠ Warning: ${message} @ ${new Date()}`); + return console.warn(`⚠ Warning at ${new Date()}:\n`, message); } static error(message) { - return console.error(`✗ Error: ${message} @ ${new Date()}`); + return console.error(`✗ Error at ${new Date()}:\n`, message); } } diff --git a/test/index.js b/test/index.js index f608e08..6ffb9ad 100644 --- a/test/index.js +++ b/test/index.js @@ -192,6 +192,64 @@ describe('update', () => { }); }); +// describe('upsert', () => { +// const state = { +// configuration: { +// username: 'admin', +// password: 'district', +// hostUrl: 'https://play.dhis2.org/2.36.4', +// }, +// data: { +// org: 'orgunit', +// id: 'k68SkK5yDH9', +// }, +// }; + +// it('should make a get and then a create if nothing is found', async () => { +// testServer +// .get('/api/events/qAZJCrNJK8H', { +// program: 'program', +// orgUnit: 'hardcoded', +// date: '02-02-20', +// }) +// .reply(200, { +// httpStatus: 'OK', +// message: 'the response', +// }); + +// const response = await execute( +// update('events', 'qAZJCrNJK8H', { +// program: dataValue('program'), +// orgUnit: 'hardcoded', +// date: state => state.data.currentDate, +// }) +// )(state); +// expect(response.data).to.eql({ httpStatus: 'OK', message: 'the response' }); +// }); + +// it('should make a get and then an update if one thing is found', async () => { +// testServer +// .put('/api/events/qAZJCrNJK8H', { +// program: 'program', +// orgUnit: 'hardcoded', +// date: '02-02-20', +// }) +// .reply(200, { +// httpStatus: 'OK', +// message: 'the response', +// }); + +// const response = await execute( +// update('events', 'qAZJCrNJK8H', { +// program: dataValue('program'), +// orgUnit: 'hardcoded', +// date: state => state.data.currentDate, +// }) +// )(state); +// expect(response.data).to.eql({ httpStatus: 'OK', message: 'the response' }); +// }); +// }); + describe('URL builders', () => { const fixture = {}; From 14c7dc84129725877bef24f28e654372bcc224eb Mon Sep 17 00:00:00 2001 From: Taylor Downs Date: Tue, 21 Dec 2021 23:47:07 +0000 Subject: [PATCH 19/26] prerelease version bump --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index fbb21a2..4d5f899 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@openfn/language-dhis2", - "version": "3.0.0-1", + "version": "3.0.0-2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 0bf2186..425ad01 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@openfn/language-dhis2", - "version": "3.0.0-1", + "version": "3.0.0-2", "description": "DHIS2 Language Pack for OpenFn", "homepage": "https://docs.openfn.org", "repository": { From 775ea2935922dd04cd82a542c0ca9e017a1c6c92 Mon Sep 17 00:00:00 2001 From: Taylor Downs Date: Tue, 21 Dec 2021 23:12:15 -0700 Subject: [PATCH 20/26] add dv, update docs --- ast.json | 140 ++++++++++++++++++++++++++++++++++--------------- lib/Adaptor.js | 115 +++++++++++++++++++++++----------------- src/Adaptor.js | 108 ++++++++++++++++++++++---------------- 3 files changed, 229 insertions(+), 134 deletions(-) diff --git a/ast.json b/ast.json index de33138..2fa6fda 100644 --- a/ast.json +++ b/ast.json @@ -74,57 +74,57 @@ { "title": "example", "description": "create('programs', {\n name: 'name 20',\n shortName: 'n20',\n programType: 'WITHOUT_REGISTRATION',\n});", - "caption": "-a `program`" + "caption": "a program" }, { "title": "example", "description": "create('events', {\n program: 'eBAyeGv0exc',\n orgUnit: 'DiszpKrYNg8',\n status: 'COMPLETED',\n});", - "caption": "-an `event`" + "caption": "an 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`" + "caption": "a trackedEntityInstance" }, { "title": "example", "description": "create('dataSets', { name: 'OpenFn Data Set', periodType: 'Monthly' });", - "caption": "-a `dataSet`" + "caption": "a dataSet" }, { "title": "example", "description": "create('dataSetNotificationTemplates', {\n dataSetNotificationTrigger: 'DATA_SET_COMPLETION',\n notificationRecipient: 'ORGANISATION_UNIT_CONTACT',\n name: 'Notification',\n messageTemplate: 'Hello',\n deliveryChannels: ['SMS'],\n dataSets: [],\n});", - "caption": "-a `dataSetNotification`" + "caption": "a dataSetNotification" }, { "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": "a dataElement" }, { "title": "example", "description": "create('dataElementGroups', {\n name: 'Data Element Group 1',\n dataElements: [],\n});", - "caption": "-a `dataElementGroup`" + "caption": "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": "a dataElementGroupSet" }, { "title": "example", "description": "create('dataValueSets', {\n dataElement: 'f7n9E0hX8qk',\n period: '201401',\n orgUnit: 'DiszpKrYNg8',\n value: '12',\n});", - "caption": "-a `dataValueSet`" + "caption": "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": "a dataValueSet with related dataValues" }, { "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`" + "caption": "an enrollment" } ] }, @@ -214,52 +214,52 @@ { "title": "example", "description": "update('programs', 'qAZJCrNJK8H', {\n name: '14e1aa02c3f0a31618e096f2c6d03bed',\n shortName: '14e1aa02',\n programType: 'WITHOUT_REGISTRATION',\n});", - "caption": "-a program" + "caption": "a program" }, { "title": "example", "description": "update('events', 'PVqUD2hvU4E', {\n program: 'eBAyeGv0exc',\n orgUnit: 'Ngelehun CHC',\n status: 'COMPLETED',\n storedBy: 'admin',\n dataValues: [],\n});", - "caption": "an `event`" + "caption": "an event" }, { "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`" + "caption": "a trackedEntityInstance" }, { "title": "example", "description": "update('dataSets', 'lyLU2wR22tC', { name: 'OpenFN Data Set', periodType: 'Weekly' });", - "caption": "-a `dataSet`" + "caption": "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});", - "caption": "-a `dataSetNotification`" + "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": "a dataElement" }, { "title": "example", "description": "update('dataElementGroups', 'QrprHT61XFk', {\n name: 'Data Element Group 1',\n dataElements: [],\n});", - "caption": "-a `dataElementGroup`" + "caption": "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": "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": "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": "a dataValueSet with related dataValues" }, { "title": "example", @@ -344,12 +344,17 @@ { "title": "example", "description": "get('dataValueSets', {\n dataSet: 'pBOMPrpg1QX',\n orgUnit: 'DiszpKrYNg8',\n period: '201401',\n fields: '*',\n});", - "caption": "Get all data values for the 'pBOMPrpg1QX' dataset." + "caption": "all data values for the 'pBOMPrpg1QX' dataset" }, { "title": "example", "description": "get('programs', { orgUnit: 'TSyzvBiovKh', fields: '*' });", - "caption": "get all programs for an organization unit" + "caption": "all programs for an organization unit" + }, + { + "title": "example", + "description": "get('trackedEntityInstances', {\n ou: 'DiszpKrYNg8',\n filters: ['flGbXLXCrEo:Eq:124'],\n});", + "caption": "a single tracked entity instance by a unique external ID" } ] }, @@ -403,7 +408,7 @@ { "title": "example", "description": "discover('post', '/trackedEntityInstances')", - "caption": "Example getting a list of `parameters allowed` on a given `endpoint` for specific `http method`" + "caption": "a list of parameters allowed on a given endpoint for specific http method" } ] }, @@ -530,15 +535,15 @@ }, { "title": "example", - "description": "patch('dataElements', 'FTRrcoaog83',\n{\n name: 'New Name',\n});", - "caption": "Example `patching` a `data element`" + "description": "patch('dataElements', 'FTRrcoaog83', { name: 'New Name' });", + "caption": "a dataElement" } ] }, "valid": true }, { - "name": "del", + "name": "destroy", "params": [ "resourceType", "path", @@ -661,18 +666,18 @@ }, { "title": "example", - "description": "del('trackedEntityInstances', 'LcRd6Nyaq7T');", - "caption": "Example`deleting` a `tracked entity instance`" + "description": "destroy('trackedEntityInstances', 'LcRd6Nyaq7T');", + "caption": "a tracked entity instance" } ] }, "valid": true }, { - "name": "attrVal", + "name": "findAttributeValue", "params": [ - "tei", - "attributeName" + "trackedEntityInstance", + "attributeDisplayName" ], "docs": { "description": "Gets an attribute value by its case-insensitive display name", @@ -684,7 +689,7 @@ }, { "title": "example", - "description": "attrVal(tei.attributes, 'first name')" + "description": "findAttributeValue(state.data.trackedEntityInstances[0], 'first name')" }, { "title": "function", @@ -698,7 +703,7 @@ "type": "NameExpression", "name": "Object" }, - "name": "tei" + "name": "trackedEntityInstance" }, { "title": "param", @@ -707,7 +712,7 @@ "type": "NameExpression", "name": "string" }, - "name": "attributeName" + "name": "attributeDisplayName" }, { "title": "returns", @@ -722,10 +727,10 @@ "valid": true }, { - "name": "attribute", + "name": "attr", "params": [ - "attributeId", - "attributeValue" + "attribute", + "value" ], "docs": { "description": "Converts an attribute ID and value into a DSHI2 attribute object", @@ -737,7 +742,7 @@ }, { "title": "example", - "description": "attribute('w75KJ2mc4zz', 'Elias')" + "description": "attr('w75KJ2mc4zz', 'Elias')" }, { "title": "function", @@ -751,7 +756,7 @@ "type": "NameExpression", "name": "string" }, - "name": "attributeId" + "name": "attribute" }, { "title": "param", @@ -760,7 +765,60 @@ "type": "NameExpression", "name": "string" }, - "name": "attributeValue" + "name": "value" + }, + { + "title": "returns", + "description": null, + "type": { + "type": "NameExpression", + "name": "object" + } + } + ] + }, + "valid": true + }, + { + "name": "dv", + "params": [ + "dataElement", + "value" + ], + "docs": { + "description": "Converts a dataElement and value into a DSHI2 dataValue object", + "tags": [ + { + "title": "public", + "description": null, + "type": null + }, + { + "title": "example", + "description": "dv('f7n9E0hX8qk', 12)" + }, + { + "title": "function", + "description": null, + "name": null + }, + { + "title": "param", + "description": "A data element ID.", + "type": { + "type": "NameExpression", + "name": "string" + }, + "name": "dataElement" + }, + { + "title": "param", + "description": "The value for that data element.", + "type": { + "type": "NameExpression", + "name": "string" + }, + "name": "value" }, { "title": "returns", diff --git a/lib/Adaptor.js b/lib/Adaptor.js index ed93012..197293c 100644 --- a/lib/Adaptor.js +++ b/lib/Adaptor.js @@ -9,9 +9,10 @@ exports.update = update; exports.get = get; exports.discover = discover; exports.patch = patch; -exports.del = del; -exports.attrVal = attrVal; -exports.attribute = attribute; +exports.destroy = destroy; +exports.findAttributeValue = findAttributeValue; +exports.attr = attr; +exports.dv = dv; Object.defineProperty(exports, "field", { enumerable: true, get: function () { @@ -201,19 +202,19 @@ _axios.default.interceptors.response.use(function (response) { * @param {Object} [options] - Optional `options` to define URL parameters (E.g. `filters`, `dimensions` and `import parameters`), axios configurations (E.g. `auth`) and DHIS 2 api version to use. * @param {function} [callback] - Optional callback to handle the response * @returns {Operation} - * @example -a `program` + * @example a program * create('programs', { * name: 'name 20', * shortName: 'n20', * programType: 'WITHOUT_REGISTRATION', * }); - * @example -an `event` + * @example an event * create('events', { * program: 'eBAyeGv0exc', * orgUnit: 'DiszpKrYNg8', * status: 'COMPLETED', * }); - * @example -a `trackedEntityInstance` + * @example a trackedEntityInstance * create('trackedEntityInstances', { * orgUnit: 'TSyzvBiovKh', * trackedEntityType: 'nEenWmSyUEp', @@ -224,9 +225,9 @@ _axios.default.interceptors.response.use(function (response) { * }, * ] * }); - * @example -a `dataSet` + * @example a dataSet * create('dataSets', { name: 'OpenFn Data Set', periodType: 'Monthly' }); - * @example -a `dataSetNotification` + * @example a dataSetNotification * create('dataSetNotificationTemplates', { * dataSetNotificationTrigger: 'DATA_SET_COMPLETION', * notificationRecipient: 'ORGANISATION_UNIT_CONTACT', @@ -235,7 +236,7 @@ _axios.default.interceptors.response.use(function (response) { * deliveryChannels: ['SMS'], * dataSets: [], * }); - * @example -a `dataElement` + * @example a dataElement * create('dataElements', { * aggregationType: 'SUM', * domainType: 'AGGREGATE', @@ -243,26 +244,26 @@ _axios.default.interceptors.response.use(function (response) { * name: 'Paracetamol', * shortName: 'Para', * }); - * @example -a `dataElementGroup` + * @example a dataElementGroup * create('dataElementGroups', { * name: 'Data Element Group 1', * dataElements: [], * }); - * @example -a `dataElementGroupSet` + * @example a dataElementGroupSet * create('dataElementGroupSets', { * name: 'Data Element Group Set 4', * dataDimension: true, * shortName: 'DEGS4', * dataElementGroups: [], * }); - * @example -a `dataValueSet` + * @example a dataValueSet * create('dataValueSets', { * dataElement: 'f7n9E0hX8qk', * period: '201401', * orgUnit: 'DiszpKrYNg8', * value: '12', * }); - * @example -a `dataValueSet` with related `dataValues` + * @example a dataValueSet with related dataValues * create('dataValueSets', { * dataSet: 'pBOMPrpg1QX', * completeDate: '2014-02-03', @@ -283,7 +284,7 @@ _axios.default.interceptors.response.use(function (response) { * }, * ], * }); - * @example -an `enrollment` + * @example an enrollment * create('enrollments', { * trackedEntityInstance: 'bmshzEacgxa', * orgUnit: 'TSyzvBiovKh', @@ -331,13 +332,13 @@ function create(resourceType, data, options, callback) { * @param {Object} [options] - Optional `options` to define URL parameters (E.g. `filters`, `dimensions` and `import parameters`), axios configurations (E.g. `auth`) and DHIS 2 api version to use. * @param {function} [callback] - Optional callback to handle the response * @returns {Operation} - * @example -a program + * @example a program * update('programs', 'qAZJCrNJK8H', { * name: '14e1aa02c3f0a31618e096f2c6d03bed', * shortName: '14e1aa02', * programType: 'WITHOUT_REGISTRATION', * }); - * @example an `event` + * @example an event * update('events', 'PVqUD2hvU4E', { * program: 'eBAyeGv0exc', * orgUnit: 'Ngelehun CHC', @@ -345,7 +346,7 @@ function create(resourceType, data, options, callback) { * storedBy: 'admin', * dataValues: [], * }); - * @example a `trackedEntityInstance` + * @example a trackedEntityInstance * update('trackedEntityInstances', 'IeQfgUtGPq2', { * created: '2015-08-06T21:12:37.256', * orgUnit: 'TSyzvBiovKh', @@ -385,9 +386,9 @@ function create(resourceType, data, options, callback) { * }, * ], * }); - * @example -a `dataSet` + * @example a dataSet * update('dataSets', 'lyLU2wR22tC', { name: 'OpenFN Data Set', periodType: 'Weekly' }); - * @example -a `dataSetNotification` + * @example a dataSetNotification * update('dataSetNotificationTemplates', 'VbQBwdm1wVP', { * dataSetNotificationTrigger: 'DATA_SET_COMPLETION', * notificationRecipient: 'ORGANISATION_UNIT_CONTACT', @@ -396,7 +397,7 @@ function create(resourceType, data, options, callback) { * deliveryChannels: ['SMS'], * dataSets: [], * }); - * @example -a `dataElement` + * @example a dataElement * update('dataElements', 'FTRrcoaog83', { * aggregationType: 'SUM', * domainType: 'AGGREGATE', @@ -404,26 +405,26 @@ function create(resourceType, data, options, callback) { * name: 'Paracetamol', * shortName: 'Para', * }); - * @example -a `dataElementGroup` + * @example a dataElementGroup * update('dataElementGroups', 'QrprHT61XFk', { * name: 'Data Element Group 1', * dataElements: [], * }); - * @example -a `dataElementGroupSet` + * @example a dataElementGroupSet * update('dataElementGroupSets', 'VxWloRvAze8', { * name: 'Data Element Group Set 4', * dataDimension: true, * shortName: 'DEGS4', * dataElementGroups: [], * }); - * @example -a `dataValueSet` + * @example a dataValueSet * update('dataValueSets', 'AsQj6cDsUq4', { * dataElement: 'f7n9E0hX8qk', * period: '201401', * orgUnit: 'DiszpKrYNg8', * value: '12', * }); - * @example -a `dataValueSet` with related `dataValues` + * @example a dataValueSet with related dataValues * update('dataValueSets', 'Ix2HsbDMLea', { * dataSet: 'pBOMPrpg1QX', * completeDate: '2014-02-03', @@ -492,15 +493,20 @@ function update(resourceType, path, data, options, callback) { * @param {Object} [options] - Optional `options` to define URL parameters 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 Get all data values for the 'pBOMPrpg1QX' dataset. + * @example all data values for the 'pBOMPrpg1QX' dataset * get('dataValueSets', { * dataSet: 'pBOMPrpg1QX', * orgUnit: 'DiszpKrYNg8', * period: '201401', * fields: '*', * }); - * @example get all programs for an organization unit + * @example 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', + * filters: ['flGbXLXCrEo:Eq:124'], + * }); */ @@ -587,7 +593,7 @@ function get(resourceType, filters, options, callback) { * @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` * @returns {Operation} - * @example Example getting a list of `parameters allowed` on a given `endpoint` for specific `http method` + * @example a list of parameters allowed on a given endpoint for specific http method * discover('post', '/trackedEntityInstances') */ @@ -650,11 +656,8 @@ function discover(httpMethod, endpoint) { * @param {{apiVersion: number,operationName: string,responseType: string}} [options] - Optional options for update method. Defaults to `{operationName: 'patch', apiVersion: state.configuration.apiVersion, responseType: 'json'}` * @param {function} [callback] - Optional callback to handle the response * @returns {Operation} - * @example Example `patching` a `data element` - * patch('dataElements', 'FTRrcoaog83', - * { - * name: 'New Name', - * }); + * @example a dataElement + * patch('dataElements', 'FTRrcoaog83', { name: 'New Name' }); */ // TODO: @Elias, can this be deleted in favor of update? How does DHIS2 handle PATCH vs PUT? // I need to investigate on this. But I think DHIS 2 forces to send all properties back when we do an update. If that's confirmed then this may be needed. @@ -720,13 +723,13 @@ function patch(resourceType, path, data, params, options, callback) { * @param {{apiVersion: number,operationName: string,resourceType: string}} [options] - Optional `options` for `del` operation. Defaults to `{operationName: 'delete', apiVersion: state.configuration.apiVersion, responseType: 'json'}` * @param {function} [callback] - Optional callback to handle the response * @returns {Operation} - * @example Example`deleting` a `tracked entity instance` - * del('trackedEntityInstances', 'LcRd6Nyaq7T'); + * @example a tracked entity instance + * destroy('trackedEntityInstances', 'LcRd6Nyaq7T'); */ // TODO: @Elias, can this be implemented using the same pattern as update but without data? -function del(resourceType, path, data, params, options, callback) { +function destroy(resourceType, path, data, params, options, callback) { return state => { var _options$operationNam2, _options4, _options$responseType2, _options5, _queryParams3, _queryParams4, _options$apiVersion2, _options6, _CONTENT_TYPES$respon2; @@ -774,34 +777,52 @@ function del(resourceType, path, data, params, options, callback) { * Gets an attribute value by its case-insensitive display name * @public * @example - * attrVal(tei.attributes, 'first name') + * findAttributeValue(state.data.trackedEntityInstances[0], 'first name') * @function - * @param {Object} tei - A tracked entity instance (TEI) object - * @param {string} attributeName - The 'displayName' to search for in the TEI's attributes + * @param {Object} trackedEntityInstance - A tracked entity instance (TEI) object + * @param {string} attributeDisplayName - The 'displayName' to search for in the TEI's attributes * @returns {string} */ -function attrVal(tei, attributeName) { - var _tei$attributes, _tei$attributes$find; +function findAttributeValue(trackedEntityInstance, attributeDisplayName) { + var _trackedEntityInstanc, _trackedEntityInstanc2; - return tei === null || tei === void 0 ? void 0 : (_tei$attributes = tei.attributes) === null || _tei$attributes === void 0 ? void 0 : (_tei$attributes$find = _tei$attributes.find(a => (a === null || a === void 0 ? void 0 : a.displayName.toLowerCase()) == attributeName.toLowerCase())) === null || _tei$attributes$find === void 0 ? void 0 : _tei$attributes$find.value; + return trackedEntityInstance === null || trackedEntityInstance === void 0 ? void 0 : (_trackedEntityInstanc = trackedEntityInstance.attributes) === null || _trackedEntityInstanc === void 0 ? void 0 : (_trackedEntityInstanc2 = _trackedEntityInstanc.find(a => (a === null || a === void 0 ? void 0 : a.displayName.toLowerCase()) == attributeDisplayName.toLowerCase())) === null || _trackedEntityInstanc2 === void 0 ? void 0 : _trackedEntityInstanc2.value; } /** * Converts an attribute ID and value into a DSHI2 attribute object * @public * @example - * attribute('w75KJ2mc4zz', 'Elias') + * attr('w75KJ2mc4zz', 'Elias') + * @function + * @param {string} attribute - A tracked entity instance (TEI) attribute ID. + * @param {string} value - The value for that attribute. + * @returns {object} + */ + + +function attr(attribute, value) { + return { + attribute, + value + }; +} +/** + * Converts a dataElement and value into a DSHI2 dataValue object + * @public + * @example + * dv('f7n9E0hX8qk', 12) * @function - * @param {string} attributeId - A tracked entity instance (TEI) attribute ID. - * @param {string} attributeValue - The value for that attribute. + * @param {string} dataElement - A data element ID. + * @param {string} value - The value for that data element. * @returns {object} */ -function attribute(attributeId, attributeValue) { +function dv(dataElement, value) { return { - attribute: attributeId, - value: attributeValue + dataElement, + value }; } \ No newline at end of file diff --git a/src/Adaptor.js b/src/Adaptor.js index b35e385..03e9383 100644 --- a/src/Adaptor.js +++ b/src/Adaptor.js @@ -128,19 +128,19 @@ axios.interceptors.response.use( * @param {Object} [options] - Optional `options` to define URL parameters (E.g. `filters`, `dimensions` and `import parameters`), axios configurations (E.g. `auth`) and DHIS 2 api version to use. * @param {function} [callback] - Optional callback to handle the response * @returns {Operation} - * @example -a `program` + * @example a program * create('programs', { * name: 'name 20', * shortName: 'n20', * programType: 'WITHOUT_REGISTRATION', * }); - * @example -an `event` + * @example an event * create('events', { * program: 'eBAyeGv0exc', * orgUnit: 'DiszpKrYNg8', * status: 'COMPLETED', * }); - * @example -a `trackedEntityInstance` + * @example a trackedEntityInstance * create('trackedEntityInstances', { * orgUnit: 'TSyzvBiovKh', * trackedEntityType: 'nEenWmSyUEp', @@ -151,9 +151,9 @@ axios.interceptors.response.use( * }, * ] * }); - * @example -a `dataSet` + * @example a dataSet * create('dataSets', { name: 'OpenFn Data Set', periodType: 'Monthly' }); - * @example -a `dataSetNotification` + * @example a dataSetNotification * create('dataSetNotificationTemplates', { * dataSetNotificationTrigger: 'DATA_SET_COMPLETION', * notificationRecipient: 'ORGANISATION_UNIT_CONTACT', @@ -162,7 +162,7 @@ axios.interceptors.response.use( * deliveryChannels: ['SMS'], * dataSets: [], * }); - * @example -a `dataElement` + * @example a dataElement * create('dataElements', { * aggregationType: 'SUM', * domainType: 'AGGREGATE', @@ -170,26 +170,26 @@ axios.interceptors.response.use( * name: 'Paracetamol', * shortName: 'Para', * }); - * @example -a `dataElementGroup` + * @example a dataElementGroup * create('dataElementGroups', { * name: 'Data Element Group 1', * dataElements: [], * }); - * @example -a `dataElementGroupSet` + * @example a dataElementGroupSet * create('dataElementGroupSets', { * name: 'Data Element Group Set 4', * dataDimension: true, * shortName: 'DEGS4', * dataElementGroups: [], * }); - * @example -a `dataValueSet` + * @example a dataValueSet * create('dataValueSets', { * dataElement: 'f7n9E0hX8qk', * period: '201401', * orgUnit: 'DiszpKrYNg8', * value: '12', * }); - * @example -a `dataValueSet` with related `dataValues` + * @example a dataValueSet with related dataValues * create('dataValueSets', { * dataSet: 'pBOMPrpg1QX', * completeDate: '2014-02-03', @@ -210,7 +210,7 @@ axios.interceptors.response.use( * }, * ], * }); - * @example -an `enrollment` + * @example an enrollment * create('enrollments', { * trackedEntityInstance: 'bmshzEacgxa', * orgUnit: 'TSyzvBiovKh', @@ -254,13 +254,13 @@ export function create(resourceType, data, options, callback) { * @param {Object} [options] - Optional `options` to define URL parameters (E.g. `filters`, `dimensions` and `import parameters`), axios configurations (E.g. `auth`) and DHIS 2 api version to use. * @param {function} [callback] - Optional callback to handle the response * @returns {Operation} - * @example -a program + * @example a program * update('programs', 'qAZJCrNJK8H', { * name: '14e1aa02c3f0a31618e096f2c6d03bed', * shortName: '14e1aa02', * programType: 'WITHOUT_REGISTRATION', * }); - * @example an `event` + * @example an event * update('events', 'PVqUD2hvU4E', { * program: 'eBAyeGv0exc', * orgUnit: 'Ngelehun CHC', @@ -268,7 +268,7 @@ export function create(resourceType, data, options, callback) { * storedBy: 'admin', * dataValues: [], * }); - * @example a `trackedEntityInstance` + * @example a trackedEntityInstance * update('trackedEntityInstances', 'IeQfgUtGPq2', { * created: '2015-08-06T21:12:37.256', * orgUnit: 'TSyzvBiovKh', @@ -308,9 +308,9 @@ export function create(resourceType, data, options, callback) { * }, * ], * }); - * @example -a `dataSet` + * @example a dataSet * update('dataSets', 'lyLU2wR22tC', { name: 'OpenFN Data Set', periodType: 'Weekly' }); - * @example -a `dataSetNotification` + * @example a dataSetNotification * update('dataSetNotificationTemplates', 'VbQBwdm1wVP', { * dataSetNotificationTrigger: 'DATA_SET_COMPLETION', * notificationRecipient: 'ORGANISATION_UNIT_CONTACT', @@ -319,7 +319,7 @@ export function create(resourceType, data, options, callback) { * deliveryChannels: ['SMS'], * dataSets: [], * }); - * @example -a `dataElement` + * @example a dataElement * update('dataElements', 'FTRrcoaog83', { * aggregationType: 'SUM', * domainType: 'AGGREGATE', @@ -327,26 +327,26 @@ export function create(resourceType, data, options, callback) { * name: 'Paracetamol', * shortName: 'Para', * }); - * @example -a `dataElementGroup` + * @example a dataElementGroup * update('dataElementGroups', 'QrprHT61XFk', { * name: 'Data Element Group 1', * dataElements: [], * }); - * @example -a `dataElementGroupSet` + * @example a dataElementGroupSet * update('dataElementGroupSets', 'VxWloRvAze8', { * name: 'Data Element Group Set 4', * dataDimension: true, * shortName: 'DEGS4', * dataElementGroups: [], * }); - * @example -a `dataValueSet` + * @example a dataValueSet * update('dataValueSets', 'AsQj6cDsUq4', { * dataElement: 'f7n9E0hX8qk', * period: '201401', * orgUnit: 'DiszpKrYNg8', * value: '12', * }); - * @example -a `dataValueSet` with related `dataValues` + * @example a dataValueSet with related dataValues * update('dataValueSets', 'Ix2HsbDMLea', { * dataSet: 'pBOMPrpg1QX', * completeDate: '2014-02-03', @@ -411,15 +411,20 @@ export function update(resourceType, path, data, options, callback) { * @param {Object} [options] - Optional `options` to define URL parameters 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 Get all data values for the 'pBOMPrpg1QX' dataset. + * @example all data values for the 'pBOMPrpg1QX' dataset * get('dataValueSets', { * dataSet: 'pBOMPrpg1QX', * orgUnit: 'DiszpKrYNg8', * period: '201401', * fields: '*', * }); - * @example get all programs for an organization unit + * @example 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', + * filters: ['flGbXLXCrEo:Eq:124'], + * }); */ export function get(resourceType, filters, options, callback) { return state => { @@ -502,7 +507,7 @@ export function get(resourceType, filters, options, callback) { * @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` * @returns {Operation} - * @example Example getting a list of `parameters allowed` on a given `endpoint` for specific `http method` + * @example a list of parameters allowed on a given endpoint for specific http method * discover('post', '/trackedEntityInstances') */ export function discover(httpMethod, endpoint) { @@ -603,11 +608,8 @@ export function discover(httpMethod, endpoint) { * @param {{apiVersion: number,operationName: string,responseType: string}} [options] - Optional options for update method. Defaults to `{operationName: 'patch', apiVersion: state.configuration.apiVersion, responseType: 'json'}` * @param {function} [callback] - Optional callback to handle the response * @returns {Operation} - * @example Example `patching` a `data element` - * patch('dataElements', 'FTRrcoaog83', - * { - * name: 'New Name', - * }); + * @example a dataElement + * patch('dataElements', 'FTRrcoaog83', { name: 'New Name' }); */ // TODO: @Elias, can this be deleted in favor of update? How does DHIS2 handle PATCH vs PUT? // I need to investigate on this. But I think DHIS 2 forces to send all properties back when we do an update. If that's confirmed then this may be needed. @@ -686,11 +688,11 @@ export function patch(resourceType, path, data, params, options, callback) { * @param {{apiVersion: number,operationName: string,resourceType: string}} [options] - Optional `options` for `del` operation. Defaults to `{operationName: 'delete', apiVersion: state.configuration.apiVersion, responseType: 'json'}` * @param {function} [callback] - Optional callback to handle the response * @returns {Operation} - * @example Example`deleting` a `tracked entity instance` - * del('trackedEntityInstances', 'LcRd6Nyaq7T'); + * @example a tracked entity instance + * destroy('trackedEntityInstances', 'LcRd6Nyaq7T'); */ // TODO: @Elias, can this be implemented using the same pattern as update but without data? -export function del(resourceType, path, data, params, options, callback) { +export function destroy(resourceType, path, data, params, options, callback) { return state => { resourceType = expandReferences(resourceType)(state); @@ -754,15 +756,18 @@ export function del(resourceType, path, data, params, options, callback) { * Gets an attribute value by its case-insensitive display name * @public * @example - * attrVal(tei.attributes, 'first name') + * findAttributeValue(state.data.trackedEntityInstances[0], 'first name') * @function - * @param {Object} tei - A tracked entity instance (TEI) object - * @param {string} attributeName - The 'displayName' to search for in the TEI's attributes + * @param {Object} trackedEntityInstance - A tracked entity instance (TEI) object + * @param {string} attributeDisplayName - The 'displayName' to search for in the TEI's attributes * @returns {string} */ -export function attrVal(tei, attributeName) { - return tei?.attributes?.find( - a => a?.displayName.toLowerCase() == attributeName.toLowerCase() +export function findAttributeValue( + trackedEntityInstance, + attributeDisplayName +) { + return trackedEntityInstance?.attributes?.find( + a => a?.displayName.toLowerCase() == attributeDisplayName.toLowerCase() )?.value; } @@ -770,17 +775,28 @@ export function attrVal(tei, attributeName) { * Converts an attribute ID and value into a DSHI2 attribute object * @public * @example - * attribute('w75KJ2mc4zz', 'Elias') + * attr('w75KJ2mc4zz', 'Elias') * @function - * @param {string} attributeId - A tracked entity instance (TEI) attribute ID. - * @param {string} attributeValue - The value for that attribute. + * @param {string} attribute - A tracked entity instance (TEI) attribute ID. + * @param {string} value - The value for that attribute. * @returns {object} */ -export function attribute(attributeId, attributeValue) { - return { - attribute: attributeId, - value: attributeValue, - }; +export function attr(attribute, value) { + return { attribute, value }; +} + +/** + * Converts a dataElement and value into a DSHI2 dataValue object + * @public + * @example + * dv('f7n9E0hX8qk', 12) + * @function + * @param {string} dataElement - A data element ID. + * @param {string} value - The value for that data element. + * @returns {object} + */ +export function dv(dataElement, value) { + return { dataElement, value }; } export { From 62b4e24c7bfd3724624044d9eb4a182bee2fdeda Mon Sep 17 00:00:00 2001 From: Taylor Downs Date: Wed, 22 Dec 2021 06:13:07 +0000 Subject: [PATCH 21/26] prerelease version bump --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4d5f899..c7f156c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@openfn/language-dhis2", - "version": "3.0.0-2", + "version": "3.0.0-3", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 425ad01..1c1311c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@openfn/language-dhis2", - "version": "3.0.0-2", + "version": "3.0.0-3", "description": "DHIS2 Language Pack for OpenFn", "homepage": "https://docs.openfn.org", "repository": { From 8f1bbe85ccb6cf985dbd64856c096be530827177 Mon Sep 17 00:00:00 2001 From: Taylor Downs Date: Wed, 22 Dec 2021 06:44:30 -0700 Subject: [PATCH 22/26] standardize and remove custom URL param builder --- lib/Adaptor.js | 9 +++++---- lib/Utils.js | 27 ++++++++++++--------------- src/Adaptor.js | 19 +++++++++---------- src/Utils.js | 17 ----------------- test/index.js | 23 +++-------------------- test/integration.js | 2 +- 6 files changed, 30 insertions(+), 67 deletions(-) diff --git a/lib/Adaptor.js b/lib/Adaptor.js index 197293c..4246fbf 100644 --- a/lib/Adaptor.js +++ b/lib/Adaptor.js @@ -149,6 +149,7 @@ function configMigrationHelper(state) { _axios.default.interceptors.response.use(function (response) { var _response$headers$con, _response; + console.log(response); const contentType = (_response$headers$con = response.headers['content-type']) === null || _response$headers$con === void 0 ? void 0 : _response$headers$con.split(';')[0]; const acceptHeaders = response.config.headers['Accept'].split(';')[0].split(','); @@ -311,7 +312,7 @@ function create(resourceType, data, options, callback) { return (0, _Client.request)(configuration, { method: 'post', url: (0, _Utils.generateUrl)(configuration, options, resourceType), - params: params && (0, _Utils.buildUrlParams)(params), + params, data: (0, _Utils.nestArray)(data, resourceType), ...requestConfig }).then(result => { @@ -473,7 +474,7 @@ function update(resourceType, path, data, options, callback) { return (0, _Client.request)(configuration, { method: 'put', url: `${(0, _Utils.generateUrl)(configuration, options, resourceType)}/${path}`, - params: params && (0, _Utils.buildUrlParams)(params), + params, data, ...requestConfig }).then(result => { @@ -526,9 +527,9 @@ function get(resourceType, filters, options, callback) { return (0, _Client.request)(configuration, { method: 'get', url: (0, _Utils.generateUrl)(configuration, options, resourceType), - params: (0, _Utils.buildUrlParams)({ ...filters, + params: { ...filters, ...params - }), + }, responseType: 'json', ...requestConfig }).then(result => { diff --git a/lib/Utils.js b/lib/Utils.js index 408c0a2..b56d421 100644 --- a/lib/Utils.js +++ b/lib/Utils.js @@ -8,7 +8,6 @@ exports.handleResponse = handleResponse; exports.prettyJson = prettyJson; exports.nestArray = nestArray; exports.generateUrl = generateUrl; -exports.buildUrlParams = buildUrlParams; exports.Log = exports.CONTENT_TYPES = void 0; var _languageCommon = require("@openfn/language-common"); @@ -76,17 +75,15 @@ function generateUrl(configuration, options, resourceType) { const apiMessage = apiVersion ? `Using DHIS2 api version ${apiVersion}` : 'Using latest available version of the DHIS2 api on this server.'; console.log(apiMessage); return buildUrl(urlString, hostUrl, apiVersion); -} - -function buildUrlParams(params) { - const filters = params === null || params === void 0 ? void 0 : params.filters; - const dimensions = params === null || params === void 0 ? void 0 : params.dimensions; // We remove filters and dimensions before building standard search params. - - params === null || params === void 0 ? true : delete params.filters; - params === null || params === void 0 ? true : delete params.dimensions; - const urlParams = new URLSearchParams(params); // Then we re-apply the filters and dimensions in this dhis2-specific way. - - filters === null || filters === void 0 ? void 0 : filters.map(f => urlParams.append('filter', f)); - dimensions === null || dimensions === void 0 ? void 0 : dimensions.map(d => urlParams.append('dimension', d)); - return urlParams; -} \ No newline at end of file +} // export function buildUrlParams(params) { +// const filters = params?.filters; +// const dimensions = params?.dimensions; +// // We remove filters and dimensions before building standard search params. +// delete params?.filters; +// delete params?.dimensions; +// const urlParams = new URLSearchParams(params); +// // Then we re-apply the filters and dimensions in this dhis2-specific way. +// filters?.map(f => urlParams.append('filter', f)); +// dimensions?.map(d => urlParams.append('dimension', d)); +// return urlParams; +// } \ No newline at end of file diff --git a/src/Adaptor.js b/src/Adaptor.js index 03e9383..646da6f 100644 --- a/src/Adaptor.js +++ b/src/Adaptor.js @@ -8,7 +8,6 @@ import { import { indexOf } from 'lodash'; import { buildUrl, - buildUrlParams, CONTENT_TYPES, generateUrl, handleResponse, @@ -125,7 +124,7 @@ axios.interceptors.response.use( * @function * @param {string} resourceType - Type of resource to create. E.g. `trackedEntityInstances`, `programs`, `events`, ... * @param {Object} data - 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 (E.g. `filters`, `dimensions` and `import parameters`), axios configurations (E.g. `auth`) and DHIS 2 api version to use. + * @param {Object} [options] - Optional `options` to define URL parameters (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 @@ -233,7 +232,7 @@ export function create(resourceType, data, options, callback) { return request(configuration, { method: 'post', url: generateUrl(configuration, options, resourceType), - params: params && buildUrlParams(params), + params, data: nestArray(data, resourceType), ...requestConfig, }).then(result => { @@ -251,7 +250,7 @@ export function create(resourceType, data, options, callback) { * @param {string} resourceType - The type of resource to be updated. E.g. `dataElements`, `organisationUnits`, etc. * @param {string} path - The `id` or `path` to the `object` to be updated. E.g. `FTRrcoaog83` or `FTRrcoaog83/{collection-name}/{object-id}` * @param {Object} data - Data to update. It requires to send `all required fields` or the `full body`. If you want `partial updates`, use `patch` operation. - * @param {Object} [options] - Optional `options` to define URL parameters (E.g. `filters`, `dimensions` and `import parameters`), axios configurations (E.g. `auth`) and DHIS 2 api version to use. + * @param {Object} [options] - Optional `options` to define URL parameters (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 @@ -391,7 +390,7 @@ export function update(resourceType, path, data, options, callback) { return request(configuration, { method: 'put', url: `${generateUrl(configuration, options, resourceType)}/${path}`, - params: params && buildUrlParams(params), + params, data, ...requestConfig, }).then(result => { @@ -407,7 +406,7 @@ export function update(resourceType, path, data, options, callback) { * @public * @function * @param {string} resourceType - The type of resource to get(use its `plural` name). E.g. `dataElements`, `trackedEntityInstances`,`organisationUnits`, etc. - * @param {Object} filters - Filters to limit what resources are retrieved. + * @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 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 @@ -423,15 +422,15 @@ export function update(resourceType, path, data, options, callback) { * @example a single tracked entity instance by a unique external ID * get('trackedEntityInstances', { * ou: 'DiszpKrYNg8', - * filters: ['flGbXLXCrEo:Eq:124'], + * filter: ['flGbXLXCrEo:Eq:124', 'w75KJ2mc4zz:Eq:John'], * }); */ -export function get(resourceType, filters, options, callback) { +export function get(resourceType, query, options, callback) { return state => { console.log(`Preparing get operation...`); resourceType = expandReferences(resourceType)(state); - filters = expandReferences(filters)(state); + query = expandReferences(query)(state); options = expandReferences(options)(state); const { params, requestConfig } = options || {}; @@ -440,7 +439,7 @@ export function get(resourceType, filters, options, callback) { return request(configuration, { method: 'get', url: generateUrl(configuration, options, resourceType), - params: buildUrlParams({ ...filters, ...params }), + params: { ...query, ...params }, responseType: 'json', ...requestConfig, }).then(result => { diff --git a/src/Utils.js b/src/Utils.js index efd7085..48536ff 100644 --- a/src/Utils.js +++ b/src/Utils.js @@ -59,20 +59,3 @@ export function generateUrl(configuration, options, resourceType) { return buildUrl(urlString, hostUrl, apiVersion); } - -export function buildUrlParams(params) { - const filters = params?.filters; - const dimensions = params?.dimensions; - - // We remove filters and dimensions before building standard search params. - delete params?.filters; - delete params?.dimensions; - - const urlParams = new URLSearchParams(params); - - // Then we re-apply the filters and dimensions in this dhis2-specific way. - filters?.map(f => urlParams.append('filter', f)); - dimensions?.map(d => urlParams.append('dimension', d)); - - return urlParams; -} diff --git a/test/index.js b/test/index.js index 6ffb9ad..0fed5e1 100644 --- a/test/index.js +++ b/test/index.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { execute, create, update, get, upsert } from '../lib/Adaptor'; import { dataValue } from '@openfn/language-common'; -import { buildUrl, buildUrlParams, generateUrl, nestArray } from '../lib/Utils'; +import { buildUrl, generateUrl, nestArray } from '../lib/Utils'; import nock from 'nock'; const testServer = nock('https://play.dhis2.org/2.36.4'); @@ -327,7 +327,7 @@ describe('URL builders', () => { const options = { ...fixture.options, apiVersion: 33, - params: { filters: ['a:eq:b', 'c:ge:d'] }, + params: { filter: ['a:eq:b', 'c:ge:d'] }, }; const finalURL = generateUrl( @@ -335,30 +335,13 @@ describe('URL builders', () => { options, fixture.resourceType ); + const expectedURL = 'https://play.dhis2.org/2.36.4/api/33/dataValueSets'; expect(finalURL).to.eq(expectedURL); done(); }); }); - - describe('buildURLParams', () => { - it('should handle special filter and dimensions params and build the rest per usual', () => { - const params = { - dryRun: true, - filters: ['sex:eq:male', 'origin:eq:senegal'], - someNonesense: 'other', - dimensions: ['dx:fbfJHSPpUQD', 'ou:O6uvpzGd5pu;lc3eMKXaEfw'], - }; - - const finalParams = buildUrlParams(params).toString(); - - const expected = - 'dryRun=true&someNonesense=other&filter=sex%3Aeq%3Amale&filter=origin%3Aeq%3Asenegal&dimension=dx%3AfbfJHSPpUQD&dimension=ou%3AO6uvpzGd5pu%3Blc3eMKXaEfw'; - - expect(finalParams).to.eq(expected); - }); - }); }); describe('nestArray', () => { diff --git a/test/integration.js b/test/integration.js index 5bf2720..95cba6c 100644 --- a/test/integration.js +++ b/test/integration.js @@ -263,7 +263,7 @@ describe('get', () => { data: {}, }; - it('should get dataValueSets matching the filters specified', async () => { + it('should get dataValueSets matching the query specified', async () => { const finalState = await execute( get('dataValueSets', { dataSet: 'pBOMPrpg1QX', From 60eec59ac4f94f60feba1e488b5fd2228447bd91 Mon Sep 17 00:00:00 2001 From: Taylor Downs Date: Wed, 22 Dec 2021 06:51:32 -0700 Subject: [PATCH 23/26] update travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index ff1fa2c..ba21454 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,5 +3,5 @@ node_js: # NOTE: to test current stable, uncomment "node" # - node # Current stable, i.e. 13 in October, 2019 # - lts/* # Most recent LTS version, i.e. 12 in October, 2019 -- 12 # Explicitly include an active LTS version +- 14 # Explicitly include an active LTS version # - 10 # Explicitly include an active LTS version \ No newline at end of file From 217c1b9a38652b077372b9dd515c45ea47cd1ccb Mon Sep 17 00:00:00 2001 From: Taylor Downs Date: Wed, 22 Dec 2021 06:54:10 -0700 Subject: [PATCH 24/26] build and fix param log in client --- ast.json | 12 ++++++------ lib/Adaptor.js | 15 +++++++-------- lib/Client.js | 2 +- lib/Utils.js | 13 +------------ src/Client.js | 2 +- 5 files changed, 16 insertions(+), 28 deletions(-) diff --git a/ast.json b/ast.json index 2fa6fda..e83e49e 100644 --- a/ast.json +++ b/ast.json @@ -41,7 +41,7 @@ }, { "title": "param", - "description": "Optional `options` to define URL parameters (E.g. `filters`, `dimensions` and `import parameters`), axios configurations (E.g. `auth`) and DHIS 2 api version to use.", + "description": "Optional `options` to define URL parameters (E.g. `filter`, `dimension` and other import parameters), request config (E.g. `auth`) and the DHIS2 apiVersion.", "type": { "type": "OptionalType", "expression": { @@ -181,7 +181,7 @@ }, { "title": "param", - "description": "Optional `options` to define URL parameters (E.g. `filters`, `dimensions` and `import parameters`), axios configurations (E.g. `auth`) and DHIS 2 api version to use.", + "description": "Optional `options` to define URL parameters (E.g. `filter`, `dimension` and other import parameters), request config (E.g. `auth`) and the DHIS2 apiVersion.", "type": { "type": "OptionalType", "expression": { @@ -274,7 +274,7 @@ "name": "get", "params": [ "resourceType", - "filters", + "query", "options", "callback" ], @@ -302,12 +302,12 @@ }, { "title": "param", - "description": "Filters to limit what resources are retrieved.", + "description": "A query object that will limit what resources are retrieved when converted into request params.", "type": { "type": "NameExpression", "name": "Object" }, - "name": "filters" + "name": "query" }, { "title": "param", @@ -353,7 +353,7 @@ }, { "title": "example", - "description": "get('trackedEntityInstances', {\n ou: 'DiszpKrYNg8',\n filters: ['flGbXLXCrEo:Eq:124'],\n});", + "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" } ] diff --git a/lib/Adaptor.js b/lib/Adaptor.js index 4246fbf..f4b5c54 100644 --- a/lib/Adaptor.js +++ b/lib/Adaptor.js @@ -149,7 +149,6 @@ function configMigrationHelper(state) { _axios.default.interceptors.response.use(function (response) { var _response$headers$con, _response; - console.log(response); const contentType = (_response$headers$con = response.headers['content-type']) === null || _response$headers$con === void 0 ? void 0 : _response$headers$con.split(';')[0]; const acceptHeaders = response.config.headers['Accept'].split(';')[0].split(','); @@ -200,7 +199,7 @@ _axios.default.interceptors.response.use(function (response) { * @function * @param {string} resourceType - Type of resource to create. E.g. `trackedEntityInstances`, `programs`, `events`, ... * @param {Object} data - 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 (E.g. `filters`, `dimensions` and `import parameters`), axios configurations (E.g. `auth`) and DHIS 2 api version to use. + * @param {Object} [options] - Optional `options` to define URL parameters (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 @@ -330,7 +329,7 @@ function create(resourceType, data, options, callback) { * @param {string} resourceType - The type of resource to be updated. E.g. `dataElements`, `organisationUnits`, etc. * @param {string} path - The `id` or `path` to the `object` to be updated. E.g. `FTRrcoaog83` or `FTRrcoaog83/{collection-name}/{object-id}` * @param {Object} data - Data to update. It requires to send `all required fields` or the `full body`. If you want `partial updates`, use `patch` operation. - * @param {Object} [options] - Optional `options` to define URL parameters (E.g. `filters`, `dimensions` and `import parameters`), axios configurations (E.g. `auth`) and DHIS 2 api version to use. + * @param {Object} [options] - Optional `options` to define URL parameters (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 @@ -490,7 +489,7 @@ function update(resourceType, path, data, options, callback) { * @public * @function * @param {string} resourceType - The type of resource to get(use its `plural` name). E.g. `dataElements`, `trackedEntityInstances`,`organisationUnits`, etc. - * @param {Object} filters - Filters to limit what resources are retrieved. + * @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 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 @@ -506,16 +505,16 @@ function update(resourceType, path, data, options, callback) { * @example a single tracked entity instance by a unique external ID * get('trackedEntityInstances', { * ou: 'DiszpKrYNg8', - * filters: ['flGbXLXCrEo:Eq:124'], + * filter: ['flGbXLXCrEo:Eq:124', 'w75KJ2mc4zz:Eq:John'], * }); */ -function get(resourceType, filters, options, callback) { +function get(resourceType, query, options, callback) { return state => { console.log(`Preparing get operation...`); resourceType = (0, _languageCommon.expandReferences)(resourceType)(state); - filters = (0, _languageCommon.expandReferences)(filters)(state); + query = (0, _languageCommon.expandReferences)(query)(state); options = (0, _languageCommon.expandReferences)(options)(state); const { params, @@ -527,7 +526,7 @@ function get(resourceType, filters, options, callback) { return (0, _Client.request)(configuration, { method: 'get', url: (0, _Utils.generateUrl)(configuration, options, resourceType), - params: { ...filters, + params: { ...query, ...params }, responseType: 'json', diff --git a/lib/Client.js b/lib/Client.js index d99c1aa..440f2d1 100644 --- a/lib/Client.js +++ b/lib/Client.js @@ -29,7 +29,7 @@ function request({ params } = axiosRequest; console.log(`Sending ${method} request to ${url}`); - if (params) console.log(` with params: ${params}`); // NOTE: We don't follow redirects for unsafe methods: https://github.com/axios/axios/issues/2460 + if (params) console.log(` with params:`, params); // NOTE: We don't follow redirects for unsafe methods: https://github.com/axios/axios/issues/2460 const safeRedirect = ['get', 'head', 'options', 'trace'].includes(method.toLowerCase()); return (0, _axios.default)({ diff --git a/lib/Utils.js b/lib/Utils.js index b56d421..c4a1e8c 100644 --- a/lib/Utils.js +++ b/lib/Utils.js @@ -75,15 +75,4 @@ function generateUrl(configuration, options, resourceType) { const apiMessage = apiVersion ? `Using DHIS2 api version ${apiVersion}` : 'Using latest available version of the DHIS2 api on this server.'; console.log(apiMessage); return buildUrl(urlString, hostUrl, apiVersion); -} // export function buildUrlParams(params) { -// const filters = params?.filters; -// const dimensions = params?.dimensions; -// // We remove filters and dimensions before building standard search params. -// delete params?.filters; -// delete params?.dimensions; -// const urlParams = new URLSearchParams(params); -// // Then we re-apply the filters and dimensions in this dhis2-specific way. -// filters?.map(f => urlParams.append('filter', f)); -// dimensions?.map(d => urlParams.append('dimension', d)); -// return urlParams; -// } \ No newline at end of file +} \ No newline at end of file diff --git a/src/Client.js b/src/Client.js index dcbee31..21237dd 100644 --- a/src/Client.js +++ b/src/Client.js @@ -14,7 +14,7 @@ export function request({ username, password }, axiosRequest) { const { method, url, params } = axiosRequest; console.log(`Sending ${method} request to ${url}`); - if (params) console.log(` with params: ${params}`); + if (params) console.log(` with params:`, params); // NOTE: We don't follow redirects for unsafe methods: https://github.com/axios/axios/issues/2460 const safeRedirect = ['get', 'head', 'options', 'trace'].includes( From 93058676c84fc10196a6cf229910a0d97ac1bdc3 Mon Sep 17 00:00:00 2001 From: Taylor Downs Date: Wed, 22 Dec 2021 07:49:12 -0700 Subject: [PATCH 25/26] migrate patch and destroy --- ast.json | 61 ++------------- lib/Adaptor.js | 102 ++++++++++--------------- lib/Utils.js | 6 +- package-lock.json | 24 +++--- package.json | 2 +- src/Adaptor.js | 191 ++++++++++++++++------------------------------ src/Utils.js | 7 +- 7 files changed, 131 insertions(+), 262 deletions(-) diff --git a/ast.json b/ast.json index e83e49e..57a4d89 100644 --- a/ast.json +++ b/ast.json @@ -41,7 +41,7 @@ }, { "title": "param", - "description": "Optional `options` to define URL parameters (E.g. `filter`, `dimension` and other import parameters), request config (E.g. `auth`) and the DHIS2 apiVersion.", + "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": { @@ -181,7 +181,7 @@ }, { "title": "param", - "description": "Optional `options` to define URL parameters (E.g. `filter`, `dimension` and other import parameters), request config (E.g. `auth`) and the DHIS2 apiVersion.", + "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": { @@ -311,7 +311,7 @@ }, { "title": "param", - "description": "Optional `options` to define URL parameters beyond filters, request configuration (e.g. `auth`) and DHIS2 api version to use.", + "description": "Optional `options` to define URL parameters via params beyond filters, request configuration (e.g. `auth`) and DHIS2 api version to use.", "type": { "type": "OptionalType", "expression": { @@ -420,7 +420,6 @@ "resourceType", "path", "data", - "params", "options", "callback" ], @@ -466,7 +465,7 @@ }, { "title": "param", - "description": "Optional `update` parameters 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}", + "description": "Optional configuration, including params for the update ({preheatCache: true, strategy: 'UPDATE', mergeMode: 'REPLACE'}). Defaults to `{operationName: 'patch', apiVersion: state.configuration.apiVersion, responseType: 'json'}`", "type": { "type": "OptionalType", "expression": { @@ -474,43 +473,6 @@ "name": "Object" } }, - "name": "params" - }, - { - "title": "param", - "description": "Optional options for update method. Defaults to `{operationName: 'patch', apiVersion: state.configuration.apiVersion, responseType: 'json'}`", - "type": { - "type": "OptionalType", - "expression": { - "type": "RecordType", - "fields": [ - { - "type": "FieldType", - "key": "apiVersion", - "value": { - "type": "NameExpression", - "name": "number" - } - }, - { - "type": "FieldType", - "key": "operationName", - "value": { - "type": "NameExpression", - "name": "string" - } - }, - { - "type": "FieldType", - "key": "responseType", - "value": { - "type": "NameExpression", - "name": "string" - } - } - ] - } - }, "name": "options" }, { @@ -548,7 +510,6 @@ "resourceType", "path", "data", - "params", "options", "callback" ], @@ -597,19 +558,7 @@ }, { "title": "param", - "description": "Optional `update` parameters 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}", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "Object" - } - }, - "name": "params" - }, - { - "title": "param", - "description": "Optional `options` for `del` operation. Defaults to `{operationName: 'delete', apiVersion: state.configuration.apiVersion, responseType: 'json'}`", + "description": "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'}`", "type": { "type": "OptionalType", "expression": { diff --git a/lib/Adaptor.js b/lib/Adaptor.js index f4b5c54..3efd7cb 100644 --- a/lib/Adaptor.js +++ b/lib/Adaptor.js @@ -199,7 +199,7 @@ _axios.default.interceptors.response.use(function (response) { * @function * @param {string} resourceType - Type of resource to create. E.g. `trackedEntityInstances`, `programs`, `events`, ... * @param {Object} data - 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 (E.g. `filter`, `dimension` and other import parameters), request config (E.g. `auth`) and the DHIS2 apiVersion. + * @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 @@ -295,7 +295,7 @@ _axios.default.interceptors.response.use(function (response) { */ -function create(resourceType, data, options, callback) { +function create(resourceType, data, options = {}, callback = false) { return state => { console.log(`Preparing create operation...`); resourceType = (0, _languageCommon.expandReferences)(resourceType)(state); @@ -304,7 +304,7 @@ function create(resourceType, data, options, callback) { const { params, requestConfig - } = options || {}; + } = options; const { configuration } = state; @@ -329,7 +329,7 @@ function create(resourceType, data, options, callback) { * @param {string} resourceType - The type of resource to be updated. E.g. `dataElements`, `organisationUnits`, etc. * @param {string} path - The `id` or `path` to the `object` to be updated. E.g. `FTRrcoaog83` or `FTRrcoaog83/{collection-name}/{object-id}` * @param {Object} data - Data to update. It requires to send `all required fields` or the `full body`. If you want `partial updates`, use `patch` operation. - * @param {Object} [options] - Optional `options` to define URL parameters (E.g. `filter`, `dimension` and other import parameters), request config (E.g. `auth`) and the DHIS2 apiVersion. + * @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 @@ -456,7 +456,7 @@ function create(resourceType, data, options, callback) { */ -function update(resourceType, path, data, options, callback) { +function update(resourceType, path, data, options = {}, callback = false) { return state => { console.log(`Preparing update operation...`); resourceType = (0, _languageCommon.expandReferences)(resourceType)(state); @@ -466,13 +466,13 @@ function update(resourceType, path, data, options, callback) { const { params, requestConfig - } = options || {}; + } = options; const { configuration } = state; return (0, _Client.request)(configuration, { method: 'put', - url: `${(0, _Utils.generateUrl)(configuration, options, resourceType)}/${path}`, + url: (0, _Utils.generateUrl)(configuration, options, resourceType, path), params, data, ...requestConfig @@ -490,7 +490,7 @@ function update(resourceType, path, data, options, callback) { * @function * @param {string} resourceType - The type of resource to get(use its `plural` name). E.g. `dataElements`, `trackedEntityInstances`,`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 beyond filters, request configuration (e.g. `auth`) and DHIS2 api version to use. + * @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 @@ -510,16 +510,16 @@ function update(resourceType, path, data, options, callback) { */ -function get(resourceType, query, options, callback) { +function get(resourceType, query, options = {}, callback = false) { return state => { - console.log(`Preparing get operation...`); + console.log('Preparing get operation...'); resourceType = (0, _languageCommon.expandReferences)(resourceType)(state); query = (0, _languageCommon.expandReferences)(query)(state); options = (0, _languageCommon.expandReferences)(options)(state); const { params, requestConfig - } = options || {}; + } = options; const { configuration } = state; @@ -560,7 +560,7 @@ function get(resourceType, query, options, callback) { // * { ou: 'TSyzvBiovKh' } // * ); // */ -// export function upsert(resourceType, data, options, callback) { +// export function upsert(resourceType, data, options = {}, callback = false) { // return state => { // console.log(`Preparing upsert via 'get' then 'create' OR 'update'...`); // return get( @@ -652,8 +652,7 @@ function discover(httpMethod, endpoint) { * @param {string} resourceType - The type of resource to be updated. E.g. `dataElements`, `organisationUnits`, etc. * @param {string} path - The `id` or `path` to the `object` to be updated. E.g. `FTRrcoaog83` or `FTRrcoaog83/{collection-name}/{object-id}` * @param {Object} data - Data to update. Include only the fields you want to update. E.g. `{name: "New Name"}` - * @param {Object} [params] - Optional `update` parameters 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} - * @param {{apiVersion: number,operationName: string,responseType: string}} [options] - Optional options for update method. Defaults to `{operationName: 'patch', apiVersion: state.configuration.apiVersion, responseType: 'json'}` + * @param {Object} [options] - Optional configuration, including params for the update ({preheatCache: true, strategy: 'UPDATE', mergeMode: 'REPLACE'}). Defaults to `{operationName: 'patch', apiVersion: state.configuration.apiVersion, responseType: 'json'}` * @param {function} [callback] - Optional callback to handle the response * @returns {Operation} * @example a dataElement @@ -663,52 +662,30 @@ function discover(httpMethod, endpoint) { // I need to investigate on this. But I think DHIS 2 forces to send all properties back when we do an update. If that's confirmed then this may be needed. -function patch(resourceType, path, data, params, options, callback) { +function patch(resourceType, path, data, options = {}, callback = false) { return state => { - var _options$operationNam, _options, _options$responseType, _options2, _queryParams, _queryParams2, _options$apiVersion, _options3, _CONTENT_TYPES$respon; - + console.log('Preparing patch operation...'); resourceType = (0, _languageCommon.expandReferences)(resourceType)(state); path = (0, _languageCommon.expandReferences)(path)(state); - const body = (0, _languageCommon.expandReferences)(data)(state); - params = (0, _languageCommon.expandReferences)(params)(state); + data = (0, _languageCommon.expandReferences)(data)(state); options = (0, _languageCommon.expandReferences)(options)(state); - const operationName = (_options$operationNam = (_options = options) === null || _options === void 0 ? void 0 : _options.operationName) !== null && _options$operationNam !== void 0 ? _options$operationNam : 'patch'; const { - username, - password, - hostUrl - } = state.configuration; - const responseType = (_options$responseType = (_options2 = options) === null || _options2 === void 0 ? void 0 : _options2.responseType) !== null && _options$responseType !== void 0 ? _options$responseType : 'json'; - let queryParams = params; - const filters = (_queryParams = queryParams) === null || _queryParams === void 0 ? void 0 : _queryParams.filters; - (_queryParams2 = queryParams) === null || _queryParams2 === void 0 ? true : delete _queryParams2.filters; - queryParams = new URLSearchParams(queryParams); - filters === null || filters === void 0 ? void 0 : filters.map(f => queryParams.append('filter', f)); - const apiVersion = (_options$apiVersion = (_options3 = options) === null || _options3 === void 0 ? void 0 : _options3.apiVersion) !== null && _options$apiVersion !== void 0 ? _options$apiVersion : state.configuration.apiVersion; - const url = (0, _Utils.buildUrl)('/' + resourceType + '/' + path, hostUrl, apiVersion); - const headers = { - Accept: (_CONTENT_TYPES$respon = _Utils.CONTENT_TYPES[responseType]) !== null && _CONTENT_TYPES$respon !== void 0 ? _CONTENT_TYPES$respon : 'application/json' - }; - return _axios.default.request({ - method: 'PATCH', - url, - auth: { - username, - password - }, - params: queryParams, - data: body, - headers + params, + requestConfig + } = options; + const { + configuration + } = state; + return (0, _Client.request)(configuration, { + method: 'patch', + url: (0, _Utils.generateUrl)(configuration, options, resourceType, path), + params, + data, + ...requestConfig }).then(result => { - let resultObject = { - status: result.status, - statusText: result.statusText - }; + _Utils.Log.success(`Patched ${resourceType} at ${path}`); - _Utils.Log.success(`${operationName} succeeded. Updated ${resourceType}.\nSummary:\n${(0, _Utils.prettyJson)(resultObject)}`); - - if (callback) return callback((0, _languageCommon.composeNextState)(state, resultObject)); - return (0, _languageCommon.composeNextState)(state, resultObject); + return (0, _Utils.handleResponse)(result, state, callback); }); }; } @@ -719,8 +696,7 @@ function patch(resourceType, path, data, params, options, callback) { * @param {string} resourceType - The type of resource to be deleted. E.g. `trackedEntityInstances`, `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 {Object} [params] - Optional `update` parameters 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} - * @param {{apiVersion: number,operationName: string,resourceType: string}} [options] - Optional `options` for `del` operation. Defaults to `{operationName: 'delete', apiVersion: state.configuration.apiVersion, responseType: 'json'}` + * @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 @@ -729,30 +705,30 @@ function patch(resourceType, path, data, params, options, callback) { // TODO: @Elias, can this be implemented using the same pattern as update but without data? -function destroy(resourceType, path, data, params, options, callback) { +function destroy(resourceType, path, data, options = {}, callback = false) { return state => { - var _options$operationNam2, _options4, _options$responseType2, _options5, _queryParams3, _queryParams4, _options$apiVersion2, _options6, _CONTENT_TYPES$respon2; + var _options$operationNam, _options, _options$responseType, _options2, _queryParams, _queryParams2, _options$apiVersion, _options3, _CONTENT_TYPES$respon; resourceType = (0, _languageCommon.expandReferences)(resourceType)(state); path = (0, _languageCommon.expandReferences)(path)(state); const body = (0, _languageCommon.expandReferences)(data)(state); params = (0, _languageCommon.expandReferences)(params)(state); options = (0, _languageCommon.expandReferences)(options)(state); - const operationName = (_options$operationNam2 = (_options4 = options) === null || _options4 === void 0 ? void 0 : _options4.operationName) !== null && _options$operationNam2 !== void 0 ? _options$operationNam2 : 'delete'; + const operationName = (_options$operationNam = (_options = options) === null || _options === void 0 ? void 0 : _options.operationName) !== null && _options$operationNam !== void 0 ? _options$operationNam : 'delete'; const { username, password, hostUrl } = state.configuration; - const responseType = (_options$responseType2 = (_options5 = options) === null || _options5 === void 0 ? void 0 : _options5.responseType) !== null && _options$responseType2 !== void 0 ? _options$responseType2 : 'json'; + const responseType = (_options$responseType = (_options2 = options) === null || _options2 === void 0 ? void 0 : _options2.responseType) !== null && _options$responseType !== void 0 ? _options$responseType : 'json'; let queryParams = params; - const filters = (_queryParams3 = queryParams) === null || _queryParams3 === void 0 ? void 0 : _queryParams3.filters; - (_queryParams4 = queryParams) === null || _queryParams4 === void 0 ? true : delete _queryParams4.filters; + const filters = (_queryParams = queryParams) === null || _queryParams === void 0 ? void 0 : _queryParams.filters; + (_queryParams2 = queryParams) === null || _queryParams2 === void 0 ? true : delete _queryParams2.filters; queryParams = new URLSearchParams(queryParams); filters === null || filters === void 0 ? void 0 : filters.map(f => queryParams.append('filter', f)); - const apiVersion = (_options$apiVersion2 = (_options6 = options) === null || _options6 === void 0 ? void 0 : _options6.apiVersion) !== null && _options$apiVersion2 !== void 0 ? _options$apiVersion2 : state.configuration.apiVersion; + const apiVersion = (_options$apiVersion = (_options3 = options) === null || _options3 === void 0 ? void 0 : _options3.apiVersion) !== null && _options$apiVersion !== void 0 ? _options$apiVersion : state.configuration.apiVersion; const headers = { - Accept: (_CONTENT_TYPES$respon2 = _Utils.CONTENT_TYPES[responseType]) !== null && _CONTENT_TYPES$respon2 !== void 0 ? _CONTENT_TYPES$respon2 : 'application/json' + Accept: (_CONTENT_TYPES$respon = _Utils.CONTENT_TYPES[responseType]) !== null && _CONTENT_TYPES$respon !== void 0 ? _CONTENT_TYPES$respon : 'application/json' }; const url = (0, _Utils.buildUrl)('/' + resourceType + '/' + path, hostUrl, apiVersion); return _axios.default.request({ diff --git a/lib/Utils.js b/lib/Utils.js index c4a1e8c..fbb7aeb 100644 --- a/lib/Utils.js +++ b/lib/Utils.js @@ -64,7 +64,7 @@ function nestArray(data, key) { } : data; } -function generateUrl(configuration, options, resourceType) { +function generateUrl(configuration, options, resourceType, path = null) { let { hostUrl, apiVersion @@ -74,5 +74,7 @@ function generateUrl(configuration, options, resourceType) { if (options === null || options === void 0 ? void 0 : options.apiVersion) apiVersion = options.apiVersion; const apiMessage = apiVersion ? `Using DHIS2 api version ${apiVersion}` : 'Using latest available version of the DHIS2 api on this server.'; console.log(apiMessage); - return buildUrl(urlString, hostUrl, apiVersion); + const url = buildUrl(urlString, hostUrl, apiVersion); + if (path) return `${url}/${path}`; + return url; } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index c7f156c..06eb406 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1271,9 +1271,9 @@ } }, "@openfn/simple-ast": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@openfn/simple-ast/-/simple-ast-0.3.0.tgz", - "integrity": "sha512-NKRhH7aDS8gck3o1id+C+W70SdqQR9r04jkGhVgU5DZys1lgqkucubE8k9x7Uo0o+BwAsaT93TQIDK7PaSvk7A==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@openfn/simple-ast/-/simple-ast-0.4.0.tgz", + "integrity": "sha512-Jai/HVhyv5YkfKnJOTb490AbqfsNaRZrsPSy4RuFZsS/rLkQrFdOVe4syXyY26+Z2fUrj5lhxcIhuJ1AwT3VTQ==", "dev": true, "requires": { "@babel/core": "^7.13.10", @@ -2717,9 +2717,9 @@ "dev": true }, "is-core-module": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", - "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", + "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", "dev": true, "requires": { "has": "^1.0.3" @@ -4126,9 +4126,9 @@ "dev": true }, "signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz", + "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==", "dev": true }, "sinon": { @@ -4320,9 +4320,9 @@ } }, "spdx-license-ids": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.9.tgz", - "integrity": "sha512-Ki212dKK4ogX+xDo4CtOZBVIwhsKBEfsEEcwmJfLQzirgc2jIWdzg40Unxz/HzEUqM1WFzVlQSMF9kZZ2HboLQ==", + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz", + "integrity": "sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g==", "dev": true }, "split-string": { diff --git a/package.json b/package.json index 1c1311c..713be02 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "@babel/preset-env": "^7.9.5", "@babel/preset-stage-0": "^7.8.3", "@babel/register": "^7.9.0", - "@openfn/simple-ast": "^0.3.0", + "@openfn/simple-ast": "^0.4.0", "assertion-error": "^1.0.1", "chai": "^3.4.0", "chai-http": "^4.3.0", diff --git a/src/Adaptor.js b/src/Adaptor.js index 646da6f..01bc698 100644 --- a/src/Adaptor.js +++ b/src/Adaptor.js @@ -1,13 +1,11 @@ /** @module Adaptor */ import axios from 'axios'; +import { indexOf } from 'lodash'; import { execute as commonExecute, - composeNextState, expandReferences, } from '@openfn/language-common'; -import { indexOf } from 'lodash'; import { - buildUrl, CONTENT_TYPES, generateUrl, handleResponse, @@ -124,7 +122,7 @@ axios.interceptors.response.use( * @function * @param {string} resourceType - Type of resource to create. E.g. `trackedEntityInstances`, `programs`, `events`, ... * @param {Object} data - 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 (E.g. `filter`, `dimension` and other import parameters), request config (E.g. `auth`) and the DHIS2 apiVersion. + * @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 @@ -218,7 +216,7 @@ axios.interceptors.response.use( * incidentDate: '2013-09-17', * }); */ -export function create(resourceType, data, options, callback) { +export function create(resourceType, data, options = {}, callback = false) { return state => { console.log(`Preparing create operation...`); @@ -226,7 +224,7 @@ export function create(resourceType, data, options, callback) { data = expandReferences(data)(state); options = expandReferences(options)(state); - const { params, requestConfig } = options || {}; + const { params, requestConfig } = options; const { configuration } = state; return request(configuration, { @@ -250,7 +248,7 @@ export function create(resourceType, data, options, callback) { * @param {string} resourceType - The type of resource to be updated. E.g. `dataElements`, `organisationUnits`, etc. * @param {string} path - The `id` or `path` to the `object` to be updated. E.g. `FTRrcoaog83` or `FTRrcoaog83/{collection-name}/{object-id}` * @param {Object} data - Data to update. It requires to send `all required fields` or the `full body`. If you want `partial updates`, use `patch` operation. - * @param {Object} [options] - Optional `options` to define URL parameters (E.g. `filter`, `dimension` and other import parameters), request config (E.g. `auth`) and the DHIS2 apiVersion. + * @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 @@ -375,7 +373,13 @@ export function create(resourceType, data, options, callback) { * incidentDate: '2013-10-17', * }); */ -export function update(resourceType, path, data, options, callback) { +export function update( + resourceType, + path, + data, + options = {}, + callback = false +) { return state => { console.log(`Preparing update operation...`); @@ -384,12 +388,12 @@ export function update(resourceType, path, data, options, callback) { data = expandReferences(data)(state); options = expandReferences(options)(state); - const { params, requestConfig } = options || {}; + const { params, requestConfig } = options; const { configuration } = state; return request(configuration, { method: 'put', - url: `${generateUrl(configuration, options, resourceType)}/${path}`, + url: generateUrl(configuration, options, resourceType, path), params, data, ...requestConfig, @@ -407,7 +411,7 @@ export function update(resourceType, path, data, options, callback) { * @function * @param {string} resourceType - The type of resource to get(use its `plural` name). E.g. `dataElements`, `trackedEntityInstances`,`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 beyond filters, request configuration (e.g. `auth`) and DHIS2 api version to use. + * @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 @@ -425,15 +429,15 @@ export function update(resourceType, path, data, options, callback) { * filter: ['flGbXLXCrEo:Eq:124', 'w75KJ2mc4zz:Eq:John'], * }); */ -export function get(resourceType, query, options, callback) { +export function get(resourceType, query, options = {}, callback = false) { return state => { - console.log(`Preparing get operation...`); + console.log('Preparing get operation...'); resourceType = expandReferences(resourceType)(state); query = expandReferences(query)(state); options = expandReferences(options)(state); - const { params, requestConfig } = options || {}; + const { params, requestConfig } = options; const { configuration } = state; return request(configuration, { @@ -472,7 +476,7 @@ export function get(resourceType, query, options, callback) { // * { ou: 'TSyzvBiovKh' } // * ); // */ -// export function upsert(resourceType, data, options, callback) { +// export function upsert(resourceType, data, options = {}, callback = false) { // return state => { // console.log(`Preparing upsert via 'get' then 'create' OR 'update'...`); @@ -603,8 +607,7 @@ export function discover(httpMethod, endpoint) { * @param {string} resourceType - The type of resource to be updated. E.g. `dataElements`, `organisationUnits`, etc. * @param {string} path - The `id` or `path` to the `object` to be updated. E.g. `FTRrcoaog83` or `FTRrcoaog83/{collection-name}/{object-id}` * @param {Object} data - Data to update. Include only the fields you want to update. E.g. `{name: "New Name"}` - * @param {Object} [params] - Optional `update` parameters 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} - * @param {{apiVersion: number,operationName: string,responseType: string}} [options] - Optional options for update method. Defaults to `{operationName: 'patch', apiVersion: state.configuration.apiVersion, responseType: 'json'}` + * @param {Object} [options] - Optional configuration, including params for the update ({preheatCache: true, strategy: 'UPDATE', mergeMode: 'REPLACE'}). Defaults to `{operationName: 'patch', apiVersion: state.configuration.apiVersion, responseType: 'json'}` * @param {function} [callback] - Optional callback to handle the response * @returns {Operation} * @example a dataElement @@ -612,67 +615,34 @@ export function discover(httpMethod, endpoint) { */ // TODO: @Elias, can this be deleted in favor of update? How does DHIS2 handle PATCH vs PUT? // I need to investigate on this. But I think DHIS 2 forces to send all properties back when we do an update. If that's confirmed then this may be needed. -export function patch(resourceType, path, data, params, options, callback) { +export function patch( + resourceType, + path, + data, + options = {}, + callback = false +) { return state => { - resourceType = expandReferences(resourceType)(state); + console.log('Preparing patch operation...'); + resourceType = expandReferences(resourceType)(state); path = expandReferences(path)(state); - - const body = expandReferences(data)(state); - - params = expandReferences(params)(state); - + data = expandReferences(data)(state); options = expandReferences(options)(state); - const operationName = options?.operationName ?? 'patch'; - - const { username, password, hostUrl } = state.configuration; - - const responseType = options?.responseType ?? 'json'; - - let queryParams = params; - - const filters = queryParams?.filters; - - delete queryParams?.filters; - - queryParams = new URLSearchParams(queryParams); - - filters?.map(f => queryParams.append('filter', f)); - - const apiVersion = options?.apiVersion ?? state.configuration.apiVersion; - - const url = buildUrl('/' + resourceType + '/' + path, hostUrl, apiVersion); - - const headers = { - Accept: CONTENT_TYPES[responseType] ?? 'application/json', - }; + const { params, requestConfig } = options; + const { configuration } = state; - return axios - .request({ - method: 'PATCH', - url, - auth: { - username, - password, - }, - params: queryParams, - data: body, - headers, - }) - .then(result => { - let resultObject = { - status: result.status, - statusText: result.statusText, - }; - Log.success( - `${operationName} succeeded. Updated ${resourceType}.\nSummary:\n${prettyJson( - resultObject - )}` - ); - if (callback) return callback(composeNextState(state, resultObject)); - return composeNextState(state, resultObject); - }); + return request(configuration, { + method: 'patch', + url: generateUrl(configuration, options, resourceType, path), + params, + data, + ...requestConfig, + }).then(result => { + Log.success(`Patched ${resourceType} at ${path}`); + return handleResponse(result, state, callback); + }); }; } @@ -683,71 +653,40 @@ export function patch(resourceType, path, data, params, options, callback) { * @param {string} resourceType - The type of resource to be deleted. E.g. `trackedEntityInstances`, `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 {Object} [params] - Optional `update` parameters 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} - * @param {{apiVersion: number,operationName: string,resourceType: string}} [options] - Optional `options` for `del` operation. Defaults to `{operationName: 'delete', apiVersion: state.configuration.apiVersion, responseType: 'json'}` + * @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'); */ -// TODO: @Elias, can this be implemented using the same pattern as update but without data? -export function destroy(resourceType, path, data, params, options, callback) { +export function destroy( + resourceType, + path, + data = null, + options = {}, + callback = false +) { return state => { - resourceType = expandReferences(resourceType)(state); + console.log('Preparing destroy operation...'); + resourceType = expandReferences(resourceType)(state); path = expandReferences(path)(state); - - const body = expandReferences(data)(state); - - params = expandReferences(params)(state); - + data = expandReferences(data)(state); options = expandReferences(options)(state); - const operationName = options?.operationName ?? 'delete'; - - const { username, password, hostUrl } = state.configuration; - - const responseType = options?.responseType ?? 'json'; - - let queryParams = params; - - const filters = queryParams?.filters; - - delete queryParams?.filters; - - queryParams = new URLSearchParams(queryParams); - - filters?.map(f => queryParams.append('filter', f)); - - const apiVersion = options?.apiVersion ?? state.configuration.apiVersion; - - const headers = { - Accept: CONTENT_TYPES[responseType] ?? 'application/json', - }; - - const url = buildUrl('/' + resourceType + '/' + path, hostUrl, apiVersion); + const { params, requestConfig } = options; + const { configuration } = state; - return axios - .request({ - method: 'DELETE', - url, - auth: { - username, - password, - }, - params: queryParams, - data: body, - headers, - }) - .then(result => { - Log.success( - `${operationName} succeeded. DELETED ${resourceType}.\nSummary:\n${prettyJson( - result.data - )}` - ); - if (callback) return callback(composeNextState(state, result.data)); - return composeNextState(state, result.data); - }); + return request({ + method: 'delete', + url: generateUrl(configuration, options, resourceType, path), + params, + data, + ...requestConfig, + }).then(result => { + Log.success(`Deleted ${resourceType} at ${path}`); + return handleResponse(result, state, callback); + }); }; } diff --git a/src/Utils.js b/src/Utils.js index 48536ff..1a0cd94 100644 --- a/src/Utils.js +++ b/src/Utils.js @@ -44,7 +44,7 @@ export function nestArray(data, key) { return isArray(data) ? { [key]: data } : data; } -export function generateUrl(configuration, options, resourceType) { +export function generateUrl(configuration, options, resourceType, path = null) { let { hostUrl, apiVersion } = configuration; const urlString = '/' + resourceType; @@ -57,5 +57,8 @@ export function generateUrl(configuration, options, resourceType) { console.log(apiMessage); - return buildUrl(urlString, hostUrl, apiVersion); + const url = buildUrl(urlString, hostUrl, apiVersion); + + if (path) return `${url}/${path}`; + return url; } From 0a2f26fa637973ac383480dc951fbe5c5965fcdd Mon Sep 17 00:00:00 2001 From: Taylor Downs Date: Wed, 22 Dec 2021 14:50:54 +0000 Subject: [PATCH 26/26] run build --- lib/Adaptor.js | 56 +++++++++++++++++--------------------------------- 1 file changed, 19 insertions(+), 37 deletions(-) diff --git a/lib/Adaptor.js b/lib/Adaptor.js index 3efd7cb..f13f82d 100644 --- a/lib/Adaptor.js +++ b/lib/Adaptor.js @@ -82,10 +82,10 @@ Object.defineProperty(exports, "http", { var _axios = _interopRequireDefault(require("axios")); -var _languageCommon = require("@openfn/language-common"); - var _lodash = require("lodash"); +var _languageCommon = require("@openfn/language-common"); + var _Utils = require("./Utils"); var _Client = require("./Client"); @@ -702,50 +702,32 @@ function patch(resourceType, path, data, options = {}, callback = false) { * @example a tracked entity instance * destroy('trackedEntityInstances', 'LcRd6Nyaq7T'); */ -// TODO: @Elias, can this be implemented using the same pattern as update but without data? -function destroy(resourceType, path, data, options = {}, callback = false) { +function destroy(resourceType, path, data = null, options = {}, callback = false) { return state => { - var _options$operationNam, _options, _options$responseType, _options2, _queryParams, _queryParams2, _options$apiVersion, _options3, _CONTENT_TYPES$respon; - + console.log('Preparing destroy operation...'); resourceType = (0, _languageCommon.expandReferences)(resourceType)(state); path = (0, _languageCommon.expandReferences)(path)(state); - const body = (0, _languageCommon.expandReferences)(data)(state); - params = (0, _languageCommon.expandReferences)(params)(state); + data = (0, _languageCommon.expandReferences)(data)(state); options = (0, _languageCommon.expandReferences)(options)(state); - const operationName = (_options$operationNam = (_options = options) === null || _options === void 0 ? void 0 : _options.operationName) !== null && _options$operationNam !== void 0 ? _options$operationNam : 'delete'; const { - username, - password, - hostUrl - } = state.configuration; - const responseType = (_options$responseType = (_options2 = options) === null || _options2 === void 0 ? void 0 : _options2.responseType) !== null && _options$responseType !== void 0 ? _options$responseType : 'json'; - let queryParams = params; - const filters = (_queryParams = queryParams) === null || _queryParams === void 0 ? void 0 : _queryParams.filters; - (_queryParams2 = queryParams) === null || _queryParams2 === void 0 ? true : delete _queryParams2.filters; - queryParams = new URLSearchParams(queryParams); - filters === null || filters === void 0 ? void 0 : filters.map(f => queryParams.append('filter', f)); - const apiVersion = (_options$apiVersion = (_options3 = options) === null || _options3 === void 0 ? void 0 : _options3.apiVersion) !== null && _options$apiVersion !== void 0 ? _options$apiVersion : state.configuration.apiVersion; - const headers = { - Accept: (_CONTENT_TYPES$respon = _Utils.CONTENT_TYPES[responseType]) !== null && _CONTENT_TYPES$respon !== void 0 ? _CONTENT_TYPES$respon : 'application/json' - }; - const url = (0, _Utils.buildUrl)('/' + resourceType + '/' + path, hostUrl, apiVersion); - return _axios.default.request({ - method: 'DELETE', - url, - auth: { - username, - password - }, - params: queryParams, - data: body, - headers + params, + requestConfig + } = options; + const { + configuration + } = state; + return (0, _Client.request)({ + method: 'delete', + url: (0, _Utils.generateUrl)(configuration, options, resourceType, path), + params, + data, + ...requestConfig }).then(result => { - _Utils.Log.success(`${operationName} succeeded. DELETED ${resourceType}.\nSummary:\n${(0, _Utils.prettyJson)(result.data)}`); + _Utils.Log.success(`Deleted ${resourceType} at ${path}`); - if (callback) return callback((0, _languageCommon.composeNextState)(state, result.data)); - return (0, _languageCommon.composeNextState)(state, result.data); + return (0, _Utils.handleResponse)(result, state, callback); }); }; }