UNPKG

@openmrs/esm-api

Version:

The javascript module for interacting with the OpenMRS API

339 lines (305 loc) • 11.6 kB
/** @module @category API */ import { Observable } from 'rxjs'; import { isPlainObject } from 'lodash-es'; import { getConfig } from '@openmrs/esm-config'; import { navigate } from '@openmrs/esm-navigation'; import { clearHistory } from '@openmrs/esm-navigation/src/index'; import type { FetchResponse } from './types'; export const restBaseUrl = '/ws/rest/v1'; export const fhirBaseUrl = '/ws/fhir2/R4'; export const sessionEndpoint = `${restBaseUrl}/session`; /** * Append `path` to the OpenMRS SPA base. * * #### Example * * ```ts * makeUrl('/foo/bar'); * // => '/openmrs/foo/bar' * ``` */ export function makeUrl(path: string) { 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<T = any>(path: string, fetchInit: FetchConfig = {}): Promise<FetchResponse<T>> { 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."); } // @ts-ignore 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: string = 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 as RequestInit).then(async (r) => { const response = r as FetchResponse<T>; if (response.ok) { if (response.status === 204) { const { followRedirects } = await getConfig('@openmrs/esm-api'); 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 as unknown as T; 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 */ const { redirectAuthFailure } = await getConfig('@openmrs/esm-api'); if ( (url === makeUrl(sessionEndpoint) && response.status === 403) || (redirectAuthFailure.enabled && redirectAuthFailure.errors.includes(response.status)) ) { clearHistory(); navigate({ to: redirectAuthFailure.url }); /* 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() as unknown as Promise<FetchResponse>) : new Promise<FetchResponse>(() => {}); } 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<T>(url: string, fetchInit: FetchConfig = {}) { 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<FetchResponse<T>>((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 implements FetchError { constructor(url: string, response: Response, responseBody: ResponseBody | null, requestStacktrace: Error) { super(); 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}`; } response: Response; responseBody: string | FetchResponseJson | null; } export interface FetchConfig extends Omit<RequestInit, 'body' | 'headers'> { headers?: FetchHeaders; body?: FetchBody | string; } type ResponseBody = string | FetchResponseJson; export interface FetchHeaders { [key: string]: string | null; } interface FetchBody { [key: string]: any; } export interface FetchResponseJson { [key: string]: any; } export interface FetchError { response: Response; responseBody: ResponseBody | null; }