From 66e158c7b6d5077d8b86f0bd825968c3d3ed828a Mon Sep 17 00:00:00 2001 From: Cody Ebberson Date: Tue, 2 Apr 2024 15:15:08 -0700 Subject: [PATCH] Fixes #3944 - patient everything pagination (#4296) --- .../dist/fhir/r4/profiles-resources.json | 8 ++ .../fhir/operations/patienteverything.test.ts | 16 +++- .../src/fhir/operations/patienteverything.ts | 93 +++++-------------- 3 files changed, 47 insertions(+), 70 deletions(-) diff --git a/packages/definitions/dist/fhir/r4/profiles-resources.json b/packages/definitions/dist/fhir/r4/profiles-resources.json index b262f31f27..e6b71a8387 100644 --- a/packages/definitions/dist/fhir/r4/profiles-resources.json +++ b/packages/definitions/dist/fhir/r4/profiles-resources.json @@ -24099,6 +24099,14 @@ "documentation" : "See discussion below on the utility of paging through the results of the $everything operation", "type" : "integer" }, + { + "name" : "_offset", + "use" : "in", + "min" : 0, + "max" : "1", + "documentation" : "See discussion below on the utility of paging through the results of the $everything operation", + "type" : "integer" + }, { "name" : "return", "use" : "out", diff --git a/packages/server/src/fhir/operations/patienteverything.test.ts b/packages/server/src/fhir/operations/patienteverything.test.ts index 1f5de73474..5b10409338 100644 --- a/packages/server/src/fhir/operations/patienteverything.test.ts +++ b/packages/server/src/fhir/operations/patienteverything.test.ts @@ -1,5 +1,5 @@ import { ContentType, LOINC, createReference } from '@medplum/core'; -import { Patient } from '@medplum/fhirtypes'; +import { Bundle, Patient } from '@medplum/fhirtypes'; import express from 'express'; import request from 'supertest'; import { initApp, shutdownApp } from '../../app'; @@ -91,5 +91,19 @@ describe('Patient Everything Operation', () => { .set('Authorization', 'Bearer ' + accessToken); expect(res6.status).toBe(200); expect(res6.body.entry).toHaveLength(1); + + // Execute the operation with _count and _offset + const res7 = await request(app) + .get(`/fhir/R4/Patient/${res1.body.id}/$everything?_count=1&_offset=1`) + .set('Authorization', 'Bearer ' + accessToken); + expect(res7.status).toBe(200); + + // Bundle should have pagination links + const bundle = res7.body as Bundle; + expect(bundle.entry).toHaveLength(1); + expect(bundle.link).toBeDefined(); + expect(bundle.link?.some((link) => link.relation === 'next')).toBeTruthy(); + expect(bundle.link?.some((link) => link.relation === 'first')).toBeTruthy(); + expect(bundle.link?.some((link) => link.relation === 'previous')).toBeTruthy(); }); }); diff --git a/packages/server/src/fhir/operations/patienteverything.ts b/packages/server/src/fhir/operations/patienteverything.ts index 5220c95a7c..305a5110d4 100644 --- a/packages/server/src/fhir/operations/patienteverything.ts +++ b/packages/server/src/fhir/operations/patienteverything.ts @@ -1,24 +1,20 @@ -import { allOk, getReferenceString, Operator, SearchRequest } from '@medplum/core'; +import { allOk, getReferenceString, Operator, sortStringArray } from '@medplum/core'; import { FhirRequest, FhirResponse } from '@medplum/fhir-router'; -import { - Bundle, - BundleEntry, - CompartmentDefinitionResource, - Patient, - Resource, - ResourceType, -} from '@medplum/fhirtypes'; +import { Bundle, CompartmentDefinitionResource, Patient, ResourceType } from '@medplum/fhirtypes'; import { getAuthenticatedContext } from '../../context'; import { getPatientCompartments } from '../patient'; import { Repository } from '../repo'; -import { getFullUrl } from '../response'; import { getOperationDefinition } from './definitions'; import { parseInputParameters } from './utils/parameters'; const operation = getOperationDefinition('Patient', 'everything'); +const defaultMaxResults = 1000; + type PatientEverythingParameters = { _since?: string; + _count?: number; + _offset?: number; }; // Patient everything operation. @@ -57,12 +53,19 @@ export async function getPatientEverything( patient: Patient, params?: PatientEverythingParameters ): Promise { - const patientRef = getReferenceString(patient); const resourceList = getPatientCompartments().resource as CompartmentDefinitionResource[]; - const searches: SearchRequest[] = []; + const types = resourceList.map((r) => r.code as ResourceType).filter((t) => t !== 'Binary'); + types.push('Patient'); + sortStringArray(types); + + const filters = [ + { + code: '_compartment', + operator: Operator.EQUALS, + value: getReferenceString(patient), + }, + ]; - // Build a list of filters to apply to the searches - const filters = []; if (params?._since) { filters.push({ code: '_lastUpdated', @@ -71,59 +74,11 @@ export async function getPatientEverything( }); } - // Build a list of searches - for (const resource of resourceList) { - const searchParams = resource.param; - if (!searchParams) { - continue; - } - for (const code of searchParams) { - searches.push({ - resourceType: resource.code as ResourceType, - count: 1000, - filters: [ - { - code, - operator: Operator.EQUALS, - value: patientRef, - }, - ...filters, - ], - }); - } - } - - // Execute all of the searches in parallel - // Some day we could do this in a single SQL query - const promises = searches.map((searchRequest) => repo.search(searchRequest)); - const searchResults = await Promise.all(promises); - - // Build the result bundle - const entry: BundleEntry[] = []; - - if (!params?._since || (patient.meta?.lastUpdated as string) >= params?._since) { - entry.push({ - fullUrl: getFullUrl('Patient', patient.id as string), - resource: patient, - }); - } - - const resourceSet = new Set([getReferenceString(patient)]); - for (const searchResult of searchResults) { - if (searchResult.entry) { - for (const e of searchResult.entry) { - const resourceRef = getReferenceString(e.resource as Resource); - if (!resourceSet.has(resourceRef)) { - resourceSet.add(resourceRef); - entry.push(e); - } - } - } - } - - return { - resourceType: 'Bundle', - type: 'searchset', - entry, - }; + return repo.search({ + resourceType: 'Patient', + types, + filters, + count: params?._count ?? defaultMaxResults, + offset: params?._offset, + }); }