PromucFlow_constructor/app/client/src/workers/evaluate.ts
2021-07-14 16:19:01 +05:30

188 lines
5.7 KiB
TypeScript

import { ActionDescription, DataTree } from "entities/DataTree/dataTreeFactory";
import { addFunctions } from "workers/evaluationUtils";
import _ from "lodash";
import {
EvaluationError,
extraLibraries,
PropertyEvaluationErrorType,
unsafeFunctionForEval,
} from "utils/DynamicBindingUtils";
import unescapeJS from "unescape-js";
import { JSHINT as jshint } from "jshint";
import { Severity } from "entities/AppsmithConsole";
export type EvalResult = {
result: any;
triggers?: ActionDescription<any>[];
errors: EvaluationError[];
};
export enum EvaluationScriptType {
EXPRESSION = "EXPRESSION",
ANONYMOUS_FUNCTION = "ANONYMOUS_FUNCTION",
TRIGGERS = "TRIGGERS",
}
const evaluationScripts: Record<
EvaluationScriptType,
(script: string) => string
> = {
[EvaluationScriptType.EXPRESSION]: (script: string) => `return ${script}`,
[EvaluationScriptType.ANONYMOUS_FUNCTION]: (script) =>
`const userFunction = ${script}
return userFunction.apply(self, ARGUMENTS)`,
[EvaluationScriptType.TRIGGERS]: (script) => `(function() { ${script} })()`,
};
const getScriptToEval = (
userScript: string,
evalArguments?: Array<any>,
isTriggerBased = false,
): string => {
let scriptType = EvaluationScriptType.EXPRESSION;
if (evalArguments) {
scriptType = EvaluationScriptType.ANONYMOUS_FUNCTION;
} else if (isTriggerBased) {
scriptType = EvaluationScriptType.TRIGGERS;
}
return evaluationScripts[scriptType](userScript);
};
const getLintingErrors = (
script: string,
data: Record<string, unknown>,
): EvaluationError[] => {
const globalData: Record<string, boolean> = {};
Object.keys(data).forEach((datum) => (globalData[datum] = false));
const options = {
indent: 2,
esversion: 7,
eqeqeq: true,
curly: true,
freeze: true,
undef: true,
unused: true,
asi: true,
worker: true,
globals: globalData,
};
jshint(script, options);
return jshint.errors.map((lintError) => {
return {
errorType: PropertyEvaluationErrorType.LINT,
raw: script,
severity: lintError.code.startsWith("W")
? Severity.WARNING
: Severity.ERROR,
errorMessage: lintError.reason,
errorSegment: lintError.evidence,
};
});
};
const beginsWithLineBreakRegex = /^\s+|\s+$/;
export default function evaluate(
js: string,
data: DataTree,
evalArguments?: Array<any>,
isTriggerBased = false,
): EvalResult {
// We remove any line breaks from the beginning of the script because that
// makes the final function invalid. We also unescape any escaped characters
// so that eval can happen
const unescapedJS = unescapeJS(js.replace(beginsWithLineBreakRegex, ""));
const script = getScriptToEval(unescapedJS, evalArguments, isTriggerBased);
return (function() {
let errors: EvaluationError[] = [];
let result;
let triggers: any[] = [];
/**** Setting the eval context ****/
const GLOBAL_DATA: Record<string, any> = {};
///// Adding callback data
GLOBAL_DATA.ARGUMENTS = evalArguments;
if (isTriggerBased) {
//// Add internal functions to dataTree;
const dataTreeWithFunctions = addFunctions(data);
///// Adding Data tree with functions
Object.keys(dataTreeWithFunctions).forEach((datum) => {
GLOBAL_DATA[datum] = dataTreeWithFunctions[datum];
});
///// Fixing action paths and capturing their execution response
if (dataTreeWithFunctions.actionPaths) {
GLOBAL_DATA.triggers = [];
const pusher = function(
this: DataTree,
action: any,
...payload: any[]
) {
const actionPayload = action(...payload);
GLOBAL_DATA.triggers.push(actionPayload);
};
GLOBAL_DATA.actionPaths.forEach((path: string) => {
const action = _.get(GLOBAL_DATA, path);
const entity = _.get(GLOBAL_DATA, path.split(".")[0]);
if (action) {
_.set(GLOBAL_DATA, path, pusher.bind(data, action.bind(entity)));
}
});
}
} else {
///// Adding Data tree
Object.keys(data).forEach((datum) => {
GLOBAL_DATA[datum] = data[datum];
});
}
// Set it to self so that the eval function can have access to it
// as global data. This is what enables access all appsmith
// entity properties from the global context
Object.keys(GLOBAL_DATA).forEach((key) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: No types available
self[key] = GLOBAL_DATA[key];
});
errors = getLintingErrors(script, GLOBAL_DATA);
///// Adding extra libraries separately
extraLibraries.forEach((library) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: No types available
self[library.accessor] = library.lib;
});
///// Remove all unsafe functions
unsafeFunctionForEval.forEach((func) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: No types available
self[func] = undefined;
});
try {
result = Function(script)();
if (isTriggerBased) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
triggers = [...self.triggers];
}
} catch (e) {
errors.push({
errorMessage: `${e.stack.split(`\n`)[0]}`,
severity: Severity.ERROR,
raw: script,
errorType: PropertyEvaluationErrorType.PARSE,
});
}
// Remove it from self
// This is needed so that next eval can have a clean sheet
Object.keys(GLOBAL_DATA).forEach((key) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: No types available
delete self[key];
});
return { result, triggers, errors };
})();
}