fhirclient
Version:
JavaScript client for Fast Healthcare Interoperability Resources
376 lines (375 loc) • 13.5 kB
JavaScript
;
var __asyncValues = void 0 && (void 0).__asyncValues || function (o) {
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
var m = o[Symbol.asyncIterator],
i;
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () {
return this;
}, i);
function verb(n) {
i[n] = o[n] && function (v) {
return new Promise(function (resolve, reject) {
v = o[n](v), settle(resolve, reject, v.done, v.value);
});
};
}
function settle(resolve, reject, d, v) {
Promise.resolve(v).then(function (v) {
resolve({
value: v,
done: d
});
}, reject);
}
};
var __await = void 0 && (void 0).__await || function (v) {
return this instanceof __await ? (this.v = v, this) : new __await(v);
};
var __asyncGenerator = void 0 && (void 0).__asyncGenerator || function (thisArg, _arguments, generator) {
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
var g = generator.apply(thisArg, _arguments || []),
i,
q = [];
return i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () {
return this;
}, i;
function verb(n) {
if (g[n]) i[n] = function (v) {
return new Promise(function (a, b) {
q.push([n, v, a, b]) > 1 || resume(n, v);
});
};
}
function resume(n, v) {
try {
step(g[n](v));
} catch (e) {
settle(q[0][3], e);
}
}
function step(r) {
r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r);
}
function fulfill(value) {
resume("next", value);
}
function reject(value) {
resume("throw", value);
}
function settle(f, v) {
if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]);
}
};
var __rest = void 0 && (void 0).__rest || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]];
}
return t;
};
Object.defineProperty(exports, "__esModule", {
value: true
});
const settings_1 = require("./settings");
const lib_1 = require("./lib");
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;
}
/**
* 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, Object.assign(Object.assign({}, requestOptions), {
method: "POST",
body: JSON.stringify(resource),
headers: Object.assign({
"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}`, Object.assign(Object.assign({}, requestOptions), {
method: "PUT",
body: JSON.stringify(resource),
headers: Object.assign({
"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, Object.assign(Object.assign({}, 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, Object.assign(Object.assign({}, requestOptions), {
method: "PATCH",
body: JSON.stringify(patch),
headers: Object.assign({
"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, Object.assign(Object.assign({}, 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)
*/
resources(bundleOrUrl, options) {
return __asyncGenerator(this, arguments, function* resources_1() {
var e_1, _a;
let count = 0;
try {
for (var _b = __asyncValues(this.pages(bundleOrUrl, options)), _c; _c = yield __await(_b.next()), !_c.done;) {
const page = _c.value;
for (const entry of page.entry || []) {
if ((options === null || options === void 0 ? void 0 : options.limit) && ++count > options.limit) {
return yield __await(void 0);
}
yield yield __await(entry.resource);
}
}
} catch (e_1_1) {
e_1 = {
error: e_1_1
};
} finally {
try {
if (_c && !_c.done && (_a = _b.return)) yield __await(_a.call(_b));
} finally {
if (e_1) throw e_1.error;
}
}
});
}
/**
* 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.
*/
pages(bundleOrUrl, requestOptions) {
var _a, _b;
return __asyncGenerator(this, arguments, function* pages_1() {
const _c = requestOptions || {},
{
limit
} = _c,
options = __rest(_c, ["limit"]);
const fetchPage = url => this.fhirRequest(url, options);
let page = typeof bundleOrUrl === "string" || bundleOrUrl instanceof URL ? yield __await(fetchPage(bundleOrUrl)) : bundleOrUrl;
let count = 0;
while (page && page.resourceType === "Bundle" && (!limit || ++count <= limit)) {
// Yield the current page
yield yield __await(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 = yield __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 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;