Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 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
22 changes: 0 additions & 22 deletions lib/client.js
Original file line number Diff line number Diff line change
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
164 changes: 153 additions & 11 deletions lib/pagination.js
Original file line number Diff line number Diff line change
@@ -1,41 +1,183 @@
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);
}

/**
* Return the next page of results.
* Initialize pagination functionality based on current bundle results.
*
* @param {object} results a bundle result of a FHIR search
*
* @returns {void}
*/
initialize(results) {
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.

If results is always a Bundle, I think it should be called bundle.

this.currentResults = results;

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

/**
* Detect and save search parameters based on provided or default paramNames.
*
* @param {string} link a link containing search parameters matching indicated paramNames.
*
* @returns {void}
*/
parseUrl(link) {
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.

I think this could have a more descriptive name. Something like extractSearchParams or detectSearchParams?

const linkUrl = new URL(link.url);

Object.keys(this.paramNames).forEach((paramName) => {
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.

This looks like it should use Object.entries

Object.entries(this.paramNames).foreach(([paramName, param]) => ...)

const param = this.paramNames[paramName];
this[param] = linkUrl.searchParams.get(param);
});
}

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

this.currentResults = selectedPage ? this.httpClient.get(selectedPage) : undefined;
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.

This is really just getAndSetCurrent(selectedPage), right?

return this.currentResults;
}

/**
* Return the current page of results.
*
* @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;
currentPage() {
return this.getAndSetCurrent(this.selfLink());
}

/**
* Return the next page of results.
*
* @return {Promise<Object>} FHIR resources in a FHIR Bundle structure.
*/
nextPage() {
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.

If nextPage and prevPage take an optional bundle as an argument, they can call initialize with it. That way this PR won't break the existing interface.

return this.getAndSetCurrent(this.nextLink());
}

/**
* 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() {
return this.getAndSetCurrent(this.prevLink());
}

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

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

/**
* Return the link for the current page of results.
*
* @return {String} link for the current page of results.
*/
selfLink() {
return this.getLink(/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;
getAndSetCurrent(link) {
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.

I'd rename this setCurrent. I think it's pretty normal for a setter to return the value that was set, and the fact that it does so doesn't need to be called out in its name. Maybe we could look into using getters and setters or something and come up with a solution that doesn't need set in the name either.

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.
*/
getLink(regex) {
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.

Maybe findLink?

return this.currentResults.link.find(link => link.relation.match(regex));
}
}

Expand Down
Loading