Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 16 additions & 47 deletions examples/smart-ehr/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ aws-sign2@~0.7.0:
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"

aws4@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"
version "1.7.0"
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.7.0.tgz#d4d0e9b9dbfca77bf08eeb0a8a471550fe39e289"

bcrypt-pbkdf@^1.0.0:
version "1.0.1"
Expand All @@ -67,18 +67,6 @@ body-parser@1.18.2:
raw-body "2.3.2"
type-is "~1.6.15"

boom@4.x.x:
version "4.3.1"
resolved "https://registry.yarnpkg.com/boom/-/boom-4.3.1.tgz#4f8a3005cb4a7e3889f749030fd25b96e01d2e31"
dependencies:
hoek "4.x.x"

boom@5.x.x:
version "5.2.0"
resolved "https://registry.yarnpkg.com/boom/-/boom-5.2.0.tgz#5dd9da6ee3a5f302077436290cb717d3f4a54e02"
dependencies:
hoek "4.x.x"

bytes@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
Expand Down Expand Up @@ -121,12 +109,6 @@ crc@3.4.4:
version "3.4.4"
resolved "https://registry.yarnpkg.com/crc/-/crc-3.4.4.tgz#9da1e980e3bd44fc5c93bf5ab3da3378d85e466b"

cryptiles@3.x.x:
version "3.1.2"
resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.2.tgz#a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe"
dependencies:
boom "5.x.x"

dashdash@^1.12.0:
version "1.14.1"
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
Expand Down Expand Up @@ -305,15 +287,6 @@ har-validator@~5.0.3:
ajv "^5.1.0"
har-schema "^2.0.0"

hawk@~6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/hawk/-/hawk-6.0.2.tgz#af4d914eb065f9b5ce4d9d11c1cb2126eecc3038"
dependencies:
boom "4.x.x"
cryptiles "3.x.x"
hoek "4.x.x"
sntp "2.x.x"

hoek@4.x.x:
version "4.2.1"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb"
Expand Down Expand Up @@ -471,17 +444,21 @@ proxy-addr@~2.0.3:
ipaddr.js "1.6.0"

punycode@2.x.x:
version "2.1.0"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.0.tgz#5f863edc89b96db09074bad7947bf09056ca4e7d"
version "2.1.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"

punycode@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"

qs@6.5.1, qs@~6.5.1:
qs@6.5.1:
version "6.5.1"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"

qs@~6.5.1:
version "6.5.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"

random-bytes@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b"
Expand All @@ -500,8 +477,8 @@ raw-body@2.3.2:
unpipe "1.0.0"

request@^2.81.0:
version "2.85.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.85.0.tgz#5a03615a47c61420b3eb99b7dba204f83603e1fa"
version "2.87.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.87.0.tgz#32f00235cd08d482b4d0d68db93a829c0ed5756e"
dependencies:
aws-sign2 "~0.7.0"
aws4 "^1.6.0"
Expand All @@ -511,7 +488,6 @@ request@^2.81.0:
forever-agent "~0.6.1"
form-data "~2.3.1"
har-validator "~5.0.3"
hawk "~6.0.2"
http-signature "~1.2.0"
is-typedarray "~1.0.0"
isstream "~0.1.2"
Expand All @@ -521,15 +497,18 @@ request@^2.81.0:
performance-now "^2.1.0"
qs "~6.5.1"
safe-buffer "^5.1.1"
stringstream "~0.0.5"
tough-cookie "~2.3.3"
tunnel-agent "^0.6.0"
uuid "^3.1.0"

safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.1:
safe-buffer@5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"

safe-buffer@^5.0.1, safe-buffer@^5.1.1:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"

send@0.16.2:
version "0.16.2"
resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1"
Expand Down Expand Up @@ -575,12 +554,6 @@ simple-oauth2@1.5.2:
joi "^12.0.0"
request "^2.81.0"

sntp@2.x.x:
version "2.1.0"
resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.1.0.tgz#2c6cec14fedc2222739caf9b5c3d85d1cc5a2cc8"
dependencies:
hoek "4.x.x"

