Skip to content

Commit

Permalink
Merge pull request #6 from beda-software/directives-support
Browse files Browse the repository at this point in the history
feat: add support for if, for, assign and merge

Closes #1 #2 #3 #7 #8
  • Loading branch information
ruscoder authored May 28, 2024
2 parents 4778131 + 3fba890 commit c32c69c
Show file tree
Hide file tree
Showing 14 changed files with 2,502 additions and 286 deletions.
1 change: 1 addition & 0 deletions ts/server/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module.exports = {
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'prettier/prettier': 0,
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
Expand Down
2 changes: 1 addition & 1 deletion ts/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"@nestjs/core": "^10.0.0",
"@nestjs/platform-express": "^10.0.0",
"@types/fhir": "^0.0.37",
"fhirpath": "^3.6.1",
"fhirpath": "^3.13.2",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1"
},
Expand Down
44 changes: 31 additions & 13 deletions ts/server/src/app.controller.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Controller, Post, Body, HttpCode } from '@nestjs/common';
import { Controller, Post, Body, HttpCode, UseFilters } from '@nestjs/common';
import { AppService } from './app.service';
import { Resource } from 'fhir/r4b';

import * as fhirpath_r4_model from 'fhirpath/fhir-context/r4';
import { FPMLValidationErrorFilter } from './app.filters';
class Template {
context: Record<string, Resource> | Resource;
template: object;
strict?: boolean;
}

