diff --git a/app/client/src/constants/AppsmithActionConstants/ActionConstants.tsx b/app/client/src/constants/AppsmithActionConstants/ActionConstants.tsx index f598f1ee3f..4f6d93df8c 100644 --- a/app/client/src/constants/AppsmithActionConstants/ActionConstants.tsx +++ b/app/client/src/constants/AppsmithActionConstants/ActionConstants.tsx @@ -118,7 +118,8 @@ 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 EXECUTION_PARAM_REFERENCE_REGEX = /this.params|this\?.params/g; +export const THIS_DOT_PARAMS_KEY = "params"; export const RESP_HEADER_DATATYPE = "X-APPSMITH-DATATYPE"; export const API_REQUEST_HEADERS: APIHeaders = { diff --git a/app/client/src/workers/DataTreeEvaluator.test.ts b/app/client/src/workers/DataTreeEvaluator.test.ts new file mode 100644 index 0000000000..77c158e0cf --- /dev/null +++ b/app/client/src/workers/DataTreeEvaluator.test.ts @@ -0,0 +1,109 @@ +import DataTreeEvaluator from "./DataTreeEvaluator"; + +describe("DataTreeEvaluator", () => { + let dataTreeEvaluator: DataTreeEvaluator; + beforeAll(() => { + dataTreeEvaluator = new DataTreeEvaluator({}); + }); + describe("evaluateActionBindings", () => { + it("handles this.params.property", () => { + const result = dataTreeEvaluator.evaluateActionBindings( + [ + "(function() { return this.params.property })()", + "(() => { return this.params.property })()", + 'this.params.property || "default value"', + 'this.params.property1 || "default value"', + ], + { + property: "my value", + }, + ); + expect(result).toStrictEqual([ + "my value", + "my value", + "my value", + "default value", + ]); + }); + + it("handles this?.params.property", () => { + const result = dataTreeEvaluator.evaluateActionBindings( + [ + "(() => { return this?.params.property })()", + "(function() { return this?.params.property })()", + 'this?.params.property || "default value"', + 'this?.params.property1 || "default value"', + ], + { + property: "my value", + }, + ); + expect(result).toStrictEqual([ + "my value", + "my value", + "my value", + "default value", + ]); + }); + + it("handles this?.params?.property", () => { + const result = dataTreeEvaluator.evaluateActionBindings( + [ + "(() => { return this?.params?.property })()", + "(function() { return this?.params?.property })()", + 'this?.params?.property || "default value"', + 'this?.params?.property1 || "default value"', + ], + { + property: "my value", + }, + ); + expect(result).toStrictEqual([ + "my value", + "my value", + "my value", + "default value", + ]); + }); + + it("handles executionParams.property", () => { + const result = dataTreeEvaluator.evaluateActionBindings( + [ + "(function() { return executionParams.property })()", + "(() => { return executionParams.property })()", + 'executionParams.property || "default value"', + 'executionParams.property1 || "default value"', + ], + { + property: "my value", + }, + ); + expect(result).toStrictEqual([ + "my value", + "my value", + "my value", + "default value", + ]); + }); + + it("handles executionParams?.property", () => { + const result = dataTreeEvaluator.evaluateActionBindings( + [ + "(function() { return executionParams?.property })()", + "(() => { return executionParams?.property })()", + 'executionParams?.property || "default value"', + 'executionParams?.property1 || "default value"', + ], + { + property: "my value", + }, + ); + expect(result).toStrictEqual([ + "my value", + "my value", + "my value", + "default value", + ]); + }); + }); +}); diff --git a/app/client/src/workers/DataTreeEvaluator.ts b/app/client/src/workers/DataTreeEvaluator.ts index 51313b71c5..22a57ce6d8 100644 --- a/app/client/src/workers/DataTreeEvaluator.ts +++ b/app/client/src/workers/DataTreeEvaluator.ts @@ -55,11 +55,13 @@ import toposort from "toposort"; import { EXECUTION_PARAM_KEY, EXECUTION_PARAM_REFERENCE_REGEX, + THIS_DOT_PARAMS_KEY, } from "constants/AppsmithActionConstants/ActionConstants"; import { DATA_BIND_REGEX } from "constants/BindingsConstants"; import evaluateSync, { createGlobalData, EvalResult, + EvaluateContext, EvaluationScriptType, getScriptToEval, evaluateAsync, @@ -609,12 +611,19 @@ export default class DataTreeEvaluator { entity.bindingPaths[propertyPath] || EvaluationSubstitutionType.TEMPLATE; + const contextData: EvaluateContext = {}; + if (isAction(entity)) { + contextData.thisContext = { + params: {}, + }; + } try { evalPropertyValue = this.getDynamicValue( unEvalPropertyValue, currentTree, resolvedFunctions, evaluationSubstitutionType, + contextData, undefined, fullPropertyPath, ); @@ -764,6 +773,7 @@ export default class DataTreeEvaluator { data: DataTree, resolvedFunctions: Record, evaluationSubstitutionType: EvaluationSubstitutionType, + contextData?: EvaluateContext, callBackData?: Array, fullPropertyPath?: string, ) { @@ -792,6 +802,7 @@ export default class DataTreeEvaluator { toBeSentForEval, data, resolvedFunctions, + contextData, callBackData, ); if (fullPropertyPath && result.errors.length) { @@ -864,10 +875,17 @@ export default class DataTreeEvaluator { js: string, data: DataTree, resolvedFunctions: Record, + contextData?: EvaluateContext, callbackData?: Array, ): EvalResult { try { - return evaluateSync(js, data, resolvedFunctions, callbackData); + return evaluateSync( + js, + data, + resolvedFunctions, + contextData, + callbackData, + ); } catch (e) { return { result: undefined, @@ -1555,24 +1573,26 @@ 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) => - this.getDynamicValue( - `{{${binding}}}`, - dataTreeWithExecutionParams, + return bindings.map((binding) => { + // Replace any reference of 'this.params' to 'executionParams' (backwards compatibility) + // also helps with dealing with IIFE which are normal functions (not arrow) + // because normal functions won't retain 'this' context (when executed elsewhere) + const replacedBinding = binding.replace( + EXECUTION_PARAM_REFERENCE_REGEX, + EXECUTION_PARAM_KEY, + ); + return this.getDynamicValue( + `{{${replacedBinding}}}`, + this.evalTree, this.resolvedFunctions, EvaluationSubstitutionType.TEMPLATE, - ), - ); + // params can be accessed via "this.params" or "executionParams" + { + thisContext: { [THIS_DOT_PARAMS_KEY]: evaluatedExecutionParams }, + globalContext: { [EXECUTION_PARAM_KEY]: evaluatedExecutionParams }, + }, + ); + }); } clearErrors() { diff --git a/app/client/src/workers/evaluate.test.ts b/app/client/src/workers/evaluate.test.ts index a34cbcf116..3e1963b772 100644 --- a/app/client/src/workers/evaluate.test.ts +++ b/app/client/src/workers/evaluate.test.ts @@ -30,6 +30,9 @@ describe("evaluateSync", () => { triggerPaths: {}, validationPaths: {}, logBlackList: {}, + overridingPropertyPaths: {}, + privateWidgets: {}, + propertyOverrideDependency: {}, }; const dataTree: DataTree = { Input1: widget, @@ -75,7 +78,7 @@ describe("evaluateSync", () => { const result = wrongJS return result; } - closedFunction() + closedFunction.call(THIS_CONTEXT) `, severity: "error", originalBinding: "wrongJS", @@ -89,7 +92,7 @@ describe("evaluateSync", () => { const result = wrongJS return result; } - closedFunction() + closedFunction.call(THIS_CONTEXT) `, severity: "error", originalBinding: "wrongJS", @@ -108,7 +111,7 @@ describe("evaluateSync", () => { const result = {}.map() return result; } - closedFunction() + closedFunction.call(THIS_CONTEXT) `, severity: "error", originalBinding: "{}.map()", @@ -135,7 +138,7 @@ describe("evaluateSync", () => { const result = setTimeout(() => {}, 100) return result; } - closedFunction() + closedFunction.call(THIS_CONTEXT) `, severity: "error", originalBinding: "setTimeout(() => {}, 100)", @@ -151,7 +154,7 @@ describe("evaluateSync", () => { 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("handles EXPRESSIONS with new lines", () => { @@ -165,22 +168,38 @@ describe("evaluateSync", () => { }); it("handles TRIGGERS with new lines", () => { let js = "\n"; - let response = evaluate(js, dataTree, {}, undefined); + let response = evaluate(js, dataTree, {}, undefined, undefined); expect(response.errors.length).toBe(0); js = "\n\n\n"; - response = evaluate(js, dataTree, {}, undefined); + response = evaluate(js, dataTree, {}, undefined, undefined); expect(response.errors.length).toBe(0); }); it("handles ANONYMOUS_FUNCTION with new lines", () => { let js = "\n"; - let response = evaluate(js, dataTree, {}, undefined); + let response = evaluate(js, dataTree, {}, undefined, undefined); expect(response.errors.length).toBe(0); js = "\n\n\n"; - response = evaluate(js, dataTree, {}, undefined); + response = evaluate(js, dataTree, {}, undefined, undefined); expect(response.errors.length).toBe(0); }); + 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); + }); }); describe("evaluateAsync", () => { @@ -256,7 +275,7 @@ describe("isFunctionAsync", () => { if (typeof testFunc === "string") { testFunc = eval(testFunc); } - const actual = isFunctionAsync(testFunc, {}); + const actual = isFunctionAsync(testFunc, {}, {}); expect(actual).toBe(testCase.expected); } }); diff --git a/app/client/src/workers/evaluate.ts b/app/client/src/workers/evaluate.ts index 521080e987..ed023a6b1f 100644 --- a/app/client/src/workers/evaluate.ts +++ b/app/client/src/workers/evaluate.ts @@ -34,12 +34,12 @@ export const EvaluationScripts: Record = { 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}) @@ -49,7 +49,7 @@ export const EvaluationScripts: Record = { const result = await ${ScriptTemplate}; return result; } - closedFunction(); + closedFunction.call(THIS_CONTEXT); `, }; @@ -102,6 +102,18 @@ export const createGlobalData = ( const GLOBAL_DATA: Record = {}; ///// 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; + }); + } + } //// Add internal functions to dataTree; const dataTreeWithFunctions = enhanceDataTreeWithFunctions( dataTree, @@ -134,9 +146,13 @@ export function sanitizeScript(js: string) { } /** Define a context just for this script + * thisContext will define it on the `this` + * globalContext will define it globally * requestId is used for completing promises */ export type EvaluateContext = { + thisContext?: Record; + globalContext?: Record; requestId?: string; }; @@ -172,6 +188,7 @@ export default function evaluateSync( userScript: string, dataTree: DataTree, resolvedFunctions: Record, + context?: EvaluateContext, evalArguments?: Array, ): EvalResult { return (function() { @@ -181,7 +198,7 @@ export default function evaluateSync( const GLOBAL_DATA: Record = createGlobalData( dataTree, resolvedFunctions, - undefined, + context, evalArguments, ); GLOBAL_DATA.ALLOW_ASYNC = false; diff --git a/app/client/src/workers/validations.ts b/app/client/src/workers/validations.ts index 543394faef..890af53651 100644 --- a/app/client/src/workers/validations.ts +++ b/app/client/src/workers/validations.ts @@ -799,7 +799,7 @@ export const VALIDATORS: Record = { }; if (config.params?.fnString && isString(config.params?.fnString)) { try { - const { result } = evaluate(config.params.fnString, {}, {}, [ + const { result } = evaluate(config.params.fnString, {}, {}, undefined, [ value, props, _,