sshpk@^1.7.0:
version "1.14.1"
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.14.1.tgz#130f5975eddad963f1d56f92b9ac6c51fa9f83eb"
Expand All @@ -603,10 +576,6 @@ statuses@~1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087"

stringstream@~0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"

topo@2.x.x:
version "2.0.2"
resolved "https://registry.yarnpkg.com/topo/-/topo-2.0.2.tgz#cd5615752539057c0dc0491a621c3bc6fbe1d182"
Expand Down
26 changes: 2 additions & 24 deletions lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ class Client {
* @param {Object} config Client configuration
* @param {String} config.baseUrl ISS for FHIR server
*/
constructor({ baseUrl } = {}) {
constructor({ baseUrl, paginationParams } = {}) {
this.httpClient = new HttpClient({ baseUrl });
this.baseUrl = baseUrl;
this.resolver = new ReferenceResolver(this);
this.pagination = new Pagination(this.httpClient);
this.pagination = new Pagination(this.httpClient, paginationParams);
}

/**
Expand Down Expand Up @@ -443,28 +443,6 @@ class Client {
return this.httpClient.post('/', body);
}

/**
* Return the next page of results.
*
* @param {object} results a bundle result of a FHIR search
*
* @return {Promise<Object>} FHIR resources in a FHIR Bundle structure.
*/
nextPage(results) {
return this.pagination.nextPage(results);
}

/**
* Return the previous page of results.
*
* @param {object} results a bundle result of a FHIR search
*
* @return {Promise<Object>} FHIR resources in a FHIR Bundle structure.
*/
prevPage(results) {
return this.pagination.prevPage(results);
}

/**
* Search for a FHIR resource, with or without compartments, or the entire system
*
Expand Down
174 changes: 163 additions & 11 deletions lib/pagination.js
Original file line number Diff line number Diff line change
@@ -1,41 +1,193 @@
const { URL } = require('url');

/**
* Class for paging Bundles.
* @private
*
*/
class Pagination {
/**
* Add pagination functionality for the provided client.
*
* @private
* FHIR-conforming servers that support pagination will, at minimum,
* allow for the use of `currentPage()`, `prevPage()`, and `nextPage()`.
* (see https://www.hl7.org/fhir/http.html#paging)
*
* Some servers will also have page offset and/or unique search IDs,
* which enable `goToPage()`. The parameter names can optionally be renamed
* here if needed for variations in server parameter names.
*
* @param {HttpClient} httpClient a configured instance of http client.
* @param {Object} paramNames pagination-related search parameter names, optional.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we discuss some alternative names for paramNames? Probably easier to do face to face.

* @param {Object} [paramNames.searchIdParam] server-based unique ID for search session, optional.
* @param {Object} [paramNames.offsetParam] server-based page offset for search, optional.
* @param {Object} [paramNames.countParam] server-based result count per page, optional.
*/
constructor(httpClient) {
constructor(httpClient, paramNames = {}) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The client constructor doesn't provide a way to pass paramNames to this constructor.

this.httpClient = httpClient;
this.baseUrl = httpClient.axiosInstance.defaults.baseURL;
this.currentResults = null;

const defaultParamNames = {
searchIdParam: '_getpages',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain what this param actually is?
I see:

offsetParam: '_getpageoffset',
countParam: '_count'

and those make obvious sense, but when I look at searchIdParam: '_getpages' I don't really know what's going on.

offsetParam: '_getpagesoffset',
countParam: '_count',
};

this.paramNames = Object.assign({}, defaultParamNames, paramNames);
}

/**
* Initialize pagination functionality based on current bundle results.
*
* @param {object} bundle a results bundle of a FHIR search.
*
* @returns {void}
*/
initialize(bundle) {
this.currentResults = bundle;

const availableLink = this.nextLink() || this.prevLink() || this.selfLink();
if (availableLink) { this.extractSearchParams(availableLink); }
}

/**
* Extract each type of param (searchIdParam, offsetParam, & countParam) from link.
*
* @param {string} link a link containing search parameters matching indicated paramNames.
*
* @returns {void}
*/
extractSearchParams(link) {
const linkUrl = new URL(link.url);

Object.values(this.paramNames).forEach((paramName) => {
this[paramName] = linkUrl.searchParams.get(paramName);
});
}

/**
* Return the specified page of results based on available search parameters.
*
* @param {number} page the page number to navigate to.
* @param {object} [bundle] a results bundle of a FHIR search, optional.
*
* @return {Promise<Object>} FHIR resources in a FHIR Bundle structure.
*/
goToPage(page, bundle) {
if (bundle) { this.initialize(bundle); }
const selectedPage = this.goToPageLink(page);

this.currentResults = selectedPage ? this.httpClient.get(selectedPage) : undefined;
return this.currentResults;
}

/**
* Return the current page of results.
*
* @param {object} [bundle] a results bundle of a FHIR search, optional.
*
* @return {Promise<Object>} FHIR resources in a FHIR Bundle structure.
*/
currentPage(bundle) {
if (bundle) { this.initialize(bundle); }
return this.updateCurrent(this.selfLink());
}

/**
* Return the next page of results.
*
* @param {object} results a bundle result of a FHIR search
* @param {object} [bundle] a results bundle of a FHIR search, optional.
*
* @return {Promise<Object>} FHIR resources in a FHIR Bundle structure.
*/
nextPage(results) {
const nextLink = results.link.find(link => link.relation === 'next');
return nextLink ? this.httpClient.get(nextLink.url) : undefined;
nextPage(bundle) {
if (bundle) { this.initialize(bundle); }
return this.updateCurrent(this.nextLink());
}

/**
* Return the previous page of results.
*
* @param {object} results a bundle result of a FHIR search
* @param {object} [bundle] a results bundle of a FHIR search, optional.
*
* @return {Promise<Object>} FHIR resources in a FHIR Bundle structure.
*/
prevPage(bundle) {
if (bundle) { this.initialize(bundle); }
return this.updateCurrent(this.prevLink());
}

/**
* Return the link for the next page of results.
*
* @return {String} link for the next page of results.
*/
nextLink() {
return this.findLink(/next/);
}

/**
* Return the link for the previous page of results.
*
* @return {String} link for the previous page of results.
*/
prevLink() {
return this.findLink(/^prev(ious)?$/);
}

/**
* Return the link for the current page of results.
*
* @return {String} link for the current page of results.
*/
selfLink() {
return this.findLink(/self/);
}

/**
* Return a link for the specified page of results based on available search parameters.
*
* @param {number} page the page number to navigate to.
*
* @return {String} link for the specified page of results.
*/
goToPageLink(page) {
const { countParam, offsetParam } = this.paramNames;

this[offsetParam] = (page * this[countParam]) - this[countParam];
const paramNames = Object.keys(this.paramNames);

const pageLinkUrl = new URL(`${this.baseUrl}`);

paramNames.forEach((paramName) => {
const param = this.paramNames[paramName];
pageLinkUrl.searchParams.append(param, this[param]);
});

return pageLinkUrl.href;
}

/**
* Return results and set them to the current results.
*
* @param {String} link a link referring to a page of search results.
*
* @return {Promise<Object>} FHIR resources in a FHIR Bundle structure.
*/
prevPage(results) {
const prevLink = results.link.find(link => link.relation.match(/^prev(ious)?$/));
return prevLink ? this.httpClient.get(prevLink.url) : undefined;
updateCurrent(link) {
this.currentResults = link ? this.httpClient.get(link.url) : undefined;
return this.currentResults;
}

/**
* Return a link from the current results bundle.
*
* @param {String} regex to match specific link text.
*
* @return {String} link to specified page based on regex.
*/
findLink(regex) {
return this.currentResults.link.find(link => link.relation.match(regex));
}
}

Expand Down
Loading