UNPKG

@openmrs/esm-api

Version:

The javascript module for interacting with the OpenMRS API

275 lines (274 loc) • 12.2 kB
function _define_property(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 @category API */ import { Observable } from "rxjs"; import { isPlainObject } from "lodash-es"; import { getConfig } from "@openmrs/esm-config"; import { clearHistory, navigate } from "@openmrs/esm-navigation"; import { defaultRedirectAuthFailureUrl } from "./config-schema.js"; /** The base URL for the OpenMRS REST API (e.g., '/ws/rest/v1'). */ export const restBaseUrl = '/ws/rest/v1'; /** The base URL for the OpenMRS FHIR R4 API (e.g., '/ws/fhir2/R4'). */ export const fhirBaseUrl = '/ws/fhir2/R4'; /** The endpoint for session management operations. */ export const sessionEndpoint = `${restBaseUrl}/session`; /** * Append `path` to the OpenMRS SPA base. * * @param path The path to append to the OpenMRS base URL. * @returns The full URL with the OpenMRS base prepended. If the path is already * an absolute URL (starting with 'http'), it is returned unchanged. * * @example * * ```ts * makeUrl('/foo/bar'); * // => '/openmrs/foo/bar' * ``` */ export function makeUrl(path) { if (path && path.startsWith('http')) { return path; } else if (path[0] !== '/') { // ensure path starts with / path = '/' + path; } return window.openmrsBase + path; } /** * The openmrsFetch function is a wrapper around the * [browser's built-in fetch function](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch), * with extra handling for OpenMRS-specific API behaviors, such as * request headers, authentication, authorization, and the API urls. * * @param path A string url to make the request to. Note that the * openmrs base url (by default `/openmrs`) will be automatically * prepended to the URL, so there is no need to include it. * @param fetchInit A [fetch init object](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Syntax). * Note that the `body` property does not need to be `JSON.stringify()`ed * because openmrsFetch will do that for you. * @returns A [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) * that resolves with a [Response object](https://developer.mozilla.org/en-US/docs/Web/API/Response). * Note that the openmrs version of the Response object has already * downloaded the HTTP response body as json, and has an additional * `data` property with the HTTP response json as a javascript object. * * @example * ```js * import { openmrsFetch } from '@openmrs/esm-api' * const abortController = new AbortController(); * openmrsFetch(`${restBaseUrl}/session', {signal: abortController.signal}) * .then(response => { * console.log(response.data.authenticated) * }) * .catch(err => { * console.error(err.status); * }) * abortController.abort(); * openmrsFetch(`${restBaseUrl}/session', { * method: 'POST', * body: { * username: 'hi', * password: 'there', * } * }) * ``` * * #### Cancellation * * To cancel a network request, use an * [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort). * It is best practice to cancel your network requests when the user * navigates away from a page while the request is pending request, to * free up memory and network resources and to prevent race conditions. * * @category API */ export function openmrsFetch(path, fetchInit = {}) { if (typeof path !== 'string') { throw Error("The first argument to @openmrs/api's openmrsFetch function must be a url string"); } if (typeof fetchInit !== 'object') { throw Error("The second argument to @openmrs/api's openmrsFetch function must be a plain object."); } if (!window.openmrsBase) { throw Error("@openmrs/api is running in a browser that doesn't have window.openmrsBase, which is provided by openmrs-module-spa's HTML file."); } // Prefix the url with the openmrs spa base let url = makeUrl(path); // We're going to need some headers if (!fetchInit.headers) { fetchInit.headers = {}; } /* Automatically stringify javascript objects being sent in the * request body. */ if (isPlainObject(fetchInit.body)) { fetchInit.body = JSON.stringify(fetchInit.body); } /* Add a request header to tell the server to respond with json, * since frontend code almost always wants json and the OpenMRS * server won't give you json unless you explicitly ask for it. * If a different Accept header is preferred, pass it into the fetchInit. * If no Accept header is desired, pass it in explicitly as null. */ if (typeof fetchInit.headers.Accept === 'undefined') { fetchInit.headers.Accept = 'application/json'; } if (fetchInit.headers.Accept === null) { delete fetchInit.headers.Accept; } /* This tells the OpenMRS REST API not to return a WWW-Authenticate * header. Returning that header is useful when using the API, but * not from a UI. */ if (path.startsWith(restBaseUrl) && typeof fetchInit.headers['Disable-WWW-Authenticate'] === 'undefined') { fetchInit.headers['Disable-WWW-Authenticate'] = 'true'; } if (path.startsWith(fhirBaseUrl)) { const urlUrl = new URL(url, window.location.toString()); if (!urlUrl.searchParams.has('_summary')) { urlUrl.searchParams.set('_summary', 'data'); url = urlUrl.toString(); } } /* We capture the stacktrace before making the request, so that if an error occurs we can * log a full stacktrace that includes the code that made the request and handled the response * Otherwise, we could run into situations where the stacktrace doesn't even show which code * called @openmrs/api. */ const requestStacktrace = Error(); return window.fetch(url, fetchInit).then(async (r)=>{ const response = r; const { redirectAuthFailure, followRedirects } = await getConfig('@openmrs/esm-api'); if (response.ok) { if (response.status === 204) { if (followRedirects && response.headers.has('location')) { const location = response.headers.get('location'); if (location) { navigate({ to: location }); } } /* HTTP 204 - No Content * We should not try to download the empty response as json. Instead, * we return null since there is no response body. */ response.data = null; return response; } else { // HTTP 200s - The request succeeded return response.clone().text().then((responseText)=>{ try { if (responseText) { response.data = JSON.parse(responseText); } } catch (err) { // Server didn't respond with json } return response; }); } } else { /* HTTP response status is not in 200s. Usually this will mean * either HTTP 400s (bad request from browser) or HTTP 500s (server error) * Our goal is to come up with best possible stacktrace and error message * to help developers understand the problem and debug */ /* * Redirect to given url when redirect on auth failure is enabled */ if (url === makeUrl(sessionEndpoint) && response.status === 403 || redirectAuthFailure.enabled && redirectAuthFailure.errors.includes(response.status)) { clearHistory(); // by default, redirect to the url specified in the config. // If blank, use the location header from the response. // If that is also blank, use the default redirect url. const location = redirectAuthFailure.url || response.headers.get('location') || defaultRedirectAuthFailureUrl; navigate({ to: location }); /* We sometimes don't really want this promise to resolve since there's no response data, * nor do we want it to reject because that would trigger error handling. We instead * want it to remain in pending status while the navigation occurs. */ return redirectAuthFailure.resolvePromise ? Promise.resolve() : new Promise(()=>{}); } else { // Attempt to download a response body, if it has one return response.clone().text().then((responseText)=>{ let responseBody = responseText; try { responseBody = JSON.parse(responseText); } catch (err) { // Server didn't respond with json, so just go with the response text string } /* Make the fetch promise go into "rejected" status, with the best * possible stacktrace and error message. */ throw new OpenmrsFetchError(url, response, responseBody, requestStacktrace); }, (err)=>{ /* We weren't able to download a response body for this error. * Time to just give the best possible stacktrace and error message. */ throw new OpenmrsFetchError(url, response, null, requestStacktrace); }); } } }); } /** * The openmrsObservableFetch function is a wrapper around openmrsFetch * that returns an [Observable](https://rxjs-dev.firebaseapp.com/guide/observable) * instead of a promise. It exists in case using an Observable is * preferred or more convenient than a promise. * * @param url See [[openmrsFetch]] * @param fetchInit See [[openmrsFetch]] * @returns An Observable that produces exactly one Response object. * The response object is exactly the same as for [[openmrsFetch]]. * * @example * * ```js * import { openmrsObservableFetch } from '@openmrs/esm-api' * const subscription = openmrsObservableFetch(`${restBaseUrl}/session').subscribe( * response => console.log(response.data), * err => {throw err}, * () => console.log('finished') * ) * subscription.unsubscribe() * ``` * * #### Cancellation * * To cancel the network request, simply call `subscription.unsubscribe();` * * @category API */ export function openmrsObservableFetch(url, fetchInit = {}) { if (typeof fetchInit !== 'object') { throw Error('The second argument to openmrsObservableFetch must be either omitted or an object'); } const abortController = new AbortController(); fetchInit.signal = abortController.signal; return new Observable((observer)=>{ let hasResponse = false; openmrsFetch(url, fetchInit).then((response)=>{ hasResponse = true; observer.next(response); observer.complete(); }, (err)=>{ hasResponse = true; observer.error(err); }); return ()=>{ if (!hasResponse) { abortController.abort(); } }; }); } export class OpenmrsFetchError extends Error { constructor(url, response, responseBody, requestStacktrace){ super(), _define_property(this, "response", void 0), _define_property(this, "responseBody", void 0); this.message = `Server responded with ${response.status} (${response.statusText}) for url ${url}. Check err.responseBody or network tab in dev tools for more info`; requestStacktrace.message = this.message; this.responseBody = responseBody; this.response = response; this.stack = `Stacktrace for outgoing request:\n${requestStacktrace.stack}\nStacktrace for incoming response:\n${this.stack}`; } }