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

Feature/convert #910

Merged
merged 4 commits into from
Jul 10, 2024
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,12 @@ export class EhrProxyAppStack extends cdk.Stack {
listener.addAction('EhrProxyExtractAction', {
action: ListenerAction.forward([extractTargetGroup]),
priority: 1,
conditions: [ListenerCondition.pathPatterns(['/fhir/QuestionnaireResponse/$extract'])]
conditions: [
ListenerCondition.pathPatterns([
'/fhir/QuestionnaireResponse/$extract',
'/fhir/StructureMap/$convert'
])
]
});

// Create a target for the transform service
Expand Down
196 changes: 196 additions & 0 deletions documentation/docs/operations/extract.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
---
sidebar_position: 4
---

# StructureMap $extract

This $extract **proof-of-concept** reference implementation is an abstraction on top of an existing StructureMap $transform operation.
We leveraged Brian Postlethwaite's [.NET FHIR Mapping Language engine](https://github.com/brianpos/fhir-net-mappinglanguage/tree/main/demo-map-server) to expose a StructureMap $transform operation on https://proxy.smartforms.io/fhir/StructureMap/$transform.

:::note

This reference implementation is a proof-of-concept. It is highly likely that the underlying implementation will change in the future.

:::

## Useful links

#### Services links

Deployed service: https://proxy.smartforms.io/fhir/QuestionnaireResponse/$extract

Underlying $transform service: https://proxy.smartforms.io/fhir/StructureMap/$transform

FHIR Mapping Language to StructureMap $convert service: https://proxy.smartforms.io/fhir/StructureMap/$convert

#### Specification links

FHIR $extract operation definition: http://hl7.org/fhir/uv/sdc/OperationDefinition/QuestionnaireResponse-extract

FHIR $transform operation definition: https://hl7.org/fhir/r4/structuremap-operation-transform.html

FHIR Mapping Language $convert workflow: https://confluence.hl7.org/pages/viewpage.action?pageId=76158820#UsingtheFHIRMappingLanguage-WebServices

FHIR StructureMap-based extraction: https://hl7.org/fhir/uv/sdc/extraction.html#structuremap-based-extraction

#### Source code links

Github: https://github.com/aehrc/smart-forms/tree/main/services/extract-express

Dockerhub: https://hub.docker.com/r/aehrc/smart-forms-extract

## Usage

Resource(s) can be extracted from a QuestionnaireResponse using a **POST** request to a URL such as:

```http request
https://proxy.smartforms.io/fhir/QuestionnaireResponse/$extract (type-level)
```

#### Parameters

| Name | Cardinality | Type | Documentation |
| ---------------------- | ----------- | -------- | ------------------------------------------------------------------------------------------------------- |
| questionnaire-response | 1..1 | Resource | The QuestionnaireResponse to extract data from. Used when the operation is invoked at the 'type' level. |

#### Try it out

[<img src="https://run.pstmn.io/button.svg" alt="Run In Postman"/>](https://elements.getpostman.com/redirect?entityId=22885901-2af2cfbb-3a0a-49c6-8404-105ef0751415&entityType=collection)

## How it works

The $extract operation is an abstraction on top of an existing StructureMap $transform operation.

#### The underlying $transform

A [$transform](https://hl7.org/fhir/r4/structuremap-operation-transform.html) operation requires two input parameters:

1. `source` - Contains the structure map defining the mapping rules
2. `content` - Contains the data to be transformed (in terms of SDC, this is the QuestionnaireResponse)

The output of $transform is the transformed data, a FHIR resource.

Taking this logic, we can use a StructureMap `$transform` operation to perform the extraction of resources from a QuestionnaireResponse. Let's say we want to extract a bundle containing an Observation resource from a QuestionnaireResponse.
We need a StructureMap that maps the data from the QuestionnaireResponse to the Observation resource. Normally we would write this mapping in [FHIR Mapping Language](https://www.hl7.org/fhir/mapping-language.html) and convert it to a StructureMap.

For more information on using the FHIR Mapping Language, refer to https://confluence.hl7.org/display/FHIR/Using+the+FHIR+Mapping+Language. Brian has a really awesome tool that can help you write and test your mappings at https://fhirpath-lab.com/FhirMapper2.

Once we have both the `source` StructureMap and the `content` QuestionnaireResponse, our $transform request body should look roughly like this:

```json
{
"resourceType": "Parameters",
"parameter": [
{
"name": "source",
"resource": //<insert StructureMap here> -
}
},
{
"name": "content",
"resource": //<insert QuestionnaireResponse here> - a filled QR from
}
]
}
```

Relevant resources:

`source`- https://smartforms.csiro.au/api/fhir/StructureMap/extract-bmi

`content`- http://smartforms.csiro.au/fhir/Questionnaire/CalculatedExpressionBMICalculatorPrepop

[//]: # 'turn it into a microservice'

Running the $transform operation will return the transformed data, which in this case is a Bundle resource containing the Observation resource:

```json
{
"resourceType": "Bundle",
"id": "<uuid>",
"type": "transaction",
"entry": [
{
"request": {
"method": "POST",
"url": "Observation"
},
"resource": {
"resourceType": "Observation",
"status": "final",
"code": {
"coding": [
{
"system": "http://snomed.info/sct",
"code": "60621009",
"display": "Body mass index"
}
]
},
"subject": {
"reference": "Patient/pat-sf"
},
"valueQuantity": {
"value": 29.55,
"unit": "kg/m2",
"system": "http://unitsofmeasure.org",
"code": "kg/m2"
}
}
}
]
}
```

#### The $extract operation

Using the logic above, we can abstract the $transform operation into a $extract operation. The $extract operation requires only one input parameter (or you can just provide the QuestionnaireResponse in the request body):

`questionnaire-response` - Contains the QuestionnaireResponse to extract data from

The provided QuestionnaireResponse should fulfill two criteria:

1. It needs to contain a canonical reference in its `questionnaire` property.

```json
{
...
"questionnaire": "https://smartforms.csiro.au/docs/sdc/population/calculated-expression-1|0.1.0",
...
}
```

2. The referenced Questionnaire should have a [questionnaire-targetStructureMap](http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-targetStructureMap) extension. This extension needs to contain a canonical reference to a StructureMap that maps the data from the QuestionnaireResponse to the desired resource(s).

```json
{
...
"extension": [
{
"url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-targetStructureMap",
"valueCanonical": "https://smartforms.csiro.au/docs/StructureMap/extract-bmi"
}
],
...
}
```

The $extract POC implementation defines a definitional repository to resolve Questionnaires and StructureMaps - https://smartforms.csiro.au/api/fhir. See https://hub.docker.com/r/aehrc/smart-forms-extract for more information.

The $extract operation will resolve the referenced Questionnaire + StructureMap, and set the StructureMap as the `source` in the $transform operation. The `content` will be the provided QuestionnaireResponse.

The underlying $transform operation will be executed, and the transformed data will be returned as the output of the $extract operation.

## The $convert operation

Brian's [.NET FHIR Mapping Language engine](https://github.com/brianpos/fhir-net-mappinglanguage/tree/main/demo-map-server) has a handy debug mode that can be activated by adding `debug=true` to the $transform query parameters.
The debug payload contains useful details such as warnings/errors, trace output, the converted StructureMap, and the output resource.

Conveniently, it also accepts both a FHIR Mapping Language map and a StructureMap resource as the `source` input.
This means we can use it as a $convert operation to convert a FHIR Mapping Language map to a StructureMap resource.

```
https://proxy.smartforms.io/fhir/StructureMap/$convert
```

It expects the request headers for the `Content-Type` to be `text/plain`. The request body should contain the FHIR Mapping Language map, and the response will contain the converted StructureMap resource.
2 changes: 1 addition & 1 deletion push-extract-image.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,5 @@ cd services/extract-express && npm run compile && cd -
# Build the Docker image for multiple architectures, then push to Docker Hub.
docker buildx build --file ./services/extract-express/Dockerfile \
--tag aehrc/smart-forms-extract:latest \
--tag aehrc/smart-forms-extract:v0.3.1 \
--tag aehrc/smart-forms-extract:v0.4.0 \
--platform linux/amd64,linux/arm64/v8 --push --no-cache .
9 changes: 6 additions & 3 deletions services/extract-express/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ It is an abstraction on top of an existing StructureMap [$transform](https://hl7

A proof-of-concept StructureMap $transform is defined on https://proxy.smartforms.io/fhir/StructureMap/$transform, leveraging Brian's .NET mapping engine from https://github.com/brianpos/fhir-net-mappinglanguage/tree/main/demo-map-server.

A `StructureMap/$convert` operation is also defined in the POC implementation to convert a FHIR Mapping Language map to a StructureMap resource, using the same .NET mapping engine.

## Configuration
Create a .env file (or copy from example.env) in the root of the project with the following:
```env
Expand All @@ -31,9 +33,10 @@ You can use `docker run -p 3003:3003 -e EHR_SERVER_URL=https://proxy.smartforms.

Docker image: https://hub.docker.com/r/aehrc/smart-forms-extract

**By default, ```FORMS_SERVER_URL``` is set to https://smartforms.csiro.au/api/fhir in the Docker image.**
**By default, ```FORMS_SERVER_URL``` is set to https://smartforms.csiro.au/api/fhir in the Docker image.** This endpoint is used to resolve referenced FHIR Questionnaires and StructureMaps.

## Sample implementation
A sample implementation of this service is available at https://proxy.smartforms.io/fhir/QuestionnaireResponse/$extract.
A sample implementation of the `$extract` service is available at https://proxy.smartforms.io/fhir/QuestionnaireResponse/$extract.
`StructureMap/$convert` is available at https://proxy.smartforms.io/fhir/StructureMap/$convert.

Note: The $extract service on https://smartforms.csiro.au/api/fhir only performs processing - it does not persist any data.
Note: The $extract and $convert service on https://proxy.smartforms.io/fhir only performs processing - it does not persist any data.
4 changes: 2 additions & 2 deletions services/extract-express/package.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"name": "extract-express",
"version": "0.3.1",
"version": "0.4.0",
"description": "",
"main": "lib/index.js",
"scripts": {
"compile": "tsc",
"start": "node lib/index.js",
"start": "node --inspect lib/index.js",
"start:watch": "node --inspect --watch lib/index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
Expand Down
76 changes: 76 additions & 0 deletions services/extract-express/src/debug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright 2024 Commonwealth Scientific and Industrial Research
* Organisation (CSIRO) ABN 41 687 119 230.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import type { OperationOutcome, Parameters, ParametersParameter, StructureMap } from 'fhir/r4b';

export function responseIsParametersResource(parameters: any): parameters is DebugOutputParameters {
return (
parameters &&
parameters.resourceType === 'Parameters' &&
!!parameters.parameter &&
parameters.parameter.length > 0 &&
parameters.parameter[2].name === 'parameters' &&
parameters.parameter[2].part.length > 0 &&
!!parameters.parameter[2].part[1] &&
parameters.parameter[2].part[1].name === 'source' &&
!!parameters.parameter[2].part[1].resource &&
parameters.parameter[2].part[1].resource.resourceType === 'StructureMap'
);
}

export function getStructureMapFromDebugOutputParameters(
outputParameters: any
): StructureMap | null {
if (!responseIsParametersResource(outputParameters)) {
return null;
}

return outputParameters.parameter[2].part[1].resource;
}

export interface DebugOutputParameters extends Parameters {
parameter: [OutcomeParameter, ResultParameter, DebugParametersParameter, TraceParameter];
}

interface OutcomeParameter extends ParametersParameter {
name: 'outcome';
resource: OperationOutcome;
}

interface ResultParameter extends ParametersParameter {
name: 'result';
valueString: string;
}

interface DebugParametersParameter extends ParametersParameter {
name: 'parameters';
part: [
{
name: 'evaluator';
valueString: string;
},
{
name: 'source';
resource: StructureMap;
}
];
}

interface TraceParameter extends ParametersParameter {
name: 'content';
part: any[];
}
24 changes: 24 additions & 0 deletions services/extract-express/src/fhirMappingLanguage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright 2024 Commonwealth Scientific and Industrial Research
* Organisation (CSIRO) ABN 41 687 119 230.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

export function getFhirMappingLanguageMap(body: any): string | null {
if (typeof body === 'string' && body.includes('map')) {
return body;
}

return null;
}
Loading
Loading