diff --git a/packages/openmrs/CHANGELOG.md b/packages/openmrs/CHANGELOG.md index 6b979efcf..23baabe08 100644 --- a/packages/openmrs/CHANGELOG.md +++ b/packages/openmrs/CHANGELOG.md @@ -1,5 +1,13 @@ # @openfn/language-openmrs +## 4.2.0 + +### Minor Changes + +- 5d6839e: Implement namespaced http.request() function. The function makes a + call against the `instanceUrl` and the path provided, while allowing + manipulation to the API call as needed. + ## 4.1.6 ### Patch Changes diff --git a/packages/openmrs/package.json b/packages/openmrs/package.json index b38d7bbfc..6802c8125 100644 --- a/packages/openmrs/package.json +++ b/packages/openmrs/package.json @@ -1,6 +1,6 @@ { "name": "@openfn/language-openmrs", - "version": "4.1.6", + "version": "4.2.0", "description": "OpenMRS Language Pack for OpenFn", "homepage": "https://docs.openfn.org", "repository": { diff --git a/packages/openmrs/src/Adaptor.js b/packages/openmrs/src/Adaptor.js index e08867636..f78e80e76 100644 --- a/packages/openmrs/src/Adaptor.js +++ b/packages/openmrs/src/Adaptor.js @@ -39,11 +39,15 @@ export function getPatient(uuid, callback = s => s) { return async state => { const [resolvedUuid] = expandReferences(state, uuid); console.log(`Fetching patient by uuid: ${resolvedUuid}`); + const { instanceUrl: baseUrl } = state.configuration; const response = await request( state, 'GET', - `/ws/rest/v1/patient/${resolvedUuid}` + `/ws/rest/v1/patient/${resolvedUuid}`, + { + baseUrl, + } ); console.log(`Retrieved patient with uuid: ${resolvedUuid}...`); @@ -73,13 +77,15 @@ export function get(path, query, callback = s => s) { path, query ); - + const { instanceUrl: baseUrl } = state.configuration; const response = await request( state, 'GET', `/ws/rest/v1/${resolvedPath}`, - {}, - resolvedQuery + { + baseUrl, + query: resolvedQuery, + } ); // TODO: later decide if we want to throw for no-results. @@ -109,12 +115,15 @@ export function get(path, query, callback = s => s) { export function post(path, data, callback = s => s) { return async state => { const [resolvedPath, resolvedData] = expandReferences(state, path, data); - + const { instanceUrl: baseUrl } = state.configuration; const response = await request( state, 'POST', `/ws/rest/v1/${resolvedPath}`, - resolvedData + { + baseUrl, + data: resolvedData, + } ); return prepareNextState(state, response, callback); @@ -134,16 +143,14 @@ export function post(path, data, callback = s => s) { export function searchPatient(query, callback = s => s) { return async state => { const [resolvedQuery] = expandReferences(state, query); + const { instanceUrl: baseUrl } = state.configuration; console.log('Searching for patient with query:', resolvedQuery); - const response = await request( - state, - 'GET', - '/ws/rest/v1/patient', - {}, - resolvedQuery - ); + const response = await request(state, 'GET', '/ws/rest/v1/patient', { + baseUrl, + query: resolvedQuery, + }); return prepareNextState(state, response, callback); }; @@ -162,16 +169,14 @@ export function searchPatient(query, callback = s => s) { export function searchPerson(query, callback = s => s) { return async state => { const [resolvedQuery = {}] = expandReferences(state, query); + const { instanceUrl: baseUrl } = state.configuration; console.log(`Searching for person with query:`, resolvedQuery); - const response = await request( - state, - 'GET', - '/ws/rest/v1/person', - {}, - resolvedQuery - ); + const response = await request(state, 'GET', '/ws/rest/v1/person', { + baseUrl, + query: resolvedQuery, + }); console.log(`Found ${response.body.results.length} people`); @@ -193,11 +198,15 @@ export function getEncounter(uuid, callback = s => s) { return async state => { const [resolvedUuid] = expandReferences(state, uuid); console.log(`Fetching encounter with UUID: ${resolvedUuid}`); + const { instanceUrl: baseUrl } = state.configuration; const response = await request( state, 'GET', - `/ws/rest/v1/encounter/${resolvedUuid}` + `/ws/rest/v1/encounter/${resolvedUuid}`, + { + baseUrl, + } ); console.log( @@ -222,14 +231,12 @@ export function getEncounters(query, callback = s => s) { return async state => { const [resolvedQuery] = expandReferences(state, query); console.log('Fetching encounters by query', resolvedQuery); + const { instanceUrl: baseUrl } = state.configuration; - const response = await request( - state, - 'GET', - `/ws/rest/v1/encounter/`, - {}, - resolvedQuery - ); + const response = await request(state, 'GET', `/ws/rest/v1/encounter/`, { + baseUrl, + query: resolvedQuery, + }); console.log(`Found ${response.body.results.length} results`); return prepareNextState(state, response, callback); @@ -308,12 +315,16 @@ export function create(resourceType, data, callback = s => s) { data ); console.log('Preparing to create', resolvedResource); + const { instanceUrl: baseUrl } = state.configuration; const response = await request( state, 'POST', `/ws/rest/v1/${resolvedResource}`, - resolvedData + { + baseUrl, + data: resolvedData, + } ); console.log('Successfully created', resolvedResource); @@ -344,12 +355,16 @@ export function update(resourceType, path, data, callback = s => s) { data ); console.log('Preparing to update', resolvedResource); + const { instanceUrl: baseUrl } = state.configuration; const response = await request( state, 'POST', `/ws/rest/v1/${resolvedResource}/${resolvedPath}`, - resolvedData + { + baseUrl, + data: resolvedData, + } ); console.log('Successfully updated', resolvedResource); @@ -404,14 +419,11 @@ export function upsert( "Preparing composed upsert (via 'get' then 'create' OR 'update') on", resolvedResource ); - - return await request( - state, - 'GET', - `/ws/rest/v1/${resolvedResource}`, - {}, - resolvedQuery - ).then(resp => { + const { instanceUrl: baseUrl } = state.configuration; + return await request(state, 'GET', `/ws/rest/v1/${resolvedResource}`, { + baseUrl, + query: resolvedQuery, + }).then(resp => { const resource = resp.body.results; if (resource.length > 1) { throw new RangeError( diff --git a/packages/openmrs/src/Utils.js b/packages/openmrs/src/Utils.js index 306fb6c57..a1b9fb8f4 100644 --- a/packages/openmrs/src/Utils.js +++ b/packages/openmrs/src/Utils.js @@ -15,28 +15,50 @@ export const prepareNextState = (state, response, callback) => { return callback(nextState); }; -export async function request(state, method, path, data, params) { - const { instanceUrl, username, password } = state.configuration; - const headers = makeBasicAuthHeader(username, password); +export async function request(state, method, path, options = {}) { + const { + baseUrl = state.configuration.instanceUrl, + query = {}, + data = {}, + headers = { 'content-type': 'application/json' }, + parseAs = 'json', + } = options; - const options = { + if (baseUrl.length <= 0) { + throw new Error( + 'Invalid instanceUrl. Include instanceUrl in state.configuration' + ); + } + + const isAbsoluteUrl = /^(https?:|\/\/)/i.test(path); + if (isAbsoluteUrl) { + throw new Error( + `Invalid path argument: "${path}" appears to be an absolute URL. Please provide a relative path.` + ); + } + + const { username, password } = state.configuration; + const authHeaders = makeBasicAuthHeader(username, password); + + const opts = { body: data, headers: { + ...authHeaders, ...headers, - 'content-type': 'application/json', }, - query: params, - parseAs: 'json', + query, + parseAs, }; - const url = `${instanceUrl}${path}`; + const url = `${baseUrl}${path}`; let allResponses; - let query = options?.query; - let allowPagination = isNaN(query?.startIndex); + let queryParams = opts?.query; + let allowPagination = isNaN(queryParams?.startIndex); do { - const requestOptions = query ? { ...options, query } : options; + const requestOptions = queryParams ? { ...opts, query: queryParams } : opts; + const response = await commonRequest(method, url, requestOptions); logResponse(response); @@ -56,7 +78,7 @@ export async function request(state, method, path, data, params) { const params = new URLSearchParams(urlObj.search); const startIndex = params.get('startIndex'); - query = { ...query, startIndex }; + queryParams = { ...queryParams, startIndex }; } else { delete allResponses.body.links; break; @@ -64,4 +86,4 @@ export async function request(state, method, path, data, params) { } while (allowPagination); return allResponses; -} \ No newline at end of file +} diff --git a/packages/openmrs/src/http.js b/packages/openmrs/src/http.js new file mode 100644 index 000000000..c5420cf94 --- /dev/null +++ b/packages/openmrs/src/http.js @@ -0,0 +1,44 @@ +import { expandReferences } from '@openfn/language-common/util'; +import * as util from './Utils'; + +/** + * Options object + * @typedef {Object} OpenMRSOptions + * @property {object} query - An object of query parameters to be encoded into the URL + * @property {object} headers - An object of all request headers + * @property {object} body - The request body (as JSON) + * @property {string} [parseAs='json'] - The response format to parse (e.g., 'json', 'text', or 'stream') + */ + +/** + * Make a HTTP request to any OpenMRS endpoint + * @example + * request("GET", + * "/ws/rest/v1/patient/d3f7e1a8-0114-4de6-914b-41a11fc8a1a8", { + * query:{ + * limit: 1, + * offset: 20 + * }, + * }); + * @function + * @public + * @param {string} method - HTTP method to use + * @param {string} path - Path to resource + * @param {OpenMRSOptions} [options={}] - An object containing query, headers, and body for the request + * @returns {Operation} + */ +export function request(method, path, options = {}, callback = s => s) { + return async state => { + const [resolvedMethod, resolvedPath, resolvedOptions = {}] = + expandReferences(state, method, path, options); + + const response = await util.request( + state, + resolvedMethod, + resolvedPath, + resolvedOptions + ); + + return util.prepareNextState(state, response, callback); + }; +} diff --git a/packages/openmrs/src/index.js b/packages/openmrs/src/index.js index 9979db36d..406f9cca8 100644 --- a/packages/openmrs/src/index.js +++ b/packages/openmrs/src/index.js @@ -3,3 +3,4 @@ export default Adaptor; export * from './Adaptor'; export * as fhir from './fhir'; +export * as http from './http' diff --git a/packages/openmrs/test/index.test.js b/packages/openmrs/test/index.test.js index eb666f2a6..fb329b9b5 100644 --- a/packages/openmrs/test/index.test.js +++ b/packages/openmrs/test/index.test.js @@ -13,6 +13,7 @@ import { searchPerson, searchPatient, fhir, + http, } from '../src'; const testServer = enableMockClient('https://fn.openmrs.org'); @@ -63,6 +64,69 @@ describe('execute', () => { }); }); }); + +describe('http', () => { + it('should GET with a query', async () => { + testServer + .intercept({ + path: '/ws/rest/v1/patient', + query: { q: 'Sarah 1' }, + method: 'GET', + }) + .reply(200, { results: [{ display: 'Sarah 1' }] }, { ...jsonHeaders }); + const state = { configuration }; + + const { data } = await http.request('GET', '/ws/rest/v1/patient', { + query: { q: 'Sarah 1' }, + })(state); + + expect(data.results[0].display).to.eql('Sarah 1'); + }); + + it('should auto-fetch patients with a limit', async () => { + testServer + .intercept({ + path: '/ws/rest/v1/patient', + query: { q: 'Sarah', limit: 1 }, + method: 'GET', + }) + .reply( + 200, + { + results: [{ display: 'Sarah 1' }], + links: [ + { + rel: 'next', + uri: 'https://fn.openmrs.org/ws/rest/v1/patient?q=Sarah&limit=1&startIndex=1', + resourceAlias: null, + }, + ], + }, + { ...jsonHeaders } + ); + testServer + .intercept({ + path: '/ws/rest/v1/patient', + query: { q: 'Sarah', limit: 1, startIndex: 1 }, + method: 'GET', + }) + .reply( + 200, + { + results: [{ display: 'Sarah 2' }], + }, + { ...jsonHeaders } + ); + + const state = { configuration }; + const { data } = await http.request('GET', '/ws/rest/v1/patient', { + query: { q: 'Sarah', limit: 1 }, + })(state); + + expect(data.results[0].display).to.eql('Sarah 1'); + expect(data.results[1].display).to.eql('Sarah 2'); + }); +}); describe('fhir', () => { it('should GET with a query', async () => { testServer @@ -130,13 +194,10 @@ describe('request', () => { .reply(200, { results: [{ display: 'Sarah 1' }] }, { ...jsonHeaders }); const state = { configuration }; - const { body } = await request( - state, - 'GET', - '/ws/rest/v1/patient', - {}, - { q: 'Sarah 1' } - ); + const { body } = await request(state, 'GET', '/ws/rest/v1/patient', { + query: { q: 'Sarah 1' }, + baseUrl: state.configuration.instanceUrl, + }); expect(body.results[0].display).to.eql('Sarah 1'); }); @@ -176,13 +237,11 @@ describe('request', () => { ); const state = { configuration }; - const { body } = await request( - state, - 'GET', - '/ws/rest/v1/patient', - {}, - { q: 'Sarah', limit: 1 } - ); + const { body } = await request(state, 'GET', '/ws/rest/v1/patient', { + query: { q: 'Sarah', limit: 1 }, + baseUrl: state.configuration.instanceUrl, + }); + expect(body.results[0].display).to.eql('Sarah 1'); expect(body.results[1].display).to.eql('Sarah 2'); }); @@ -223,13 +282,10 @@ describe('request', () => { ); const state = { configuration }; - const { body } = await request( - state, - 'GET', - '/ws/rest/v1/patient', - {}, - { q: 'Sarah', limit: 1, startIndex: 1 } - ); + const { body } = await request(state, 'GET', '/ws/rest/v1/patient', { + query: { q: 'Sarah', limit: 1, startIndex: 1 }, + baseUrl: state.configuration.instanceUrl, + }); expect(body.results[0].display).to.eql('Sarah 2'); expect(body.results.length).to.eql(1);