@openmrs/esm-api
Version:
The javascript module for interacting with the OpenMRS API
275 lines (274 loc) • 12.2 kB
JavaScript
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}`;
}
}