diff --git a/examples/smart-ehr/yarn.lock b/examples/smart-ehr/yarn.lock index d2f4808a..fa6ee7fc 100644 --- a/examples/smart-ehr/yarn.lock +++ b/examples/smart-ehr/yarn.lock @@ -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" @@ -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" @@ -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" @@ -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" @@ -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" @@ -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" @@ -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" @@ -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" @@ -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" @@ -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" diff --git a/lib/client.js b/lib/client.js index 0008e813..9b4a8801 100644 --- a/lib/client.js +++ b/lib/client.js @@ -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); } /** @@ -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} 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} 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 * diff --git a/lib/pagination.js b/lib/pagination.js index 153ad3f2..f4d42317 100644 --- a/lib/pagination.js +++ b/lib/pagination.js @@ -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. + * @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 = {}) { this.httpClient = httpClient; + this.baseUrl = httpClient.axiosInstance.defaults.baseURL; + this.currentResults = null; + + const defaultParamNames = { + searchIdParam: '_getpages', + 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} 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} 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} 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} 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} 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)); } } diff --git a/test/client-test.js b/test/client-test.js index b7b4c5bb..1f7d389e 100644 --- a/test/client-test.js +++ b/test/client-test.js @@ -387,24 +387,74 @@ describe('Client', function () { }); describe('pagination', function () { + describe('#goToPage', function () { + it('returns httpClient get for a specified page number', async function () { + nock(this.baseUrl) + .matchHeader('accept', 'application/json+fhir') + .get('/?_getpages=678cd733-8823-4324-88a7-51d369cf78a9&_getpagesoffset=3&_count=3') + .reply(200, () => readStreamFor('search-results-page-2.json')); + + const searchResults = readFixture('search-results-page-1.json'); + + this.fhirClient.pagination.initialize(searchResults); + + const response = await this.fhirClient.pagination.goToPage(2); + const url = 'https://example.com/?_getpages=678cd733-8823-4324-88a7-51d369cf78a9&_getpagesoffset=3&_count=3'; + + expect(response.link[0].url).to.equal(url); + }); + + it('initializes pagination if a bundle is provided and returns httpClient get for a specified page number', async function () { + nock(this.baseUrl) + .matchHeader('accept', 'application/json+fhir') + .get('/?_getpages=678cd733-8823-4324-88a7-51d369cf78a9&_getpagesoffset=3&_count=3') + .reply(200, () => readStreamFor('search-results-page-2.json')); + + const searchResults = readFixture('search-results-page-1.json'); + + const response = await this.fhirClient.pagination.goToPage(2, searchResults); + const url = 'https://example.com/?_getpages=678cd733-8823-4324-88a7-51d369cf78a9&_getpagesoffset=3&_count=3'; + + expect(response.link[0].url).to.equal(url); + }); + }); describe('#nextPage', function () { it('returns httpClient get for the next link', async function () { nock(this.baseUrl) .matchHeader('accept', 'application/json+fhir') - .get('/?_getpages=678cd733-8823-4324-88a7-51d369cf78a9&_getpagesoffset=3&_count=3&_pretty=true&_bundletype=searchset') + .get('/?_getpages=678cd733-8823-4324-88a7-51d369cf78a9&_getpagesoffset=3&_count=3') .reply(200, () => readStreamFor('search-results-page-2.json')); const searchResults = readFixture('search-results-page-1.json'); - const response = await this.fhirClient.nextPage(searchResults); - const url = 'https://example.com/?_getpages=678cd733-8823-4324-88a7-51d369cf78a9&_getpagesoffset=3&_count=3&_pretty=true&_bundletype=searchset'; + + this.fhirClient.pagination.initialize(searchResults); + + const response = await this.fhirClient.pagination.nextPage(); + const url = 'https://example.com/?_getpages=678cd733-8823-4324-88a7-51d369cf78a9&_getpagesoffset=3&_count=3'; + + expect(response.link[0].url).to.equal(url); + }); + + it('initializes pagination if a bundle is provided and returns httpClient get for the next link', async function () { + nock(this.baseUrl) + .matchHeader('accept', 'application/json+fhir') + .get('/?_getpages=678cd733-8823-4324-88a7-51d369cf78a9&_getpagesoffset=3&_count=3') + .reply(200, () => readStreamFor('search-results-page-2.json')); + + const searchResults = readFixture('search-results-page-1.json'); + + const response = await this.fhirClient.pagination.nextPage(searchResults); + const url = 'https://example.com/?_getpages=678cd733-8823-4324-88a7-51d369cf78a9&_getpagesoffset=3&_count=3'; expect(response.link[0].url).to.equal(url); }); it('returns undefined if no next page exists', function () { - const results = readFixture('search-results.json'); + const searchResults = readFixture('search-results.json'); + + this.fhirClient.pagination.initialize(searchResults); - expect(this.fhirClient.nextPage(results)).to.equal(undefined); + expect(this.fhirClient.pagination.nextPage()).to.equal(undefined); }); }); @@ -412,30 +462,53 @@ describe('Client', function () { it('returns httpClient get for the previous link', async function () { nock(this.baseUrl) .matchHeader('accept', 'application/json+fhir') - .get('/?_getpages=678cd733-8823-4324-88a7-51d369cf78a9&_getpagesoffset=0&_count=3&_pretty=true&_bundletype=searchset') + .get('/?_getpages=678cd733-8823-4324-88a7-51d369cf78a9&_getpagesoffset=0&_count=3') .reply(200, () => readStreamFor('search-results-page-1.json')); const searchResults = readFixture('search-results-page-2.json'); - const response = await this.fhirClient.prevPage(searchResults); + + this.fhirClient.pagination.initialize(searchResults); + + const response = await this.fhirClient.pagination.prevPage(); + const url = 'https://example.com/Patient?_count=3&gender=female'; + + expect(response.link[0].url).to.equal(url); + }); + + it('initializes pagination if a bundle is provided and returns httpClient get for the previous link', async function () { + nock(this.baseUrl) + .matchHeader('accept', 'application/json+fhir') + .get('/?_getpages=678cd733-8823-4324-88a7-51d369cf78a9&_getpagesoffset=0&_count=3') + .reply(200, () => readStreamFor('search-results-page-1.json')); + + const searchResults = readFixture('search-results-page-2.json'); + + const response = await this.fhirClient.pagination.prevPage(searchResults); const url = 'https://example.com/Patient?_count=3&gender=female'; expect(response.link[0].url).to.equal(url); }); it('returns undefined if no previous page exists', function () { - const results = readFixture('search-results.json'); - expect(this.fhirClient.prevPage(results)).to.equal(undefined); + const searchResults = readFixture('search-results.json'); + + this.fhirClient.pagination.initialize(searchResults); + + expect(this.fhirClient.pagination.prevPage()).to.equal(undefined); }); it('detects and responds to "prev" relations', async function () { nock(this.baseUrl) .matchHeader('accept', 'application/json+fhir') - .get('/?_getpages=678cd733-8823-4324-88a7-51d369cf78a9&_getpagesoffset=0&_count=3&_pretty=true&_bundletype=searchset') + .get('/?_getpages=678cd733-8823-4324-88a7-51d369cf78a9&_getpagesoffset=0&_count=3') .reply(200, () => readStreamFor('search-results-page-1.json')); const searchResults = readFixture('search-results-page-2.json'); searchResults.link[2].relation = 'prev'; - const response = await this.fhirClient.prevPage(searchResults); + + this.fhirClient.pagination.initialize(searchResults); + + const response = await this.fhirClient.pagination.prevPage(); const url = 'https://example.com/Patient?_count=3&gender=female'; expect(response.link[0].url).to.equal(url); @@ -839,3 +912,19 @@ describe('Client', function () { }); }); }); + +describe('Client with custom pagination parameters', function () { + it('merges custom search parameter names with defaults', function () { + const baseUrl = 'https://example.com'; + const paginationParams = { searchIdParam: 'my-search-id', offsetParam: '_offset' }; + const config = { baseUrl, paginationParams }; + + this.fhirClient = new Client(config); + + expect(this.fhirClient.pagination.paramNames).to.deep.equal({ + searchIdParam: 'my-search-id', + offsetParam: '_offset', + countParam: '_count', + }); + }); +}); diff --git a/test/fixtures/search-results-page-1.json b/test/fixtures/search-results-page-1.json index e800441c..6bde823c 100644 --- a/test/fixtures/search-results-page-1.json +++ b/test/fixtures/search-results-page-1.json @@ -13,7 +13,7 @@ }, { "relation": "next", - "url": "https://example.com/?_getpages=678cd733-8823-4324-88a7-51d369cf78a9&_getpagesoffset=3&_count=3&_pretty=true&_bundletype=searchset" + "url": "https://example.com/?_getpages=678cd733-8823-4324-88a7-51d369cf78a9&_getpagesoffset=3&_count=3" } ], "entry": [ diff --git a/test/fixtures/search-results-page-2.json b/test/fixtures/search-results-page-2.json index 481ed423..8991f444 100644 --- a/test/fixtures/search-results-page-2.json +++ b/test/fixtures/search-results-page-2.json @@ -9,15 +9,15 @@ "link": [ { "relation": "self", - "url": "https://example.com/?_getpages=678cd733-8823-4324-88a7-51d369cf78a9&_getpagesoffset=3&_count=3&_pretty=true&_bundletype=searchset" + "url": "https://example.com/?_getpages=678cd733-8823-4324-88a7-51d369cf78a9&_getpagesoffset=3&_count=3" }, { "relation": "next", - "url": "https://example.com/?_getpages=678cd733-8823-4324-88a7-51d369cf78a9&_getpagesoffset=6&_count=3&_pretty=true&_bundletype=searchset" + "url": "https://example.com/?_getpages=678cd733-8823-4324-88a7-51d369cf78a9&_getpagesoffset=6&_count=3" }, { "relation": "previous", - "url": "https://example.com/?_getpages=678cd733-8823-4324-88a7-51d369cf78a9&_getpagesoffset=0&_count=3&_pretty=true&_bundletype=searchset" + "url": "https://example.com/?_getpages=678cd733-8823-4324-88a7-51d369cf78a9&_getpagesoffset=0&_count=3" } ], "entry": [ diff --git a/test/pagination-test.js b/test/pagination-test.js new file mode 100644 index 00000000..555dc1cf --- /dev/null +++ b/test/pagination-test.js @@ -0,0 +1,238 @@ +/* eslint-disable func-names, no-unused-expressions */ +const fs = require('fs'); +const path = require('path'); + +const { expect } = require('chai'); +const nock = require('nock'); + +const HttpClient = require('../lib/http-client'); +const Pagination = require('../lib/pagination'); + +/** + * Read fixture data + * + * @param {String} fixture - The fixture file + * + * @returns {String} - The data from a fixture + */ +function readStreamFor(fixture) { + return fs.createReadStream(path.normalize(`${__dirname}/fixtures/${fixture}`, 'utf8')); +} + +function readFixture(filename) { + return JSON.parse(fs.readFileSync(path.normalize(`${__dirname}/fixtures/${filename}`, 'utf8'))); +} + +describe('Pagination', function () { + beforeEach(function () { + this.baseUrl = 'https://example.com'; + this.httpClient = new HttpClient({ baseUrl: this.baseUrl }); + this.pagination = new Pagination(this.httpClient); + }); + + describe('#initialize', function () { + it('sets the current bundle', async function () { + expect(this.pagination.currentResults).to.be.null; + + const searchResults = readFixture('search-results-page-1.json'); + + this.pagination.initialize(searchResults); + + expect(this.pagination.currentResults).to.equal(searchResults); + }); + + it('uses an available link to extract search parameters', async function () { + const expectedParameters = { + _getpages: '678cd733-8823-4324-88a7-51d369cf78a9', + _getpagesoffset: '3', + _count: '3', + }; + + const searchResults = readFixture('search-results-page-1.json'); + + this.pagination.initialize(searchResults); + + Object.entries(expectedParameters).forEach(([paramName, param]) => { + expect(this.pagination[paramName]).to.equal(param); + }); + }); + }); + + describe('#goToPage', function () { + it('returns httpClient get for a specified page number', async function () { + nock(this.baseUrl) + .matchHeader('accept', 'application/json+fhir') + .get('/?_getpages=678cd733-8823-4324-88a7-51d369cf78a9&_getpagesoffset=3&_count=3') + .reply(200, () => readStreamFor('search-results-page-2.json')); + + const searchResults = readFixture('search-results-page-1.json'); + + this.pagination.initialize(searchResults); + + const response = await this.pagination.goToPage(2); + const url = 'https://example.com/?_getpages=678cd733-8823-4324-88a7-51d369cf78a9&_getpagesoffset=3&_count=3'; + + expect(response.link[0].url).to.equal(url); + }); + + it('initializes pagination if a bundle is provided and returns httpClient get for a specified page number', async function () { + nock(this.baseUrl) + .matchHeader('accept', 'application/json+fhir') + .get('/?_getpages=678cd733-8823-4324-88a7-51d369cf78a9&_getpagesoffset=3&_count=3') + .reply(200, () => readStreamFor('search-results-page-2.json')); + + const searchResults = readFixture('search-results-page-1.json'); + + const response = await this.pagination.goToPage(2, searchResults); + const url = 'https://example.com/?_getpages=678cd733-8823-4324-88a7-51d369cf78a9&_getpagesoffset=3&_count=3'; + + expect(response.link[0].url).to.equal(url); + }); + }); + + describe('#currentPage', function () { + it('returns httpClient get for the current link', async function () { + nock(this.baseUrl) + .matchHeader('accept', 'application/json+fhir') + .get('/Patient?_count=3&gender=female') + .reply(200, () => readStreamFor('search-results-page-1.json')); + + const searchResults = readFixture('search-results-page-1.json'); + + this.pagination.initialize(searchResults); + + const response = await this.pagination.currentPage(); + const url = 'https://example.com/Patient?_count=3&gender=female'; + + expect(response.link[0].url).to.equal(url); + }); + + it('initializes pagination if a bundle is provided and returns httpClient get for the current link', async function () { + nock(this.baseUrl) + .matchHeader('accept', 'application/json+fhir') + .get('/Patient?_count=3&gender=female') + .reply(200, () => readStreamFor('search-results-page-1.json')); + + const searchResults = readFixture('search-results-page-1.json'); + + const response = await this.pagination.currentPage(searchResults); + const url = 'https://example.com/Patient?_count=3&gender=female'; + + expect(response.link[0].url).to.equal(url); + }); + }); + + describe('#nextPage', function () { + it('returns httpClient get for the next link', async function () { + nock(this.baseUrl) + .matchHeader('accept', 'application/json+fhir') + .get('/?_getpages=678cd733-8823-4324-88a7-51d369cf78a9&_getpagesoffset=3&_count=3') + .reply(200, () => readStreamFor('search-results-page-2.json')); + + const searchResults = readFixture('search-results-page-1.json'); + + this.pagination.initialize(searchResults); + + const response = await this.pagination.nextPage(); + const url = 'https://example.com/?_getpages=678cd733-8823-4324-88a7-51d369cf78a9&_getpagesoffset=3&_count=3'; + + expect(response.link[0].url).to.equal(url); + }); + + it('initializes pagination if a bundle is provided and returns httpClient get for the next link', async function () { + nock(this.baseUrl) + .matchHeader('accept', 'application/json+fhir') + .get('/?_getpages=678cd733-8823-4324-88a7-51d369cf78a9&_getpagesoffset=3&_count=3') + .reply(200, () => readStreamFor('search-results-page-2.json')); + + const searchResults = readFixture('search-results-page-1.json'); + + const response = await this.pagination.nextPage(searchResults); + const url = 'https://example.com/?_getpages=678cd733-8823-4324-88a7-51d369cf78a9&_getpagesoffset=3&_count=3'; + + expect(response.link[0].url).to.equal(url); + }); + + it('returns undefined if no next page exists', function () { + const searchResults = readFixture('search-results.json'); + + this.pagination.initialize(searchResults); + + expect(this.pagination.nextPage()).to.equal(undefined); + }); + }); + + describe('#prevPage', function () { + it('returns httpClient get for the previous link', async function () { + nock(this.baseUrl) + .matchHeader('accept', 'application/json+fhir') + .get('/?_getpages=678cd733-8823-4324-88a7-51d369cf78a9&_getpagesoffset=0&_count=3') + .reply(200, () => readStreamFor('search-results-page-1.json')); + + const searchResults = readFixture('search-results-page-2.json'); + + this.pagination.initialize(searchResults); + + const response = await this.pagination.prevPage(); + const url = 'https://example.com/Patient?_count=3&gender=female'; + + expect(response.link[0].url).to.equal(url); + }); + + it('initializes pagination if a bundle is provided and returns httpClient get for the previous link', async function () { + nock(this.baseUrl) + .matchHeader('accept', 'application/json+fhir') + .get('/?_getpages=678cd733-8823-4324-88a7-51d369cf78a9&_getpagesoffset=0&_count=3') + .reply(200, () => readStreamFor('search-results-page-1.json')); + + const searchResults = readFixture('search-results-page-2.json'); + + const response = await this.pagination.prevPage(searchResults); + const url = 'https://example.com/Patient?_count=3&gender=female'; + + expect(response.link[0].url).to.equal(url); + }); + + it('returns undefined if no previous page exists', function () { + const searchResults = readFixture('search-results.json'); + + this.pagination.initialize(searchResults); + + expect(this.pagination.prevPage()).to.equal(undefined); + }); + + it('detects and responds to "prev" relations', async function () { + nock(this.baseUrl) + .matchHeader('accept', 'application/json+fhir') + .get('/?_getpages=678cd733-8823-4324-88a7-51d369cf78a9&_getpagesoffset=0&_count=3') + .reply(200, () => readStreamFor('search-results-page-1.json')); + + const searchResults = readFixture('search-results-page-2.json'); + searchResults.link[2].relation = 'prev'; + + this.pagination.initialize(searchResults); + + const response = await this.pagination.prevPage(); + const url = 'https://example.com/Patient?_count=3&gender=female'; + + expect(response.link[0].url).to.equal(url); + }); + }); +}); + +describe('Pagination with custom parameters', function () { + it('merges custom search parameter names with defaults', function () { + this.baseUrl = 'https://example.com'; + this.httpClient = new HttpClient({ baseUrl: this.baseUrl }); + this.pagination = new Pagination(this.httpClient, { + searchIdParam: 'my-search-id', + offsetParam: '_offset', + }); + + expect(this.pagination.paramNames).to.deep.equal({ + searchIdParam: 'my-search-id', + offsetParam: '_offset', + countParam: '_count', + }); + }); +});