Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Util suggestion: a way to parse bundles with multiple related resources #4399

Open
Swizec opened this issue Apr 18, 2024 · 1 comment
Open
Milestone

Comments

@Swizec
Copy link

Swizec commented Apr 18, 2024

Hi,

while building with the Medplum SDK we've developed a few helper methods to help us work with the API. It would be fantastic if some version of these could be added to the official SDK. Making separate issues for more focused discussion :)

We often find ourselves building a nested or merged data structure out of bundles with multiple resources. For example when fetching observations with related practitioners and service requests.

function searchResultToRows(
    observationsBundle: Bundle<Observation | Practitioner | ServiceRequest>
): RowData[] {
    const observations = new Array<Observation>()
    const serviceRequests = new Map<string, ServiceRequest>()
    const practitioners = new Map<string, Practitioner>()

    observationsBundle?.entry?.forEach(({ resource }) => {
        if (resource && resource.resourceType) {
            switch (resource.resourceType) {
                case 'Observation':
                    observations.push(resource)
                    break
                case 'ServiceRequest':
                    serviceRequests.set(
                        `ServiceRequest/${resource.id}`,
                        resource
                    )
                    break
                case 'Practitioner':
                    practitioners.set(`Practitioner/${resource.id}`, resource)
                    break
                default:
                    break
            }
        }
    })

    return observations.map(ob => {
        const serviceRequest = serviceRequests.get(
            ob.basedOn?.[0]?.reference ?? ''
        )
        const practitioner = practitioners.get(
            serviceRequest?.requester?.reference ?? ''
        )
        const abnormalNote = ob.note?.find(note =>
            note?.text?.match(/^isAbnormal=/gi)
        )?.text
        const comment = ob.note?.find(note =>
            note?.text?.match(/^(?!isAbnormal=)/gi)
        )?.text

        // Note: about 9% of lab results don't have this flag set.
        // So there might be out-of-range results out there that do not have this flag set to true.
        const isAbnormal = abnormalNote === 'isAbnormal=true'
        const value = ob.valueQuantity
            ? `${ob.valueQuantity.value} ${ob.valueQuantity.unit ?? ''}`
            : ob.valueString ?? emptyField

        return {
            id: ob.id!,
            status: ob.status ?? emptyField,
            labName: ob.code?.coding?.at(0)?.display ?? emptyField,
            referenceRange: ob.referenceRange?.at(0)?.text ?? emptyField,
            comment: comment ?? emptyField,
            inRangeValue: !isAbnormal ? value : emptyField,
            outOfRangeValue: isAbnormal ? value : emptyField,
            provider: practitioner
                ? getDisplayString(practitioner)
                : emptyField,
            orderDate: serviceRequest?.authoredOn
                ? format(new Date(serviceRequest.authoredOn), 'MM/dd/yyyy')
                : emptyField,
            abnormalStatus:
                abnormalNote === 'isAbnormal=true'
                    ? 'Abnormal'
                    : abnormalNote === 'isAbnormal=false'
                      ? 'Normal'
                      : emptyField
        }
    })
}

There are possibly two utils hiding in here:

  1. Grabbing all resources of a type out of a bundle
  2. Nesting related resources

We've thought about doing this with the GraphQL API instead but that doesn't play as nicely with TypeScript.

@codyebberson
Copy link
Member

Hi @Swizec - thanks for submitting this.

You're right, that FHIR Bundle results, especially when using _include and _revinclude can require some unfortunate work to manipulate.

Get resources by type

A simple getResourcesByType(bundle: Bundle): Map<ResourceType, Resource> would be immediately valuable. That is easy to understand, and quite general purpose.

Get related resources

A getRelatedResources(bundle: Bundle, root: Resource): ??? is interesting. We would need to figure out a general purpose return type. Maybe Map<propertyName, Resource> ?

Or, alternatively, it could "move" the related resources into Reference.resource. This is a somewhat non-standard type which Medplum added for GraphQL support (see more on the GraphQL notes below).

To be clear, it is not part of the official FHIR Reference type: https://hl7.org/fhir/r4/references.html#Reference

But we added it because that is the shape of the FHIR GraphQL response.

We could use that same property in a utility to "get related resources". For example, for this input bundle:

{
  "resourceType": "Bundle",
  "type": "searchset",
  "entry": [
    {
      "resource": {
        "resourceType": "DiagnosticReport",
        "id": "123",
        "result": [{ "reference": "Observation/456" }]
      }
    },
    {
      "resource": {
        "resourceType": "Observation",
        "id": "456",
        "valueQuantity": { "value": 123, "unit": "mg" }
      }
    },
  ]
}

The utility could "move" the resource into Reference.resource like this:

{
  "resourceType": "DiagnosticReport",
  "id": "123",
  "result": [
    {
      "resource": {
        "resourceType": "Observation",
        "id": "456",
        "valueQuantity": { "value": 123, "unit": "mg" }
      }
    }
  ]
}

GraphQL types

You're also right that the FHIR GraphQL API creatively abuses the type schema. We have noticed some interesting patterns.

Consider this example from the Medplum GraphQL docs:

{
  DiagnosticReport(id: "example-id-1") {
    resourceType
    id
    result {
      resource {
        ... on Observation {
          resourceType
          id
          valueQuantity {
            value
            unit
          }
        }
      }
    }
  }
}

With this query, the referenced data comes right inline, because the FHIR Reference type includes an optional resource property, as mentioned above:

  data: {
    DiagnosticReport: {
      resourceType: 'DiagnosticReport',
      id: 'example-id-1',
      result: [
        {
          resource: {
            resourceType: 'Observation',
            id: 'observation-id-1',
            valueQuantity: {
              value: 5.5,
              unit: 'mg/dL',
            },
          },
        },
        {
          resource: {
            resourceType: 'Observation',
            id: 'observation-id-2',
            valueQuantity: {
              value: 3.2,
              unit: 'mg/dL',
            },
          },
        },
      ],
    },
  },

Here is another more complicated example, with embedded per-resource searches:

{
  Patient(id: "example-patient-id") {
    resourceType
    id
    encounters: EncounterList(_reference: patient) {
      resourceType
      id
    }
  }
}

Here, we're including the search results inline:

  data: {
    Patient: {
      resourceType: 'Patient',
      id: 'example-patient-id',
      encounters: [
        {
          resourceType: 'Encounter',
          id: 'encounter-id-1',
        },
        {
          resourceType: 'Encounter',
          id: 'encounter-id-2',
        },
      ],
    },
  },

You can use a TypeScript union to represent this:

import { Encounter, Patient } from '@medplum/fhirtypes';

type MyPatient = Patient & { encounters: Encounter[] };

type QueryResult = {
  data: {
    Patient: MyPatient[];
  };
};

for (const myPatient of result.data.Patient) {
  const { encounters, ...patient } = myPatient;

  console.log('patient', patient);
  console.log('encounters', encounters);
}

@codyebberson codyebberson added this to the May 31st, 2024 milestone Apr 30, 2024
@reshmakh reshmakh modified the milestones: May 31st, 2024, June 30, 2024 Jun 2, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: No status
Development

No branches or pull requests

3 participants