Skip to content

Commit

Permalink
(feat) Use Function() rather than eval() (#112)
Browse files Browse the repository at this point in the history
  • Loading branch information
ibacher authored Aug 22, 2023
1 parent 0985d8b commit 0135a7f
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 67 deletions.
11 changes: 9 additions & 2 deletions src/utils/common-expression-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ export class CommonExpressionHelpers {
this.patient = patient;
}

today() {
today = () => {
return new Date();
}
};

includes = <T = any>(collection: T[], value: T) => {
return collection?.includes(value);
Expand Down Expand Up @@ -279,6 +279,13 @@ export class CommonExpressionHelpers {
}
return daySinceLastObs === '' ? '0' : daySinceLastObs;
};

/**
* Used as wrapper around async functions. It basically evaluates the promised value.
*/
resolve = (lazy: Promise<unknown>) => {
return Promise.resolve(lazy);
};
}

export function registerDependency(node: FormNode, determinant: OHRIFormField) {
Expand Down
2 changes: 0 additions & 2 deletions src/utils/expression-runner.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { OHRIFormField } from '../api/types';
import { ConceptFalse } from '../constants';
import { CommonExpressionHelpers } from './common-expression-helpers';
import { parseExpression } from './expression-parser';
import { checkReferenceToResolvedFragment, evaluateExpression, ExpressionContext } from './expression-runner';

export const testFields: Array<OHRIFormField> = [
Expand Down
114 changes: 53 additions & 61 deletions src/utils/expression-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,41 +28,27 @@ export function evaluateExpression(
// register dependencies
findAndRegisterReferencedFields(node, parts, fields);
// setup function scope
let { mode, myValue, patient } = context;
let { myValue, patient } = context;
const { sex, age } = patient && 'sex' in patient && 'age' in patient ? patient : { sex: undefined, age: undefined };

if (node.type === 'field' && myValue === undefined) {
myValue = fieldValues[node.value['id']];
}
const {
isEmpty,
today,
includes,
isDateBefore,
isDateAfter,
addWeeksToDate,
addDaysToDate,
useFieldValue,
calcBMI,
calcEDD,
calcMonthsOnART,
calcViralLoadStatus,
calcNextVisitDate,
calcTreatmentEndDate,
calcAgeBasedOnDate,
calcBSA,
arrayContains,
arrayContainsAny,
formatDate,
extractRepeatingGroupValues,
calcGravida,
calcTimeDifference,
} = new CommonExpressionHelpers(node, patient, fields, fieldValues, allFieldsKeys);

const expressionContext = {
...new CommonExpressionHelpers(node, patient, fields, fieldValues, allFieldsKeys),
...context,
fieldValues,
patient,
myValue,
sex,
age,
};

expression = linkReferencedFieldValues(fields, fieldValues, parts);

try {
return eval(expression);
return evaluate(expression, expressionContext);
} catch (error) {
console.error(`Error: ${error} \n\n failing expression: ${expression}`);
}
Expand All @@ -83,39 +69,27 @@ export async function evaluateAsyncExpression(
let parts = parseExpression(expression.trim());
// register dependencies
findAndRegisterReferencedFields(node, parts, fields);

// setup function scope
let { mode, myValue, patient } = context;
let { myValue, patient } = context;
const { sex, age } = patient && 'sex' in patient && 'age' in patient ? patient : { sex: undefined, age: undefined };
if (node.type === 'field' && myValue === undefined) {
myValue = fieldValues[node.value['id']];
}
const {
api,
isEmpty,
today,
includes,
isDateBefore,
isDateAfter,
addWeeksToDate,
addDaysToDate,
useFieldValue,
calcBMI,
calcEDD,
calcMonthsOnART,
calcViralLoadStatus,
calcNextVisitDate,
calcTreatmentEndDate,
calcAgeBasedOnDate,
calcBSA,
arrayContains,
arrayContainsAny,
formatDate,
extractRepeatingGroupValues,
calcGravida,
calcTimeDifference,
} = new CommonExpressionHelpers(node, patient, fields, fieldValues, allFieldsKeys);

const expressionContext = {
...new CommonExpressionHelpers(node, patient, fields, fieldValues, allFieldsKeys),
...context,
fieldValues,
patient,
myValue,
sex,
age,
temporaryObjectsMap: {},
};

expression = linkReferencedFieldValues(fields, fieldValues, parts);

// parts with resolve-able field references
parts = parseExpression(expression);
const lazyFragments = [];
Expand All @@ -130,7 +104,7 @@ export async function evaluateAsyncExpression(

const temporaryObjectsMap = {};
// resolve lazy fragments
const fragments = await Promise.all(lazyFragments.map(({ expression }) => eval(expression)));
const fragments = await Promise.all(lazyFragments.map(({ expression }) => evaluate(expression, expressionContext)));
lazyFragments.forEach((fragment, index) => {
if (typeof fragments[index] == 'object') {
const objectKey = `obj_${index}`;
Expand All @@ -144,21 +118,16 @@ export async function evaluateAsyncExpression(
}
});

expressionContext.temporaryObjectsMap = temporaryObjectsMap;

try {
return eval(expression);
return evaluate(expression, expressionContext);
} catch (error) {
console.error(`Error: ${error} \n\n failing expression: ${expression}`);
}
return null;
}

/**
* Used as wrapper around async functions. It basically evaluates the promised value.
*/
export function resolve(lazy: Promise<any>) {
return Promise.resolve(lazy);
}

/**
* Checks if the given token contains a reference to a resolved fragment
* and returns the fragment and the remaining chained reference.
Expand All @@ -172,3 +141,26 @@ export function checkReferenceToResolvedFragment(token: string) {
const chainedRef = match.length ? token.substring(token.indexOf(match[0]) + match[0].length) : '';
return [match[0] || '', chainedRef];
}

/**
* A slightly safer version of the built-in eval()
*
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval
*
* ```js
* evaluate("myNum + 2", { myNum: 5 }); // 7
* ```
*
* Note that references to variables not included in the `expressionContext` will result at
* `undefined` during evaluation.
*
* @param expression A JS expression to execute
* @param expressionContext A JS object consisting of the names to make available in the scope
* the expression is executed in.
*/
function evaluate(expression: string, expressionContext?: Record<string, any>) {
return Function(...Object.keys(expressionContext), `"use strict"; return (${expression})`).call(
undefined,
...Object.values(expressionContext),
);
}
4 changes: 2 additions & 2 deletions src/validators/ohri-js-expression-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ export const OHRIJSExpressionValidator: FieldValidator = {
const FIELD_HAS_WARNINGS_MESSAGE = 'Field has warnings';
config.expressionContext.myValue = value;
return Object.keys(config)
.filter(key => key == 'failsWhenExpression' || key == 'warnsWhenExpression')
.filter(key => key === 'failsWhenExpression' || key === 'warnsWhenExpression')
.flatMap(key => {
const isErrorValidator = key == 'failsWhenExpression';
const isErrorValidator = key === 'failsWhenExpression';
return evaluateExpression(
config[key],
{ value: field, type: 'field' },
Expand Down

0 comments on commit 0135a7f

Please sign in to comment.