fhirclient
Version:
JavaScript client for Fast Healthcare Interoperability Resources
1,439 lines (1,410 loc) • 154 kB
JavaScript
/******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ({
/***/ "./src/Client.ts":
/*!***********************!*\
!*** ./src/Client.ts ***!
\***********************/
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
"use strict";
Object.defineProperty(exports, "__esModule", ({
value: true
}));
const lib_1 = __webpack_require__(/*! ./lib */ "./src/lib.ts");
const strings_1 = __webpack_require__(/*! ./strings */ "./src/strings.ts");
const settings_1 = __webpack_require__(/*! ./settings */ "./src/settings.ts");
const FhirClient_1 = __webpack_require__(/*! ./FhirClient */ "./src/FhirClient.ts");
// $lab:coverage:off$
// @ts-ignore
const {
Response
} = true ? window : 0;
// $lab:coverage:on$
const debug = lib_1.debug.extend("client");
/**
* Adds patient context to requestOptions object to be used with [[Client.request]]
* @param requestOptions Can be a string URL (relative to the serviceUrl), or an
* object which will be passed to fetch()
* @param client Current FHIR client object containing patient context
* @return requestOptions object contextualized to current patient
*/
async function contextualize(requestOptions, client) {
const base = (0, lib_1.absolute)("/", client.state.serverUrl);
async function contextualURL(_url) {
const resourceType = _url.pathname.split("/").pop();
(0, lib_1.assert)(resourceType, `Invalid url "${_url}"`);
(0, lib_1.assert)(settings_1.patientCompartment.indexOf(resourceType) > -1, `Cannot filter "${resourceType}" resources by patient`);
const conformance = await (0, lib_1.fetchConformanceStatement)(client.state.serverUrl);
const searchParam = (0, lib_1.getPatientParam)(conformance, resourceType);
_url.searchParams.set(searchParam, client.patient.id);
return _url.href;
}
if (typeof requestOptions == "string" || requestOptions instanceof URL) {
return {
url: await contextualURL(new URL(requestOptions + "", base))
};
}
requestOptions.url = await contextualURL(new URL(requestOptions.url + "", base));
return requestOptions;
}
/**
* This is a FHIR client that is returned to you from the `ready()` call of the
* **SMART API**. You can also create it yourself if needed:
*
* ```js
* // BROWSER
* const client = FHIR.client("https://r4.smarthealthit.org");
*
* // SERVER
* const client = smart(req, res).client("https://r4.smarthealthit.org");
* ```
*/
class Client extends FhirClient_1.default {
/**
* Validates the parameters, creates an instance and tries to connect it to
* FhirJS, if one is available globally.
*/
constructor(environment, state) {
const _state = typeof state == "string" ? {
serverUrl: state
} : state;
// Valid serverUrl is required!
(0, lib_1.assert)(_state.serverUrl && _state.serverUrl.match(/https?:\/\/.+/), "A \"serverUrl\" option is required and must begin with \"http(s)\"");
super(_state.serverUrl);
/**
* @category Utility
*/
this.units = lib_1.units;
this.state = _state;
this.environment = environment;
this._refreshTask = null;
const client = this;
// patient api ---------------------------------------------------------
this.patient = {
get id() {
return client.getPatientId();
},
read: requestOptions => {
const id = this.patient.id;
return id ? this.request({
...requestOptions,
url: `Patient/${id}`
}) : Promise.reject(new Error("Patient is not available"));
},
request: (requestOptions, fhirOptions = {}) => {
if (this.patient.id) {
return (async () => {
const options = await contextualize(requestOptions, this);
return this.request(options, fhirOptions);
})();
} else {
return Promise.reject(new Error("Patient is not available"));
}
}
};
// encounter api -------------------------------------------------------
this.encounter = {
get id() {
return client.getEncounterId();
},
read: requestOptions => {
const id = this.encounter.id;
return id ? this.request({
...requestOptions,
url: `Encounter/${id}`
}) : Promise.reject(new Error("Encounter is not available"));
}
};
// user api ------------------------------------------------------------
this.user = {
get fhirUser() {
return client.getFhirUser();
},
get id() {
return client.getUserId();
},
get resourceType() {
return client.getUserType();
},
read: requestOptions => {
const fhirUser = this.user.fhirUser;
return fhirUser ? this.request({
...requestOptions,
url: fhirUser
}) : Promise.reject(new Error("User is not available"));
}
};
// fhir.js api (attached automatically in browser)
// ---------------------------------------------------------------------
this.connect(environment.fhir);
}
/**
* This method is used to make the "link" between the `fhirclient` and the
* `fhir.js`, if one is available.
* **Note:** This is called by the constructor. If fhir.js is available in
* the global scope as `fhir`, it will automatically be linked to any [[Client]]
* instance. You should only use this method to connect to `fhir.js` which
* is not global.
*/
connect(fhirJs) {
if (typeof fhirJs == "function") {
const options = {
baseUrl: this.state.serverUrl.replace(/\/$/, "")
};
const accessToken = this.getState("tokenResponse.access_token");
if (accessToken) {
options.auth = {
token: accessToken
};
} else {
const {
username,
password
} = this.state;
if (username && password) {
options.auth = {
user: username,
pass: password
};
}
}
this.api = fhirJs(options);
const patientId = this.getState("tokenResponse.patient");
if (patientId) {
this.patient.api = fhirJs({
...options,
patient: patientId
});
}
}
return this;
}
/**
* Returns the ID of the selected patient or null. You should have requested
* "launch/patient" scope. Otherwise this will return null.
*/
getPatientId() {
const tokenResponse = this.state.tokenResponse;
if (tokenResponse) {
// We have been authorized against this server but we don't know
// the patient. This should be a scope issue.
if (!tokenResponse.patient) {
if (!(this.state.scope || "").match(/\blaunch(\/patient)?\b/)) {
debug(strings_1.default.noScopeForId, "patient", "patient");
} else {
// The server should have returned the patient!
debug("The ID of the selected patient is not available. Please check if your server supports that.");
}
return null;
}
return tokenResponse.patient;
}
if (this.state.authorizeUri) {
debug(strings_1.default.noIfNoAuth, "the ID of the selected patient");
} else {
debug(strings_1.default.noFreeContext, "selected patient");
}
return null;
}
/**
* Returns the ID of the selected encounter or null. You should have
* requested "launch/encounter" scope. Otherwise this will return null.
* Note that not all servers support the "launch/encounter" scope so this
* will be null if they don't.
*/
getEncounterId() {
const tokenResponse = this.state.tokenResponse;
if (tokenResponse) {
// We have been authorized against this server but we don't know
// the encounter. This should be a scope issue.
if (!tokenResponse.encounter) {
if (!(this.state.scope || "").match(/\blaunch(\/encounter)?\b/)) {
debug(strings_1.default.noScopeForId, "encounter", "encounter");
} else {
// The server should have returned the encounter!
debug("The ID of the selected encounter is not available. Please check if your server supports that, and that the selected patient has any recorded encounters.");
}
return null;
}
return tokenResponse.encounter;
}
if (this.state.authorizeUri) {
debug(strings_1.default.noIfNoAuth, "the ID of the selected encounter");
} else {
debug(strings_1.default.noFreeContext, "selected encounter");
}
return null;
}
/**
* Returns the (decoded) id_token if any. You need to request "openid" and
* "profile" scopes if you need to receive an id_token (if you need to know
* who the logged-in user is).
*/
getIdToken() {
const tokenResponse = this.state.tokenResponse;
if (tokenResponse) {
const idToken = tokenResponse.id_token;
const scope = this.state.scope || "";
// We have been authorized against this server but we don't have
// the id_token. This should be a scope issue.
if (!idToken) {
const hasOpenid = scope.match(/\bopenid\b/);
const hasProfile = scope.match(/\bprofile\b/);
const hasFhirUser = scope.match(/\bfhirUser\b/);
if (!hasOpenid || !(hasFhirUser || hasProfile)) {
debug("You are trying to get the id_token but you are not " + "using the right scopes. Please add 'openid' and " + "'fhirUser' or 'profile' to the scopes you are " + "requesting.");
} else {
// The server should have returned the id_token!
debug("The id_token is not available. Please check if your server supports that.");
}
return null;
}
return (0, lib_1.jwtDecode)(idToken, this.environment);
}
if (this.state.authorizeUri) {
debug(strings_1.default.noIfNoAuth, "the id_token");
} else {
debug(strings_1.default.noFreeContext, "id_token");
}
return null;
}
/**
* Returns the profile of the logged_in user (if any). This is a string
* having the following shape `"{user type}/{user id}"`. For example:
* `"Practitioner/abc"` or `"Patient/xyz"`.
*/
getFhirUser() {
const idToken = this.getIdToken();
if (idToken) {
// Epic may return a full url
// @see https://github.com/smart-on-fhir/client-js/issues/105
if (idToken.fhirUser) {
return idToken.fhirUser.split("/").slice(-2).join("/");
}
return idToken.profile;
}
return null;
}
/**
* Returns the user ID or null.
*/
getUserId() {
const profile = this.getFhirUser();
if (profile) {
return profile.split("/")[1];
}
return null;
}
/**
* Returns the type of the logged-in user or null. The result can be
* "Practitioner", "Patient" or "RelatedPerson".
*/
getUserType() {
const profile = this.getFhirUser();
if (profile) {
return profile.split("/")[0];
}
return null;
}
/**
* Builds and returns the value of the `Authorization` header that can be
* sent to the FHIR server
*/
getAuthorizationHeader() {
const accessToken = this.getState("tokenResponse.access_token");
if (accessToken) {
return "Bearer " + accessToken;
}
const {
username,
password
} = this.state;
if (username && password) {
return "Basic " + this.environment.btoa(username + ":" + password);
}
return null;
}
/**
* Used internally to clear the state of the instance and the state in the
* associated storage.
*/
async _clearState() {
const storage = this.environment.getStorage();
const key = await storage.get(settings_1.SMART_KEY);
if (key) {
await storage.unset(key);
}
await storage.unset(settings_1.SMART_KEY);
this.state.tokenResponse = {};
}
/**
* Default request options to be used for every request.
*/
async getRequestDefaults() {
const authHeader = this.getAuthorizationHeader();
return {
headers: {
...(authHeader ? {
authorization: authHeader
} : {})
}
};
}
/**
* @param requestOptions Can be a string URL (relative to the serviceUrl),
* or an object which will be passed to fetch()
* @param fhirOptions Additional options to control the behavior
* @param _resolvedRefs DO NOT USE! Used internally.
* @category Request
*/
async request(requestOptions, fhirOptions = {}, _resolvedRefs = {}) {
var _a;
const debugRequest = lib_1.debug.extend("client:request");
(0, lib_1.assert)(requestOptions, "request requires an url or request options as argument");
// url -----------------------------------------------------------------
let url;
if (typeof requestOptions == "string" || requestOptions instanceof URL) {
url = String(requestOptions);
requestOptions = {};
} else {
url = String(requestOptions.url);
}
url = (0, lib_1.absolute)(url, this.state.serverUrl);
const options = {
graph: fhirOptions.graph !== false,
flat: !!fhirOptions.flat,
pageLimit: (_a = fhirOptions.pageLimit) !== null && _a !== void 0 ? _a : 1,
resolveReferences: (0, lib_1.makeArray)(fhirOptions.resolveReferences || []),
useRefreshToken: fhirOptions.useRefreshToken !== false,
onPage: typeof fhirOptions.onPage == "function" ? fhirOptions.onPage : undefined
};
const signal = requestOptions.signal || undefined;
// Refresh the access token if needed
if (options.useRefreshToken) {
await this.refreshIfNeeded({
signal
});
}
// Add the Authorization header now, after the access token might
// have been updated
const authHeader = this.getAuthorizationHeader();
if (authHeader) {
requestOptions.headers = {
...requestOptions.headers,
authorization: authHeader
};
}
debugRequest("%s, options: %O, fhirOptions: %O", url, requestOptions, options);
let response;
return super.fhirRequest(url, requestOptions).then(result => {
if (requestOptions.includeResponse) {
response = result.response;
return result.body;
}
return result;
})
// Handle 401 ----------------------------------------------------------
.catch(async error => {
if (error.status == 401) {
// !accessToken -> not authorized -> No session. Need to launch.
if (!this.getState("tokenResponse.access_token")) {
error.message += "\nThis app cannot be accessed directly. Please launch it as SMART app!";
throw error;
}
// auto-refresh not enabled and Session expired.
// Need to re-launch. Clear state to start over!
if (!options.useRefreshToken) {
debugRequest("Your session has expired and the useRefreshToken option is set to false. Please re-launch the app.");
await this._clearState();
error.message += "\n" + strings_1.default.expired;
throw error;
}
// In rare cases we may have a valid access token and a refresh
// token and the request might still fail with 401 just because
// the access token has just been revoked.
// otherwise -> auto-refresh failed. Session expired.
// Need to re-launch. Clear state to start over!
debugRequest("Auto-refresh failed! Please re-launch the app.");
await this._clearState();
error.message += "\n" + strings_1.default.expired;
throw error;
}
throw error;
})
// Handle 403 ----------------------------------------------------------
.catch(error => {
if (error.status == 403) {
debugRequest("Permission denied! Please make sure that you have requested the proper scopes.");
}
throw error;
}).then(async data => {
// At this point we don't know what `data` actually is!
// We might get an empty or falsy result. If so return it as is
// Also handle raw responses
if (!data || typeof data == "string" || data instanceof Response) {
if (requestOptions.includeResponse) {
return {
body: data,
response
};
}
return data;
}
// Resolve References ----------------------------------------------
await this.fetchReferences(data, options.resolveReferences, options.graph, _resolvedRefs, requestOptions);
return Promise.resolve(data)
// Pagination ------------------------------------------------------
.then(async _data => {
if (_data && _data.resourceType == "Bundle") {
const links = _data.link || [];
if (options.flat) {
_data = (_data.entry || []).map(entry => entry.resource);
}
if (options.onPage) {
await options.onPage(_data, {
..._resolvedRefs
});
}
if (--options.pageLimit) {
const next = links.find(l => l.relation == "next");
_data = (0, lib_1.makeArray)(_data);
if (next && next.url) {
const nextPage = await this.request({
url: next.url,
// Aborting the main request (even after it is complete)
// must propagate to any child requests and abort them!
// To do so, just pass the same AbortSignal if one is
// provided.
signal
}, options, _resolvedRefs);
if (options.onPage) {
return null;
}
if (options.resolveReferences.length) {
Object.assign(_resolvedRefs, nextPage.references);
return _data.concat((0, lib_1.makeArray)(nextPage.data || nextPage));
}
return _data.concat((0, lib_1.makeArray)(nextPage));
}
}
}
return _data;
})
// Finalize --------------------------------------------------------
.then(_data => {
if (options.graph) {
_resolvedRefs = {};
} else if (!options.onPage && options.resolveReferences.length) {
return {
data: _data,
references: _resolvedRefs
};
}
return _data;
}).then(_data => {
if (requestOptions.includeResponse) {
return {
body: _data,
response
};
}
return _data;
});
});
}
/**
* Checks if access token and refresh token are present. If they are, and if
* the access token is expired or is about to expire in the next 10 seconds,
* calls `this.refresh()` to obtain new access token.
* @param requestOptions Any options to pass to the fetch call. Most of them
* will be overridden, bit it might still be useful for passing additional
* request options or an abort signal.
* @category Request
*/
refreshIfNeeded(requestOptions = {}) {
const accessToken = this.getState("tokenResponse.access_token");
const refreshToken = this.getState("tokenResponse.refresh_token");
const expiresAt = this.state.expiresAt || 0;
if (accessToken && refreshToken && expiresAt - 10 < Date.now() / 1000) {
return this.refresh(requestOptions);
}
return Promise.resolve(this.state);
}
/**
* Use the refresh token to obtain new access token. If the refresh token is
* expired (or this fails for any other reason) it will be deleted from the
* state, so that we don't enter into loops trying to re-authorize.
*
* This method is typically called internally from [[request]] if
* certain request fails with 401.
*
* @param requestOptions Any options to pass to the fetch call. Most of them
* will be overridden, bit it might still be useful for passing additional
* request options or an abort signal.
* @category Request
*/
refresh(requestOptions = {}) {
var _a, _b;
const debugRefresh = lib_1.debug.extend("client:refresh");
debugRefresh("Attempting to refresh with refresh_token...");
const refreshToken = (_b = (_a = this.state) === null || _a === void 0 ? void 0 : _a.tokenResponse) === null || _b === void 0 ? void 0 : _b.refresh_token;
(0, lib_1.assert)(refreshToken, "Unable to refresh. No refresh_token found.");
const tokenUri = this.state.tokenUri;
(0, lib_1.assert)(tokenUri, "Unable to refresh. No tokenUri found.");
const scopes = this.getState("tokenResponse.scope") || "";
const hasOfflineAccess = scopes.search(/\boffline_access\b/) > -1;
const hasOnlineAccess = scopes.search(/\bonline_access\b/) > -1;
(0, lib_1.assert)(hasOfflineAccess || hasOnlineAccess, "Unable to refresh. No offline_access or online_access scope found.");
// This method is typically called internally from `request` if certain
// request fails with 401. However, clients will often run multiple
// requests in parallel which may result in multiple refresh calls.
// To avoid that, we keep a reference to the current refresh task (if any).
if (!this._refreshTask) {
let body = `grant_type=refresh_token&refresh_token=${encodeURIComponent(refreshToken)}`;
if (this.environment.options.refreshTokenWithClientId) {
body += `&client_id=${this.state.clientId}`;
}
const refreshRequestOptions = {
credentials: this.environment.options.refreshTokenWithCredentials || "same-origin",
...requestOptions,
method: "POST",
mode: "cors",
headers: {
...(requestOptions.headers || {}),
"content-type": "application/x-www-form-urlencoded"
},
body: body
};
// custom authorization header can be passed on manual calls
if (!("authorization" in refreshRequestOptions.headers)) {
const {
clientSecret,
clientId
} = this.state;
if (clientSecret) {
// @ts-ignore
refreshRequestOptions.headers.authorization = "Basic " + this.environment.btoa(clientId + ":" + clientSecret);
}
}
this._refreshTask = (0, lib_1.request)(tokenUri, refreshRequestOptions).then(data => {
(0, lib_1.assert)(data.access_token, "No access token received");
debugRefresh("Received new access token response %O", data);
this.state.tokenResponse = {
...this.state.tokenResponse,
...data
};
this.state.expiresAt = (0, lib_1.getAccessTokenExpiration)(data, this.environment);
return this.state;
}).catch(error => {
var _a, _b;
if ((_b = (_a = this.state) === null || _a === void 0 ? void 0 : _a.tokenResponse) === null || _b === void 0 ? void 0 : _b.refresh_token) {
debugRefresh("Deleting the expired or invalid refresh token.");
delete this.state.tokenResponse.refresh_token;
}
throw error;
}).finally(() => {
this._refreshTask = null;
const key = this.state.key;
if (key) {
this.environment.getStorage().set(key, this.state);
} else {
debugRefresh("No 'key' found in Clint.state. Cannot persist the instance.");
}
});
}
return this._refreshTask;
}
// utils -------------------------------------------------------------------
/**
* Groups the observations by code. Returns a map that will look like:
* ```js
* const map = client.byCodes(observations, "code");
* // map = {
* // "55284-4": [ observation1, observation2 ],
* // "6082-2": [ observation3 ]
* // }
* ```
* @param observations Array of observations
* @param property The name of a CodeableConcept property to group by
* @todo This should be deprecated and moved elsewhere. One should not have
* to obtain an instance of [[Client]] just to use utility functions like this.
* @deprecated
* @category Utility
*/
byCode(observations, property) {
return (0, lib_1.byCode)(observations, property);
}
/**
* First groups the observations by code using `byCode`. Then returns a function
* that accepts codes as arguments and will return a flat array of observations
* having that codes. Example:
* ```js
* const filter = client.byCodes(observations, "category");
* filter("laboratory") // => [ observation1, observation2 ]
* filter("vital-signs") // => [ observation3 ]
* filter("laboratory", "vital-signs") // => [ observation1, observation2, observation3 ]
* ```
* @param observations Array of observations
* @param property The name of a CodeableConcept property to group by
* @todo This should be deprecated and moved elsewhere. One should not have
* to obtain an instance of [[Client]] just to use utility functions like this.
* @deprecated
* @category Utility
*/
byCodes(observations, property) {
return (0, lib_1.byCodes)(observations, property);
}
/**
* Walks through an object (or array) and returns the value found at the
* provided path. This function is very simple so it intentionally does not
* support any argument polymorphism, meaning that the path can only be a
* dot-separated string. If the path is invalid returns undefined.
* @param obj The object (or Array) to walk through
* @param path The path (eg. "a.b.4.c")
* @returns {*} Whatever is found in the path or undefined
* @todo This should be deprecated and moved elsewhere. One should not have
* to obtain an instance of [[Client]] just to use utility functions like this.
* @deprecated
* @category Utility
*/
getPath(obj, path = "") {
return (0, lib_1.getPath)(obj, path);
}
/**
* Returns a copy of the client state. Accepts a dot-separated path argument
* (same as for `getPath`) to allow for selecting specific properties.
* Examples:
* ```js
* client.getState(); // -> the entire state object
* client.getState("serverUrl"); // -> the URL we are connected to
* client.getState("tokenResponse.patient"); // -> The selected patient ID (if any)
* ```
* @param path The path (eg. "a.b.4.c")
* @returns {*} Whatever is found in the path or undefined
*/
getState(path = "") {
return (0, lib_1.getPath)({
...this.state
}, path);
}
}
exports["default"] = Client;
/***/ }),
/***/ "./src/FhirClient.ts":
/*!***************************!*\
!*** ./src/FhirClient.ts ***!
\***************************/
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
"use strict";
Object.defineProperty(exports, "__esModule", ({
value: true
}));
const settings_1 = __webpack_require__(/*! ./settings */ "./src/settings.ts");
const lib_1 = __webpack_require__(/*! ./lib */ "./src/lib.ts");
const debug = lib_1.debug.extend("FhirClient");
/**
* This is a basic FHIR client for making basic FHIR API calls
*/
class FhirClient {
/**
* Validates the parameters, creates an instance and tries to connect it to
* FhirJS, if one is available globally.
*/
constructor(fhirBaseUrl) {
(0, lib_1.assert)(fhirBaseUrl && typeof fhirBaseUrl === "string" && fhirBaseUrl.match(/https?:\/\/.+/), "A \"fhirBaseUrl\" string parameter is required and must begin with \"http(s)\"");
this.fhirBaseUrl = fhirBaseUrl;
}
/**
* Default request options to be used for every request. This method can be
* overridden in subclasses to provide custom default options.
*/
async getRequestDefaults() {
return {};
}
/**
* Creates a new resource in a server-assigned location
* @see http://hl7.org/fhir/http.html#create
* @param resource A FHIR resource to be created
* @param [requestOptions] Any options to be passed to the fetch call.
* Note that `method` and `body` will be ignored.
* @category Request
*/
async create(resource, requestOptions) {
return this.fhirRequest(resource.resourceType, {
...requestOptions,
method: "POST",
body: JSON.stringify(resource),
headers: {
"content-type": "application/json",
...(requestOptions || {}).headers
}
});
}
/**
* Creates a new current version for an existing resource or creates an
* initial version if no resource already exists for the given id.
* @see http://hl7.org/fhir/http.html#update
* @param resource A FHIR resource to be updated
* @param requestOptions Any options to be passed to the fetch call.
* Note that `method` and `body` will be ignored.
* @category Request
*/
async update(resource, requestOptions) {
return this.fhirRequest(`${resource.resourceType}/${resource.id}`, {
...requestOptions,
method: "PUT",
body: JSON.stringify(resource),
headers: {
"content-type": "application/json",
...(requestOptions || {}).headers
}
});
}
/**
* Removes an existing resource.
* @see http://hl7.org/fhir/http.html#delete
* @param url Relative URI of the FHIR resource to be deleted
* (format: `resourceType/id`)
* @param requestOptions Any options (except `method` which will be fixed
* to `DELETE`) to be passed to the fetch call.
* @category Request
*/
async delete(url, requestOptions = {}) {
return this.fhirRequest(url, {
...requestOptions,
method: "DELETE"
});
}
/**
* Makes a JSON Patch to the given resource
* @see http://hl7.org/fhir/http.html#patch
* @param url Relative URI of the FHIR resource to be patched
* (format: `resourceType/id`)
* @param patch A JSON Patch array to send to the server, For details
* see https://datatracker.ietf.org/doc/html/rfc6902
* @param requestOptions Any options to be passed to the fetch call,
* except for `method`, `url` and `body` which cannot be overridden.
* @since 2.4.0
* @category Request
* @typeParam ResolveType This method would typically resolve with the
* patched resource or reject with an OperationOutcome. However, this may
* depend on the server implementation or even on the request headers.
* For that reason, if the default resolve type (which is
* [[fhirclient.FHIR.Resource]]) does not work for you, you can pass
* in your own resolve type parameter.
*/
async patch(url, patch, requestOptions = {}) {
(0, lib_1.assertJsonPatch)(patch);
return this.fhirRequest(url, {
...requestOptions,
method: "PATCH",
body: JSON.stringify(patch),
headers: {
"prefer": "return=presentation",
"content-type": "application/json-patch+json; charset=UTF-8",
...requestOptions.headers
}
});
}
async resolveRef(obj, path, graph, cache, requestOptions = {}) {
const node = (0, lib_1.getPath)(obj, path);
if (node) {
const isArray = Array.isArray(node);
return Promise.all((0, lib_1.makeArray)(node).filter(Boolean).map((item, i) => {
const ref = item.reference;
if (ref) {
return this.fhirRequest(ref, {
...requestOptions,
includeResponse: false,
cacheMap: cache
}).then(sub => {
if (graph) {
if (isArray) {
if (path.indexOf("..") > -1) {
(0, lib_1.setPath)(obj, `${path.replace("..", `.${i}.`)}`, sub);
} else {
(0, lib_1.setPath)(obj, `${path}.${i}`, sub);
}
} else {
(0, lib_1.setPath)(obj, path, sub);
}
}
}).catch(ex => {
if ((ex === null || ex === void 0 ? void 0 : ex.status) === 404) {
console.warn(`Missing reference ${ref}. ${ex}`);
} else {
throw ex;
}
});
}
}));
}
}
/**
* Fetches all references in the given resource, ignoring duplicates, and
* then modifies the resource by "mounting" the resolved references in place
*/
async resolveReferences(resource, references, requestOptions = {}) {
await this.fetchReferences(resource, references, true, {}, requestOptions);
}
async fetchReferences(resource, references, graph, cache = {}, requestOptions = {}) {
if (resource.resourceType == "Bundle") {
for (const item of resource.entry || []) {
if (item.resource) {
await this.fetchReferences(item.resource, references, graph, cache, requestOptions);
}
}
return cache;
}
// 1. Sanitize paths, remove any invalid ones
let paths = references.map(path => String(path).trim()).filter(Boolean);
// 2. Remove duplicates
paths = paths.reduce((prev, cur) => {
if (prev.includes(cur)) {
debug("Duplicated reference path \"%s\"", cur);
} else {
prev.push(cur);
}
return prev;
}, []);
// 3. Early exit if no valid paths are found
if (!paths.length) {
return Promise.resolve(cache);
}
// 4. Group the paths by depth so that child refs are looked up
// after their parents!
const groups = {};
paths.forEach(path => {
const len = path.split(".").length;
if (!groups[len]) {
groups[len] = [];
}
groups[len].push(path);
});
// 5. Execute groups sequentially! Paths within same group are
// fetched in parallel!
let task = Promise.resolve();
Object.keys(groups).sort().forEach(len => {
const group = groups[len];
task = task.then(() => Promise.all(group.map(path => {
return this.resolveRef(resource, path, graph, cache, requestOptions);
})));
});
await task;
return cache;
}
/**
* Fetches all references in the given resource, ignoring duplicates
*/
async getReferences(resource, references, requestOptions = {}) {
const refs = await this.fetchReferences(resource, references, false, {}, requestOptions);
const out = {};
for (const key in refs) {
out[key] = await refs[key];
}
return out;
}
/**
* Given a FHIR Bundle or a URL pointing to a bundle, iterates over all
* entry resources. Note that this will also automatically crawl through
* further pages (if any)
*/
async *resources(bundleOrUrl, options) {
let count = 0;
for await (const page of this.pages(bundleOrUrl, options)) {
for (const entry of page.entry || []) {
if ((options === null || options === void 0 ? void 0 : options.limit) && ++count > options.limit) {
return;
}
yield entry.resource;
}
}
}
/**
* Given a FHIR Bundle or a URL pointing to a bundle, iterates over all
* pages. Note that this will automatically crawl through
* further pages (if any) but it will not detect previous pages. It is
* designed to be called on the first page and fetch any followup pages.
*/
async *pages(bundleOrUrl, requestOptions) {
var _a, _b;
const {
limit,
...options
} = requestOptions || {};
const fetchPage = url => this.fhirRequest(url, options);
let page = typeof bundleOrUrl === "string" || bundleOrUrl instanceof URL ? await fetchPage(bundleOrUrl) : bundleOrUrl;
let count = 0;
while (page && page.resourceType === "Bundle" && (!limit || ++count <= limit)) {
// Yield the current page
yield page;
// If caller aborted, stop crawling
if ((_a = options === null || options === void 0 ? void 0 : options.signal) === null || _a === void 0 ? void 0 : _a.aborted) {
break;
}
// Find the "next" link
const nextLink = ((_b = page.link) !== null && _b !== void 0 ? _b : []).find(l => l.relation === 'next' && typeof l.url === 'string');
if (!nextLink) {
break; // no more pages
}
// Fetch the next page
page = await fetchPage(nextLink.url);
}
}
/**
* The method responsible for making all http requests
*/
async fhirRequest(uri, options = {}) {
(0, lib_1.assert)(options, "fhirRequest requires a uri as first argument");
const getRequestDefaults = await this.getRequestDefaults();
options = {
...getRequestDefaults,
...options,
headers: {
...(getRequestDefaults.headers || {}),
...(options.headers || {})
}
};
const path = uri + "";
const url = (0, lib_1.absolute)(path, this.fhirBaseUrl);
const {
cacheMap
} = options;
if (cacheMap) {
if (!(path in cacheMap)) {
cacheMap[path] = (0, lib_1.request)(url, options).then(res => {
cacheMap[path] = res;
return res;
}).catch(error => {
delete cacheMap[path];
throw error;
});
}
return cacheMap[path];
}
return (0, lib_1.request)(url, options);
}
/**
* Returns a promise that will be resolved with the fhir version as defined
* in the CapabilityStatement.
*/
async getFhirVersion() {
return (0, lib_1.fetchConformanceStatement)(this.fhirBaseUrl).then(metadata => metadata.fhirVersion);
}
/**
* Returns a promise that will be resolved with the numeric fhir version
* - 2 for DSTU2
* - 3 for STU3
* - 4 for R4
* - 0 if the version is not known
*/
async getFhirRelease() {
return this.getFhirVersion().then(v => {
var _a;
return (_a = settings_1.fhirVersions[v]) !== null && _a !== void 0 ? _a : 0;
});
}
}
exports["default"] = FhirClient;
/***/ }),
/***/ "./src/HttpError.ts":
/*!**************************!*\
!*** ./src/HttpError.ts ***!
\**************************/
/***/ ((__unused_webpack_module, exports) => {
"use strict";
Object.defineProperty(exports, "__esModule", ({
value: true
}));
class HttpError extends Error {
constructor(response) {
super(`${response.status} ${response.statusText}\nURL: ${response.url}`);
this.name = "HttpError";
this.response = response;
this.statusCode = response.status;
this.status = response.status;
this.statusText = response.statusText;
}
async parse() {
if (!this.response.bodyUsed) {
try {
const type = this.response.headers.get("content-type") || "text/plain";
if (type.match(/\bjson\b/i)) {
let body = await this.response.json();
if (body.error) {
this.message += "\n" + body.error;
if (body.error_description) {
this.message += ": " + body.error_description;
}
} else {
this.message += "\n\n" + JSON.stringify(body, null, 4);
}
} else if (type.match(/^text\//i)) {
let body = await this.response.text();
if (body) {
this.message += "\n\n" + body;
}
}
} catch {
// ignore
}
}
return this;
}
toJSON() {
return {
name: this.name,
statusCode: this.statusCode,
status: this.status,
statusText: this.statusText,
message: this.message
};
}
}
exports["default"] = HttpError;
/***/ }),
/***/ "./src/adapters/BrowserAdapter.ts":
/*!****************************************!*\
!*** ./src/adapters/BrowserAdapter.ts ***!
\****************************************/
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
"use strict";
Object.defineProperty(exports, "__esModule", ({
value: true
}));
const smart_1 = __webpack_require__(/*! ../smart */ "./src/smart.ts");
const Client_1 = __webpack_require__(/*! ../Client */ "./src/Client.ts");
const BrowserStorage_1 = __webpack_require__(/*! ../storage/BrowserStorage */ "./src/storage/BrowserStorage.ts");
const security = __webpack_require__(/*! ../security/browser */ "./src/security/browser.ts");
const js_base64_1 = __webpack_require__(/*! js-base64 */ "./node_modules/js-base64/base64.js");
/**
* Browser Adapter
*/
class BrowserAdapter {
/**
* @param options Environment-specific options
*/
constructor(options = {}) {
/**
* Stores the URL instance associated with this adapter
*/
this._url = null;
/**
* Holds the Storage instance associated with this instance
*/
this._storage = null;
this.security = security;
this.options = {
// Replaces the browser's current URL
// using window.history.replaceState API or by reloading.
replaceBrowserHistory: true,
// When set to true, this variable will fully utilize
// HTML5 sessionStorage API.
// This variable can be overridden to false by setting
// FHIR.oauth2.settings.fullSessionStorageSupport = false.
// When set to false, the sessionStorage will be keyed
// by a state variable. This is to allow the embedded IE browser
// instances instantiated on a single thread to continue to
// function without having sessionStorage data shared
// across the embedded IE instances.
fullSessionStorageSupport: true,
// Do we want to send cookies while making a request to the token
// endpoint in order to obtain new access token using existing
// refresh token. In rare cases the auth server might require the
// client to send cookies along with those requests. In this case
// developers will have to change this before initializing the app
// like so:
// `FHIR.oauth2.settings.refreshTokenWithCredentials = "include";`
// or
// `FHIR.oauth2.settings.refreshTokenWithCredentials = "same-origin";`
// Can be one of:
// "include" - always send cookies
// "same-origin" - only send cookies if we are on the same domain (default)
// "omit" - do not send cookies
refreshTokenWithCredentials: "same-origin",
...options
};
}
/**
* Given a relative path, returns an absolute url using the instance base URL
*/
relative(path) {
return new URL(path, this.getUrl().href).href;
}
/**
* In browsers we need to be able to (dynamically) check if fhir.js is
* included in the page. If it is, it should have created a "fhir" variable
* in the global scope.
*/
get fhir() {
// @ts-ignore
return typeof fhir === "function" ? fhir : null;
}
/**
* Given the current environment, this method must return the current url
* as URL instance
*/
getUrl() {
if (!this._url) {
this._url = new URL(location + "");
}
return this._url;
}
/**
* Given the current environment, this method must redirect to the given
* path
*/
redirect(to) {
location.href = to;
}
/**
* Returns a BrowserStorage object which is just a wrapper around
* sessionStorage
*/
getStorage() {
if (!this._storage) {
this._storage = new BrowserStorage_1.default();
}
return this._storage;
}
/**
* Returns a reference to the AbortController constructor. In browsers,
* AbortController will always be available as global (native or polyfilled)
*/
getAbortController() {
return AbortController;
}
/**
* ASCII string to Base64
*/
atob(str) {
return window.atob(str);
}
/**
* Base64 to ASCII string
*/
btoa(str) {
return window.btoa(str);
}
base64urlencode(input) {
if (typeof input == "string") {
return (0, js_base64_1.encodeURL)(input);
}
return (0, js_base64_1.fromUint8Array)(input, true);
}
base64urldecode(input) {
return (0, js_base64_1.decode)(input);
}
/**
* Creates and returns adapter-aware SMART api. Not that while the shape of
* the returned object is well known, the arguments to this function are not.
* Those who override this method are free to require any environment-specific
* arguments. For example in node we will need a request, a response and
* optionally a storage or storage factory function.
*/
getSmartApi() {
return {
ready: (...args) => (0, smart_1.ready)(this, ...args),
authorize: options => (0, smart_1.authorize)(this, options),
init: options => (0, smart_1.init)(this, options),
client: state => new Client_1.default(this, state),
options: this.options,
utils: {
security
}
};
}
}
exports["default"] = BrowserAdapter;
/***/ }),
/***/ "./src/entry/browser.ts":
/*!******************************!*\
!*** ./src/entry/browser.ts ***!
\******************************/
/***/ ((module, __unused_webpack_exports, __webpack_require__) => {
"use strict";
// In Browsers we create an adapter, get the SMART api from it and build the
// global FHIR object
const BrowserAdapter_1 = __webpack_require__(/*! ../adapters/BrowserAdapter */ "./src/adapters/BrowserAdapter.ts");
const FhirClient_1 = __webpack_require__(/*! ../FhirClient */ "./src/FhirClient.ts");
const adapter = new BrowserAdapter_1.default();
const {
ready,
authorize,
init,
client,
options,
utils
} = adapter.getSmartApi();
// We have two kinds of browser builds - "pure" for new browsers and "legacy"
// for old ones. In pure builds we assume that the browser supports everything
// we need. In legacy mode, the library also acts as a polyfill. Babel will
// automatically polyfill everything except "fetch", which we have to handle
// manually.
// @ts-ignore
if (false) {}
// $lab:coverage:off$
const FHIR = {
AbortController: window.AbortController,
client,
/**
* Using this class if you are connecting to open server that does not
* require authorization.
*/
FhirClient: FhirClient_1.default,
utils,
oauth2: {
settings: options,
ready,
authorize,
init
}
};
module.exports = FHIR;
// $lab:coverage:on$
/***/ }),
/***/ "./src/lib.ts":
/*!********************!*\
!*** ./src/lib.ts ***!
\********************/
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
"use strict";
/*
* This file contains some shared functions. They are used by other modules, but
* are defined here so that tests can import this library and test them.
*/
Object.defineProperty(exports, "__esModule", ({
value: true
}));
exports.assertJsonPatch = exports.assert = exports.getTargetWindow = exports.getPatientParam = exports.byCodes = exports.byCode = exports.getAccessTokenExpiration = exports.getTimeInFuture = exports.jwtDecode = exports.randomString = exports.absolute = exports.makeArray = exports.setPath = exports.getPath = exports.fetchConformanceStatement = exports.getAndCache = exports.request = exports.loweCaseKeys = exports.responseToJSON = exports.checkResponse = exports.units = exports.debug = void 0;
const HttpError_1 = __webpack_require__(/*! ./HttpError */ "./src/HttpError.ts");
const settings_1 = __webpack_require__(/*! ./settings */ "./src/settings.ts");
const debug = __webpack_require__(/*! debug */ "./node_modules/debug/src/browser.js");
// $lab:coverage:off$
// @ts-ignore
const {
fetch
} = true ? window : 0;
// $lab:coverage:on$
const _debug = debug("FHIR");
exports.debug = _debug;
/**
* The cache for the `getAndCache` function
*/
const cache = {};
/**
* A namespace with functions for converting between different measurement units
*/
exports.units = {
cm({
code,
value
}) {
ensureNumerical({
code,
value
});
if (code == "cm") return value;
if (code == "m") return value * 100;
if (code == "in") return value * 2.54;
if (code == "[in_us]") return value * 2.54;
if (code == "[in_i]") return value * 2.54;
if (code == "ft") return value * 30.48;
if (code == "[ft_us]") return value * 30.48;
throw new Error("Unrecognized length unit: " + code);
},
kg({
code,
value
}) {
ensureNumerical({
code,
value
});
if (code == "kg") return value;
if (code == "g") return value / 1000;
if (code.match(/lb/)) return value / 2.20462;
if (code.match(/oz/)) return value / 35.274;
throw new Error("Unrecognized weight unit: " + code);
},
any(pq) {
ensureNumerical(pq);
return pq.value;
}
};
/**
* Assertion function to guard arguments for `units` functions
*/
function ensureNumerical({
value,
code
}) {
if (typeof value !== "number") {
throw new Error("Found a non-numerical unit: " + value + " " + code);
}
}
/**
* Used in fetch Promise chains to reject if the "ok" property is not true
*/
async function checkResponse(resp) {
if (!resp.ok) {
const error = new HttpError_1.default(resp);
await error.parse();
throw error;
}
return resp;
}
exports.checkResponse = checkResponse;
/**
* Used in fetch Promise chains to return the JSON version of the response.
* Note that `resp.json()` will throw on empty body so we use resp.text()
* instead.
*/
function responseToJSON(resp) {
return resp.text().then(text => text.length ? JSON.parse(text) : "");
}
exports.responseToJSON = responseToJSON;
function loweCaseKeys(obj) {
// Can be undefined to signal that this key should be removed
if (!obj) {
return obj;
}
// Arrays are valid values in case of recursive calls
if (Array.isArray(obj)) {
return obj.map(v => v && typeof v === "object" ? loweCaseKeys(v) : v);
}
// Plain object
let out = {};
Object.keys(obj).forEach(key => {
const lowerKey = key.toLowerCase();
const v = obj[key];
out[lowerKey] = v && typeof v == "object" ? loweCaseKeys(v) : v;
});
return out;
}
exports.loweCaseKeys = loweCaseKeys;
/**
* This is our built-in request function. It does a few things by default
* (unless told otherwise):
* - Makes CORS requests
* - Sets accept header to "application/json"
* - Handles errors
* - If the response is json return the json object
* - If the response is text return the result text
* - Otherwise return the response object on which we call stuff like `.blob()`
*/
function request(url, requestOptions = {}) {
const {
includeResponse,
...options
} = requestOptions;
return fetch(url, {
mode: "cors",
...options,
headers: {
accept: "application/json",
...loweCaseKeys(options.headers)
}
}).then(checkResponse).then(res => {
const type = res.headers.get("content-type") + "";
if (type.match(/\bjson\b/i)) {
return responseToJSO