diff --git a/ts/server/src/app.controller.ts b/ts/server/src/app.controller.ts index eab3185..5b78a77 100644 --- a/ts/server/src/app.controller.ts +++ b/ts/server/src/app.controller.ts @@ -4,9 +4,11 @@ import { Resource } from 'fhir/r4b'; import * as fhirpath_r4_model from 'fhirpath/fhir-context/r4'; import * as fhirpath from 'fhirpath'; import { FPMLValidationErrorFilter } from './app.filters'; +import { FPOptions } from './utils/extract'; class Template { context: Record | Resource; template: object; + strict?: boolean; } function containsQuestionnaireResponse( @@ -23,63 +25,75 @@ export class AppController { @Post(['parse-template', 'r4/parse-template']) @HttpCode(200) resolveTemplateR4(@Body() body: Template): object { - const { context, template } = body; + const { context, template, strict = false } = body; + const options: FPOptions = { + userInvocationTable: { + answers: { + fn: (inputs, linkId: string) => { + return fhirpath.evaluate( + inputs, + `repeat(item).where(linkId='${linkId}').answer.value`, + null, + fhirpath_r4_model, + null, + ); + }, + arity: { 0: [], 1: ['String'] }, + }, + }, + }; if (containsQuestionnaireResponse(context)) { return this.appService.resolveTemplate( context.QuestionnaireResponse, template, context, fhirpath_r4_model, - { - userInvocationTable: { - answers: { - fn: (inputs, linkId: string) => { - return fhirpath.evaluate( - inputs, - `repeat(item).where(linkId='${linkId}').answer.value`, - null, - fhirpath_r4_model, - ); - }, - arity: { 0: [], 1: ['String'] }, - }, - }, - }, + options, + strict, ); } - return this.appService.resolveTemplate(context, template, context, fhirpath_r4_model); + return this.appService.resolveTemplate( + context, + template, + context, + fhirpath_r4_model, + options, + strict, + ); } @Post('aidbox/parse-template') @HttpCode(200) resolveTemplateAidbox(@Body() body: Template): object { - const { context, template } = body; + const { context, template, strict = false } = body; + const options: FPOptions = { + userInvocationTable: { + answers: { + fn: (inputs, linkId: string) => { + return fhirpath.evaluate( + inputs, + `repeat(item).where(linkId='${linkId}').answer.value.children()`, + null, + ); + }, + arity: { 0: [], 1: ['String'] }, + }, + }, + }; if (containsQuestionnaireResponse(context)) { return this.appService.resolveTemplate( context.QuestionnaireResponse, template, context, null, - { - userInvocationTable: { - answers: { - fn: (inputs, linkId: string) => { - return fhirpath.evaluate( - inputs, - `repeat(item).where(linkId='${linkId}').answer.value.children()`, - null, - ); - }, - arity: { 0: [], 1: ['String'] }, - }, - }, - }, + options, + strict, ); } - return this.appService.resolveTemplate(context, template, context); + return this.appService.resolveTemplate(context, template, context, null, options, strict); } } diff --git a/ts/server/src/app.service.ts b/ts/server/src/app.service.ts index 0180772..bc54c20 100644 --- a/ts/server/src/app.service.ts +++ b/ts/server/src/app.service.ts @@ -10,7 +10,8 @@ export class AppService { context: Context, model?: Model, options?: FPOptions, + strict?: boolean ): object { - return resolveTemplate(resource, template, { root: resource, ...context }, model, options); + return resolveTemplate(resource, template, { root: resource, ...context }, model, options, strict); } } diff --git a/ts/server/src/utils/extract.spec.ts b/ts/server/src/utils/extract.spec.ts index 6f2008d..60cbbf3 100644 --- a/ts/server/src/utils/extract.spec.ts +++ b/ts/server/src/utils/extract.spec.ts @@ -3,6 +3,12 @@ import { FPMLValidationError, resolveTemplate } from './extract'; describe('Transformation', () => { const resource = { list: [{ key: 1 }, { key: 2 }, { key: 3 }] } as any; + test('fails on access props of resource in strict mode', () => { + expect(() => + resolveTemplate(resource, { key: '{{ list }}' }, {}, null, null, true), + ).toThrow(FPMLValidationError); + }); + test('for empty object return empty object', () => { expect(resolveTemplate(resource, {})).toStrictEqual({}); }); diff --git a/ts/server/src/utils/extract.ts b/ts/server/src/utils/extract.ts index b9a92f0..767f164 100644 --- a/ts/server/src/utils/extract.ts +++ b/ts/server/src/utils/extract.ts @@ -14,18 +14,43 @@ export interface FPOptions { export class FPMLValidationError extends Error { constructor(message: string, path: Path) { const pathStr = path.filter((x) => x != rootNodeKey).join('.'); - super(`${message} on path ${pathStr}`); + super(`${message}. Path '${pathStr}'`); } } +const guardedResource = new Proxy( + {}, + { + get: (obj, prop) => { + if (prop === '__path__' || prop === 'resourceType') { + return undefined; + } + + throw new Error( + `Forbidden access to resource property ${String( + prop, + )} in strict mode. Use context instead`, + ); + }, + }, +); + export function resolveTemplate( resource: Resource, template: any, context?: Context, model?: Model, fpOptions?: FPOptions, + strict?: boolean, ): any { - return resolveTemplateRecur([], resource, template, context, model, fpOptions); + return resolveTemplateRecur( + [], + strict ? guardedResource : resource, + template, + context, + model, + fpOptions, + ); } function resolveTemplateRecur( @@ -354,6 +379,6 @@ export function evaluateExpression( options, ); } catch (exc) { - throw new FPMLValidationError(`Can not evaluate "${expression}": ${exc}`, path); + throw new FPMLValidationError(`Can not evaluate '${expression}': ${exc}`, path); } }