feat: Add support for custom 'this' and 'global' variables during evaluation (#9651)
This commit is contained in:
parent
861ee2c46e
commit
4c54ea21fb
|
|
@ -113,7 +113,7 @@ export interface ExecuteErrorPayload extends ErrorActionPayload {
|
|||
export const urlGroupsRegexExp = /^(https?:\/{2}\S+?)(\/[\s\S]*?)(\?(?![^{]*})[\s\S]*)?$/;
|
||||
|
||||
export const EXECUTION_PARAM_KEY = "executionParams";
|
||||
export const EXECUTION_PARAM_REFERENCE_REGEX = /this.params/g;
|
||||
export const THIS_DOT_PARAMS_KEY = "params";
|
||||
|
||||
export const RESP_HEADER_DATATYPE = "X-APPSMITH-DATATYPE";
|
||||
export const API_REQUEST_HEADERS: APIHeaders = {
|
||||
|
|
|
|||
|
|
@ -53,12 +53,13 @@ import toposort from "toposort";
|
|||
import equal from "fast-deep-equal/es6";
|
||||
import {
|
||||
EXECUTION_PARAM_KEY,
|
||||
EXECUTION_PARAM_REFERENCE_REGEX,
|
||||
THIS_DOT_PARAMS_KEY,
|
||||
} from "constants/AppsmithActionConstants/ActionConstants";
|
||||
import { DATA_BIND_REGEX } from "constants/BindingsConstants";
|
||||
import evaluate, {
|
||||
createGlobalData,
|
||||
EvalResult,
|
||||
EvaluateContext,
|
||||
EvaluationScriptType,
|
||||
getScriptToEval,
|
||||
} from "workers/evaluate";
|
||||
|
|
@ -584,6 +585,13 @@ export default class DataTreeEvaluator {
|
|||
const evaluationSubstitutionType =
|
||||
entity.bindingPaths[propertyPath] ||
|
||||
EvaluationSubstitutionType.TEMPLATE;
|
||||
|
||||
const contextData: EvaluateContext = {};
|
||||
if (isAction(entity)) {
|
||||
contextData.thisContext = {
|
||||
params: {},
|
||||
};
|
||||
}
|
||||
try {
|
||||
evalPropertyValue = this.getDynamicValue(
|
||||
unEvalPropertyValue,
|
||||
|
|
@ -591,6 +599,7 @@ export default class DataTreeEvaluator {
|
|||
resolvedFunctions,
|
||||
evaluationSubstitutionType,
|
||||
false,
|
||||
contextData,
|
||||
undefined,
|
||||
fullPropertyPath,
|
||||
);
|
||||
|
|
@ -757,6 +766,7 @@ export default class DataTreeEvaluator {
|
|||
resolvedFunctions: Record<string, any>,
|
||||
evaluationSubstitutionType: EvaluationSubstitutionType,
|
||||
returnTriggers: boolean,
|
||||
contextData?: EvaluateContext,
|
||||
callBackData?: Array<any>,
|
||||
fullPropertyPath?: string,
|
||||
) {
|
||||
|
|
@ -777,6 +787,7 @@ export default class DataTreeEvaluator {
|
|||
jsSnippets[0],
|
||||
data,
|
||||
resolvedFunctions,
|
||||
contextData,
|
||||
callBackData,
|
||||
returnTriggers,
|
||||
);
|
||||
|
|
@ -793,6 +804,7 @@ export default class DataTreeEvaluator {
|
|||
toBeSentForEval,
|
||||
data,
|
||||
resolvedFunctions,
|
||||
contextData,
|
||||
callBackData,
|
||||
entity && isJSAction(entity),
|
||||
);
|
||||
|
|
@ -848,6 +860,7 @@ export default class DataTreeEvaluator {
|
|||
js: string,
|
||||
data: DataTree,
|
||||
resolvedFunctions: Record<string, any>,
|
||||
contextData?: EvaluateContext,
|
||||
callbackData?: Array<any>,
|
||||
isTriggerBased = false,
|
||||
): EvalResult {
|
||||
|
|
@ -856,6 +869,7 @@ export default class DataTreeEvaluator {
|
|||
js,
|
||||
data,
|
||||
resolvedFunctions,
|
||||
contextData,
|
||||
callbackData,
|
||||
isTriggerBased,
|
||||
);
|
||||
|
|
@ -894,6 +908,7 @@ export default class DataTreeEvaluator {
|
|||
EvaluationSubstitutionType.TEMPLATE,
|
||||
true,
|
||||
undefined,
|
||||
undefined,
|
||||
fullPropertyPath,
|
||||
);
|
||||
valueToValidate = triggers;
|
||||
|
|
@ -1532,23 +1547,18 @@ export default class DataTreeEvaluator {
|
|||
);
|
||||
}
|
||||
|
||||
// Replace any reference of 'this.params' to 'executionParams' (backwards compatibility)
|
||||
const bindingsForExecutionParams: string[] = bindings.map(
|
||||
(binding: string) =>
|
||||
binding.replace(EXECUTION_PARAM_REFERENCE_REGEX, EXECUTION_PARAM_KEY),
|
||||
);
|
||||
|
||||
const dataTreeWithExecutionParams = Object.assign({}, this.evalTree, {
|
||||
[EXECUTION_PARAM_KEY]: evaluatedExecutionParams,
|
||||
});
|
||||
|
||||
return bindingsForExecutionParams.map((binding) =>
|
||||
return bindings.map((binding) =>
|
||||
this.getDynamicValue(
|
||||
`{{${binding}}}`,
|
||||
dataTreeWithExecutionParams,
|
||||
this.evalTree,
|
||||
this.resolvedFunctions,
|
||||
EvaluationSubstitutionType.TEMPLATE,
|
||||
false,
|
||||
// params can be accessed via "this.params" or "executionParams"
|
||||
{
|
||||
thisContext: { [THIS_DOT_PARAMS_KEY]: evaluatedExecutionParams },
|
||||
globalContext: { [EXECUTION_PARAM_KEY]: evaluatedExecutionParams },
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { parse, Node } from "acorn";
|
|||
import { ancestor } from "acorn-walk";
|
||||
import _ from "lodash";
|
||||
import { ECMA_VERSION } from "workers/constants";
|
||||
import { unEscapeScript } from "./evaluate";
|
||||
import { sanitizeScript } from "./evaluate";
|
||||
|
||||
/*
|
||||
* Valuable links:
|
||||
|
|
@ -153,17 +153,17 @@ export const extractIdentifiersFromCode = (code: string): string[] => {
|
|||
let functionalParams = new Set<string>();
|
||||
let ast: Node = { end: 0, start: 0, type: "" };
|
||||
try {
|
||||
const unEscapedCode = unEscapeScript(code);
|
||||
const sanitizedScript = sanitizeScript(code);
|
||||
/* wrapCode - Wrapping code in a function, since all code/script get wrapped with a function during evaluation.
|
||||
Some syntaxes won't be valid unless they're at the RHS of a statement.
|
||||
Some syntax won't be valid unless they're at the RHS of a statement.
|
||||
Since we're assigning all code/script to RHS during evaluation, we do the same here.
|
||||
So that during ast parse, those errors are neglected.
|
||||
*/
|
||||
/* e.g. IIFE without braces
|
||||
function() { return 123; }() -> is invalid
|
||||
let result = function() { return 123; }() -> is valid
|
||||
let result = function() { return 123; }() -> is valid
|
||||
*/
|
||||
const wrappedCode = wrapCode(unEscapedCode);
|
||||
const wrappedCode = wrapCode(sanitizedScript);
|
||||
ast = getAST(wrappedCode);
|
||||
} catch (e) {
|
||||
if (e instanceof SyntaxError) {
|
||||
|
|
@ -196,7 +196,7 @@ export const extractIdentifiersFromCode = (code: string): string[] => {
|
|||
isMemberExpressionNode(parent) &&
|
||||
/* Member expressions that are "computed" (with [ ] search)
|
||||
and the ones that have optional chaining ( a.b?.c )
|
||||
will be considered top level node.
|
||||
will be considered top level node.
|
||||
We will stop looking for further parents */
|
||||
/* "computed" exception - isArrayAccessorNode
|
||||
Member expressions that are array accessors with static index - [9]
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ describe("evaluate", () => {
|
|||
const result = wrongJS
|
||||
return result;
|
||||
}
|
||||
closedFunction()
|
||||
closedFunction.call(THIS_CONTEXT)
|
||||
`,
|
||||
severity: "error",
|
||||
originalBinding: "wrongJS",
|
||||
|
|
@ -86,7 +86,7 @@ describe("evaluate", () => {
|
|||
const result = wrongJS
|
||||
return result;
|
||||
}
|
||||
closedFunction()
|
||||
closedFunction.call(THIS_CONTEXT)
|
||||
`,
|
||||
severity: "error",
|
||||
originalBinding: "wrongJS",
|
||||
|
|
@ -106,7 +106,7 @@ describe("evaluate", () => {
|
|||
const result = {}.map()
|
||||
return result;
|
||||
}
|
||||
closedFunction()
|
||||
closedFunction.call(THIS_CONTEXT)
|
||||
`,
|
||||
severity: "error",
|
||||
originalBinding: "{}.map()",
|
||||
|
|
@ -121,7 +121,7 @@ describe("evaluate", () => {
|
|||
});
|
||||
it("gets triggers from a function", () => {
|
||||
const js = "showAlert('message', 'info')";
|
||||
const response = evaluate(js, dataTree, {}, undefined, true);
|
||||
const response = evaluate(js, dataTree, {}, undefined, undefined, true);
|
||||
//this will be changed again in new implemenation for promises
|
||||
const data = {
|
||||
action: {
|
||||
|
|
@ -172,7 +172,7 @@ describe("evaluate", () => {
|
|||
const result = setTimeout(() => {}, 100)
|
||||
return result;
|
||||
}
|
||||
closedFunction()
|
||||
closedFunction.call(THIS_CONTEXT)
|
||||
`,
|
||||
severity: "error",
|
||||
originalBinding: "setTimeout(() => {}, 100)",
|
||||
|
|
@ -188,7 +188,23 @@ describe("evaluate", () => {
|
|||
it("evaluates functions with callback data", () => {
|
||||
const js = "(arg1, arg2) => arg1.value + arg2";
|
||||
const callbackData = [{ value: "test" }, "1"];
|
||||
const response = evaluate(js, dataTree, {}, callbackData);
|
||||
const response = evaluate(js, dataTree, {}, {}, callbackData);
|
||||
expect(response.result).toBe("test1");
|
||||
});
|
||||
it("has access to this context", () => {
|
||||
const js = "this.contextVariable";
|
||||
const thisContext = { contextVariable: "test" };
|
||||
const response = evaluate(js, dataTree, {}, { thisContext });
|
||||
expect(response.result).toBe("test");
|
||||
// there should not be any error when accessing "this" variables
|
||||
expect(response.errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("has access to additional global context", () => {
|
||||
const js = "contextVariable";
|
||||
const globalContext = { contextVariable: "test" };
|
||||
const response = evaluate(js, dataTree, {}, { globalContext });
|
||||
expect(response.result).toBe("test");
|
||||
expect(response.errors).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -32,12 +32,12 @@ export const EvaluationScripts: Record<EvaluationScriptType, string> = {
|
|||
const result = ${ScriptTemplate}
|
||||
return result;
|
||||
}
|
||||
closedFunction()
|
||||
closedFunction.call(THIS_CONTEXT)
|
||||
`,
|
||||
[EvaluationScriptType.ANONYMOUS_FUNCTION]: `
|
||||
function callback (script) {
|
||||
const userFunction = script;
|
||||
const result = userFunction.apply(self, ARGUMENTS);
|
||||
const result = userFunction.apply(THIS_CONTEXT, ARGUMENTS);
|
||||
return result;
|
||||
}
|
||||
callback(${ScriptTemplate})
|
||||
|
|
@ -47,7 +47,7 @@ export const EvaluationScripts: Record<EvaluationScriptType, string> = {
|
|||
const result = ${ScriptTemplate}
|
||||
return result
|
||||
}
|
||||
closedFunction();
|
||||
closedFunction.call(THIS_CONTEXT);
|
||||
`,
|
||||
};
|
||||
|
||||
|
|
@ -95,11 +95,24 @@ export const createGlobalData = (
|
|||
dataTree: DataTree,
|
||||
resolvedFunctions: Record<string, any>,
|
||||
isTriggerBased: boolean,
|
||||
context?: EvaluateContext,
|
||||
evalArguments?: Array<any>,
|
||||
) => {
|
||||
const GLOBAL_DATA: Record<string, any> = {};
|
||||
///// Adding callback data
|
||||
GLOBAL_DATA.ARGUMENTS = evalArguments;
|
||||
//// Adding contextual data not part of data tree
|
||||
GLOBAL_DATA.THIS_CONTEXT = {};
|
||||
if (context) {
|
||||
if (context.thisContext) {
|
||||
GLOBAL_DATA.THIS_CONTEXT = context.thisContext;
|
||||
}
|
||||
if (context.globalContext) {
|
||||
Object.entries(context.globalContext).forEach(([key, value]) => {
|
||||
GLOBAL_DATA[key] = value;
|
||||
});
|
||||
}
|
||||
}
|
||||
///// Mocking Promise class
|
||||
GLOBAL_DATA.Promise = AppsmithPromise;
|
||||
if (isTriggerBased) {
|
||||
|
|
@ -128,26 +141,34 @@ export const createGlobalData = (
|
|||
return GLOBAL_DATA;
|
||||
};
|
||||
|
||||
export function unEscapeScript(js: string) {
|
||||
export function sanitizeScript(js: string) {
|
||||
// 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 trimmedJS = js.replace(beginsWithLineBreakRegex, "");
|
||||
const unescapedJS =
|
||||
self.evaluationVersion > 1 ? trimmedJS : unescapeJS(trimmedJS);
|
||||
return unescapedJS;
|
||||
return self.evaluationVersion > 1 ? trimmedJS : unescapeJS(trimmedJS);
|
||||
}
|
||||
|
||||
/** Define a context just for this script
|
||||
* thisContext will define it on the `this`
|
||||
* globalContext will define it globally
|
||||
*/
|
||||
export type EvaluateContext = {
|
||||
thisContext?: Record<string, any>;
|
||||
globalContext?: Record<string, any>;
|
||||
};
|
||||
|
||||
export default function evaluate(
|
||||
js: string,
|
||||
data: DataTree,
|
||||
resolvedFunctions: Record<string, any>,
|
||||
context?: EvaluateContext,
|
||||
evalArguments?: Array<any>,
|
||||
isTriggerBased = false,
|
||||
): EvalResult {
|
||||
const unescapedJS = unEscapeScript(js);
|
||||
const sanitizedScript = sanitizeScript(js);
|
||||
const scriptType = getScriptType(evalArguments, isTriggerBased);
|
||||
const script = getScriptToEval(unescapedJS, scriptType);
|
||||
const script = getScriptToEval(sanitizedScript, scriptType);
|
||||
// We are linting original js binding,
|
||||
// This will make sure that the character count is not messed up when we do unescapejs
|
||||
const scriptToLint = getScriptToEval(js, scriptType);
|
||||
|
|
@ -160,6 +181,7 @@ export default function evaluate(
|
|||
data,
|
||||
resolvedFunctions,
|
||||
isTriggerBased,
|
||||
context,
|
||||
evalArguments,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -193,6 +193,7 @@ ctx.addEventListener(
|
|||
resolvedFunctions,
|
||||
EvaluationSubstitutionType.TEMPLATE,
|
||||
true,
|
||||
undefined,
|
||||
callbackData,
|
||||
fullPropertyPath,
|
||||
);
|
||||
|
|
@ -265,6 +266,7 @@ ctx.addEventListener(
|
|||
evalTree,
|
||||
resolvedFunctions,
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
return result;
|
||||
|
|
@ -274,7 +276,7 @@ ctx.addEventListener(
|
|||
const evalTree = dataTreeEvaluator?.evalTree;
|
||||
if (!evalTree) return {};
|
||||
return isTrigger
|
||||
? evaluate(expression, evalTree, {}, [], true)
|
||||
? evaluate(expression, evalTree, {}, undefined, [], true)
|
||||
: evaluate(expression, evalTree, {});
|
||||
case EVAL_WORKER_ACTIONS.UPDATE_REPLAY_OBJECT:
|
||||
const { entity, entityId, entityType } = requestData;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {
|
|||
} from "utils/DynamicBindingUtils";
|
||||
import { JSHINT as jshint } from "jshint";
|
||||
import { Severity } from "entities/AppsmithConsole";
|
||||
import { last, keys, isEmpty } from "lodash";
|
||||
import { isEmpty, keys, last } from "lodash";
|
||||
import {
|
||||
EvaluationScripts,
|
||||
EvaluationScriptType,
|
||||
|
|
@ -27,21 +27,20 @@ export const getPositionInEvaluationScript = (
|
|||
return { line: lines.length, ch: lastLine.length };
|
||||
};
|
||||
|
||||
const EvalutionScriptPositions: Record<string, Position> = {};
|
||||
const EvaluationScriptPositions: Record<string, Position> = {};
|
||||
|
||||
function getEvaluationScriptPosition(scriptType: EvaluationScriptType) {
|
||||
if (isEmpty(EvalutionScriptPositions)) {
|
||||
if (isEmpty(EvaluationScriptPositions)) {
|
||||
// We are computing position of <<script>> in our templates.
|
||||
// This will be used to get the exact location of error in linting
|
||||
keys(EvaluationScripts).forEach((type) => {
|
||||
const location = getPositionInEvaluationScript(
|
||||
EvaluationScriptPositions[type] = getPositionInEvaluationScript(
|
||||
type as EvaluationScriptType,
|
||||
);
|
||||
EvalutionScriptPositions[type] = location;
|
||||
});
|
||||
}
|
||||
|
||||
return EvalutionScriptPositions[scriptType];
|
||||
return EvaluationScriptPositions[scriptType];
|
||||
}
|
||||
|
||||
export const getLintingErrors = (
|
||||
|
|
|
|||
|
|
@ -795,7 +795,7 @@ export const VALIDATORS: Record<ValidationTypes, Validator> = {
|
|||
};
|
||||
if (config.params?.fnString && isString(config.params?.fnString)) {
|
||||
try {
|
||||
const { result } = evaluate(config.params.fnString, {}, {}, [
|
||||
const { result } = evaluate(config.params.fnString, {}, {}, undefined, [
|
||||
value,
|
||||
props,
|
||||
_,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user