function containsQuestionnaireResponse(
Expand All @@ -13,20 +15,36 @@ function containsQuestionnaireResponse(
return Object.keys(context).includes('QuestionnaireResponse');
}

@Controller('parse-template')
@Controller()
@UseFilters(FPMLValidationErrorFilter)
export class AppController {
constructor(private readonly appService: AppService) {}

@Post()
@Post(['parse-template', 'r4/parse-template'])
@HttpCode(200)
resolveTemplateR4(@Body() body: Template): object {
const { context, template, strict = false } = body;

return this.appService.resolveTemplate(
containsQuestionnaireResponse(context) ? context.QuestionnaireResponse : context,
template,
context,
fhirpath_r4_model,
strict,
);
}

@Post('aidbox/parse-template')
@HttpCode(200)
resolveTemplate(@Body() body: Template): object {
const { context, template } = body;
let result: object;
if (containsQuestionnaireResponse(context)) {
result = this.appService.resolveTemplate(context.QuestionnaireResponse, template);
} else {
result = this.appService.resolveTemplate(context, template);
}
return result;
resolveTemplateAidbox(@Body() body: Template): object {
const { context, template, strict = false } = body;

return this.appService.resolveTemplate(
containsQuestionnaireResponse(context) ? context.QuestionnaireResponse : context,
template,
context,
null,
strict,
);
}
}
18 changes: 18 additions & 0 deletions ts/server/src/app.filters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ExceptionFilter, Catch, ArgumentsHost, HttpStatus } from '@nestjs/common';
import { FPMLValidationError } from './utils/extract';

@Catch(FPMLValidationError)
export class FPMLValidationErrorFilter implements ExceptionFilter {
catch(exception: any, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();

response.status(HttpStatus.BAD_REQUEST).json({
statusCode: HttpStatus.BAD_REQUEST,
timestamp: new Date().toISOString(),
path: request.url,
message: exception.message || 'Unprocessable Entity',
});
}
}
45 changes: 41 additions & 4 deletions ts/server/src/app.service.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,47 @@
import { Injectable } from '@nestjs/common';
import { resolveTemplate } from './utils/extract';
import { Resource } from 'fhir/r4b';
import { FPOptions, resolveTemplate } from './utils/extract';
import * as fhirpath from 'fhirpath';

@Injectable()
export class AppService {
resolveTemplate(qr: Resource, template: object): object {
return resolveTemplate(qr, template);
resolveTemplate(
resource: Record<string, any>,
template: object,
context: Context,
model?: Model,
strict?: boolean,
): object {
const options: FPOptions = {
userInvocationTable: {
answers: {
fn: (inputs, linkId: string) => {
return fhirpath.evaluate(
inputs,
model
? `repeat(item).where(linkId='${linkId}').answer.value`
: `repeat(item).where(linkId='${linkId}').answer.value.children()`,
null,
model,
null,
);
},
arity: { 0: [], 1: ['String'] },
},
// Get rid of toString once it's fixed https://github.com/HL7/fhirpath.js/issues/156
toString: {
fn: (inputs) => fhirpath.evaluate({ x: inputs }, 'x.toString()'),
arity: { 0: [] },
},
},
};

return resolveTemplate(
resource,
template,
{ root: resource, ...context },
model,
options,
strict,
);
}
}
171 changes: 171 additions & 0 deletions ts/server/src/utils/__data__/complex-example.aidbox.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
body:
"{% assign %}":
- patientId: "{{ %Patient.id }}"
- recordedDate: "{{ %QuestionnaireResponse.authored }}"
- observationEntries:
- "{% if answers('WEIGHT').exists() and answers('HEIGHT').exists() %}":
"{% assign %}":
- observationId: >-
{{
%Observation.where(
id in %Provenance.target.id and
category.coding.code='vital-signs' and
resourceType='Observation' and
code.coding.code='29463-7'
).id
}}
fullUrl: "urn:uuid:observation-weight"
request:
"{% if %observationId.exists() %}":
url: "/Observation/{{ %observationId }}"
method: PUT
"{% else %}":
url: "/Observation?patient={{ %patientId }}&category=vital-signs&code=http://loinc.org|29463-7"
method: POST
resource:
resourceType: Observation
id: "{{ %observationId }}"
subject:
resourceType: Patient
id: "{{ %patientId }}"
status: final
effective:
dateTime: "{{ %recordedDate }}"
category:
- coding:
- system: http://terminology.hl7.org/CodeSystem/observation-category
code: vital-signs
code:
coding:
- system: "http://loinc.org"
code: 29463-7
display: Body Weight
value:
Quantity:
value:
"{% assign %}":
- rawHeight: "{{ answers('HEIGHT') }}"
- rawWeight: "{{ answers('WEIGHT') }}"
"{% if %rawHeight < 90 %}": "{{ %rawWeight / 2.205 }}"
"{% else %}": "{{ %rawWeight }}"
unit: kg
system: "http://unitsofmeasure.org"
code: kg

- "{% if answers('HEIGHT').exists() %}":
"{% assign %}":
- observationId: >-
{{
%Observation.where(
id in %Provenance.target.id and
category.coding.code='vital-signs' and
resourceType='Observation' and
code.coding.code='29463-7'
).id
}}
fullUrl: "urn:uuid:observation-height"
request:
"{% if %observationId.exists() %}":
url: "/Observation/{{ %observationId }}"
method: PUT
"{% else %}":
url: "/Observation?patient={{ %patientId }}&category=vital-signs&code=http://loinc.org|8302-2"
method: POST
resource:
resourceType: Observation
id: "{{ %observationId }}"
subject:
resourceType: Patient
id: "{{ %patientId }}"
status: final
effective:
dateTime: "{{ %recordedDate }}"
category:
- coding:
- system: http://terminology.hl7.org/CodeSystem/observation-category
code: vital-signs
code:
coding:
- system: "http://loinc.org"
code: 8302-2
display: Body Height
value:
Quantity:
value:
"{% assign %}":
- rawHeight: "{{ answers('HEIGHT') }}"
# 90 inch ~ 230cm
"{% if %rawHeight < 90 %}": "{{ %rawHeight * 2.54 }}" # inches to cm
"{% else %}": "{{ %rawHeight }}" # cm
unit: kg
system: "http://unitsofmeasure.org"
code: kg

- conditionEntries:
"{% for index, coding in answers('MEDCOND1') | answers('MEDCOND2') %}":
"{% assign %}":
- conditionId: >-
{{
%Condition.where(resourceType='Condition').where(
id in %Provenance.target.id and
category.coding.code='medicalHistory'
and code.coding.system=%coding.system and
code.coding.code=%coding.code
).id
}}
fullUrl: "urn:uuid:condition-medical-history-{{ %index }}"
request:
"{% if %conditionId.exists() %}":
url: "Condition/{{ %conditionId }}"
method: PUT
"{% else %}":
url: /Condition?category=medicalHistory&code={{ %coding.system }}|{{ %coding.code }}&patient={{ %patientId }}
method: POST
resource:
resourceType: Condition
id: "{{ %conditionId }}"
subject:
resourceType: Patient
id: "{{ %patientId }}"
recordedDate: "{{ %recordedDate }}"
code:
coding:
- "{{ %coding }}"
text: "{{ %coding.display }}"
category:
- coding:
- code: medicalHistory
display: Medical history
- provenanceEntries:
"{% for entry in %observationEntries | %conditionEntries %}":
request:
url: /Provenance
method: POST
resource:
resourceType: Provenance
target:
- uri: "{{ %entry.fullUrl }}"
recorded: "{{ %recordedDate }}"
agent:
- who:
resourceType: Organization
id: "{{ %Organization.id }}"
entity:
- role: source
what:
resourceType: QuestionnaireResponse
id: "{{ %QuestionnaireResponse.id }}"
resourceType: Bundle
type: transaction
entry:
- "{% for entry in %observationEntries | %conditionEntries | %provenanceEntries %}": "{{ %entry }}"
- "{% assign %}":
- resourceIdsToDelete:
"{% for id in %Provenance.target.id.exclude((%observationEntries | %conditionEntries).resource.id) %}": "{{ %id }}"
"{% for provenance in %Provenance.where(target.id in %resourceIdsToDelete) %}":
- request:
url: "/Provenance/{{ %provenance.id }}"
method: DELETE
- request:
url: "/{{ %provenance.target.resourceType }}/{{ %provenance.target.id }}"
method: DELETE
Loading

0 comments on commit c32c69c

Please sign in to comment.