fix: improve error message and performance in JS functions (#19137)

## Description


- Added logic to replace async function undefined error with
"{{actionName}} cannot be used in this field".
- This change improves performance for 
  - ParseJSActions
  - Triggers execution
  - Each Appsmith framework action execution.
- This change adds all platform functions to evalContext permanently.

Fixes #12179
Fixes #13273

Internal discussion for error message :-
https://theappsmith.slack.com/archives/C02K0SZQ7V3/p1667457021297869?thread_ts=1667385039.225229&cid=C02K0SZQ7V3

## Type of change

- Bug fix (non-breaking change which fixes an issue)
- Performance improvement


## How Has This Been Tested?

- Manual
- Jest
- Cypress

### Test Plan

- [ ] https://github.com/appsmithorg/TestSmith/issues/2086

### Issues raised during DP testing
> Link issues raised during DP testing for better visiblity and tracking
(copy link from comments dropped on this PR)


## Checklist:
### Dev activity
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] PR is being merged under a feature flag


### QA activity:
- [ ] Test plan has been approved by relevant developers
- [ ] Test plan has been peer reviewed by QA
- [ ] Cypress test cases have been added and approved by either SDET or
manual QA
- [ ] Organized project review call with relevant stakeholders after
Round 1/2 of QA
- [ ] Added Test Plan Approved label after reveiwing all Cypress test

Co-authored-by: Aishwarya UR <aishwarya@appsmith.com>
This commit is contained in:
Rishabh Rathod 2022-12-23 15:34:39 +05:30 committed by GitHub
parent bfd242f627
commit 6b751d914e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 960 additions and 372 deletions

View File

@ -11,6 +11,9 @@ describe("Dynamic input autocomplete", () => {
cy.wait(3000);
cy.selectEntityByName("Aditya");
cy.openPropertyPane("buttonwidget");
cy.testJsontext("label", "", {
parseSpecialCharSequences: true,
});
cy.get(dynamicInputLocators.input)
.first()
.click({ force: true })
@ -27,22 +30,29 @@ describe("Dynamic input autocomplete", () => {
// Tests if autocomplete will open
cy.get(dynamicInputLocators.hints).should("exist");
// Tests if data tree entities are sorted
cy.get(`${dynamicInputLocators.hints} li`)
.eq(1)
.should("have.text", "Button1.text");
cy.testJsontext("label", "", {
parseSpecialCharSequences: true,
});
// Tests if "No suggestions" message will pop if you type any garbage
cy.get(dynamicInputLocators.input)
.first()
.click({ force: true })
.type("{uparrow}", { parseSpecialCharSequences: true })
.type("{ctrl}{shift}{downarrow}", { parseSpecialCharSequences: true })
.type("{{ garbage", {
parseSpecialCharSequences: true,
})
.type("{backspace}", { parseSpecialCharSequences: true })
.then(() => {
cy.get(dynamicInputLocators.input)
.first()
.click({ force: true })
.type("{{garbage", {
parseSpecialCharSequences: true,
});
cy.get(".CodeMirror-Tern-tooltip").should(
"have.text",
"No suggestions",
@ -51,6 +61,24 @@ describe("Dynamic input autocomplete", () => {
});
cy.evaluateErrorMessage("ReferenceError: garbage is not defined");
});
it("test if action inside non event field throws error", () => {
cy.get(dynamicInputLocators.input)
.first()
.click({ force: true })
.type("{backspace}".repeat(12))
.type("{{storeValue()}}", { parseSpecialCharSequences: false });
cy.wait(1000);
cy.evaluateErrorMessage(
"Found a reference to {{actionName}} during evaluation. Sync fields cannot execute framework actions. Please remove any direct/indirect references to {{actionName}} and try again.".replaceAll(
"{{actionName}}",
"storeValue()",
),
);
});
it("opens current value popup", () => {
// Test on api pane
cy.NavigateToAPI_Panel();

View File

@ -21,6 +21,8 @@ export enum ActionTriggerType {
STOP_WATCHING_CURRENT_LOCATION = "STOP_WATCHING_CURRENT_LOCATION",
CONFIRMATION_MODAL = "CONFIRMATION_MODAL",
POST_MESSAGE = "POST_MESSAGE",
SET_TIMEOUT = "SET_TIMEOUT",
CLEAR_TIMEOUT = "CLEAR_TIMEOUT",
}
export const ActionTriggerFunctionNames: Record<ActionTriggerType, string> = {
@ -43,6 +45,8 @@ export const ActionTriggerFunctionNames: Record<ActionTriggerType, string> = {
[ActionTriggerType.STOP_WATCHING_CURRENT_LOCATION]: "stopWatch",
[ActionTriggerType.CONFIRMATION_MODAL]: "ConfirmationModal",
[ActionTriggerType.POST_MESSAGE]: "postWindowMessage",
[ActionTriggerType.SET_TIMEOUT]: "setTimeout",
[ActionTriggerType.CLEAR_TIMEOUT]: "clearTimeout",
};
export type RunPluginActionDescription = {

View File

@ -1,16 +1,19 @@
/* eslint-disable @typescript-eslint/ban-types */
import { DataTree, DataTreeEntity } from "entities/DataTree/dataTreeFactory";
import _ from "lodash";
import _, { set } from "lodash";
import {
ActionDescription,
ActionTriggerFunctionNames,
ActionTriggerType,
} from "@appsmith/entities/DataTree/actionTriggers";
import { NavigationTargetType } from "sagas/ActionExecution/NavigateActionSaga";
import { promisifyAction } from "workers/Evaluation/PromisifyAction";
import { klona } from "klona/full";
import uniqueId from "lodash/uniqueId";
import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
import { isAction, isAppsmithEntity, isTrueObject } from "./evaluationUtils";
import { EvalContext } from "workers/Evaluation/evaluate";
import { ActionCalledInSyncFieldError } from "workers/Evaluation/errorModifier";
declare global {
/** All identifiers added to the worker global scope should also
* be included in the DEDICATED_WORKER_GLOBAL_SCOPE_IDENTIFIERS in
@ -37,14 +40,9 @@ type ActionDispatcherWithExecutionType = (
...args: any[]
) => ActionDescriptionWithExecutionType;
export const DATA_TREE_FUNCTIONS: Record<
export const PLATFORM_FUNCTIONS: Record<
string,
| ActionDispatcherWithExecutionType
| {
qualifier: (entity: DataTreeEntity) => boolean;
func: (entity: DataTreeEntity) => ActionDispatcherWithExecutionType;
path?: string;
}
ActionDispatcherWithExecutionType
> = {
navigateTo: function(
pageNameOrUrl: string,
@ -136,6 +134,51 @@ export const DATA_TREE_FUNCTIONS: Record<
executionType: ExecutionType.PROMISE,
};
},
setInterval: function(callback: Function, interval: number, id?: string) {
return {
type: ActionTriggerType.SET_INTERVAL,
payload: {
callback: callback?.toString(),
interval,
id,
},
executionType: ExecutionType.TRIGGER,
};
},
clearInterval: function(id: string) {
return {
type: ActionTriggerType.CLEAR_INTERVAL,
payload: {
id,
},
executionType: ExecutionType.TRIGGER,
};
},
postWindowMessage: function(
message: unknown,
source: string,
targetOrigin: string,
) {
return {
type: ActionTriggerType.POST_MESSAGE,
payload: {
message,
source,
targetOrigin,
},
executionType: ExecutionType.TRIGGER,
};
},
};
const ENTITY_FUNCTIONS: Record<
string,
{
qualifier: (entity: DataTreeEntity) => boolean;
func: (entity: DataTreeEntity) => ActionDispatcherWithExecutionType;
path?: string;
}
> = {
run: {
qualifier: (entity) => isAction(entity),
func: (entity) =>
@ -190,26 +233,6 @@ export const DATA_TREE_FUNCTIONS: Record<
};
},
},
setInterval: function(callback: Function, interval: number, id?: string) {
return {
type: ActionTriggerType.SET_INTERVAL,
payload: {
callback: callback.toString(),
interval,
id,
},
executionType: ExecutionType.TRIGGER,
};
},
clearInterval: function(id: string) {
return {
type: ActionTriggerType.CLEAR_INTERVAL,
payload: {
id,
},
executionType: ExecutionType.TRIGGER,
};
},
getGeoLocation: {
qualifier: (entity) => isAppsmithEntity(entity),
path: "appsmith.geolocation.getCurrentPosition",
@ -281,70 +304,86 @@ export const DATA_TREE_FUNCTIONS: Record<
};
},
},
postWindowMessage: function(
message: unknown,
source: string,
targetOrigin: string,
) {
return {
type: ActionTriggerType.POST_MESSAGE,
payload: {
message,
source,
targetOrigin,
},
executionType: ExecutionType.TRIGGER,
};
},
};
export const enhanceDataTreeWithFunctions = (
dataTree: Readonly<DataTree>,
// Whether not to add functions like "run", "clear" to entity
skipEntityFunctions = false,
eventType?: EventType,
): DataTree => {
const clonedDT = klona(dataTree);
const platformFunctionEntries = Object.entries(PLATFORM_FUNCTIONS);
const entityFunctionEntries = Object.entries(ENTITY_FUNCTIONS);
/**
* This method returns new dataTree with entity function and platform function
*/
export const addDataTreeToContext = (args: {
EVAL_CONTEXT: EvalContext;
dataTree: Readonly<DataTree>;
skipEntityFunctions?: boolean;
eventType?: EventType;
isTriggerBased: boolean;
}) => {
const {
dataTree,
EVAL_CONTEXT,
eventType,
isTriggerBased,
skipEntityFunctions = false,
} = args;
const dataTreeEntries = Object.entries(dataTree);
const entityFunctionCollection: Record<string, Record<string, Function>> = {};
self.TRIGGER_COLLECTOR = [];
Object.entries(DATA_TREE_FUNCTIONS).forEach(([name, funcOrFuncCreator]) => {
if (
typeof funcOrFuncCreator === "object" &&
"qualifier" in funcOrFuncCreator
) {
!skipEntityFunctions &&
Object.entries(dataTree).forEach(([entityName, entity]) => {
if (funcOrFuncCreator.qualifier(entity)) {
const func = funcOrFuncCreator.func(entity);
const funcName = `${funcOrFuncCreator.path ||
`${entityName}.${name}`}`;
_.set(
clonedDT,
funcName,
pusher.bind(
{
TRIGGER_COLLECTOR: self.TRIGGER_COLLECTOR,
EVENT_TYPE: eventType,
},
func,
),
);
}
});
} else {
_.set(
clonedDT,
name,
for (const [entityName, entity] of dataTreeEntries) {
EVAL_CONTEXT[entityName] = entity;
if (skipEntityFunctions || !isTriggerBased) continue;
for (const [functionName, funcCreator] of entityFunctionEntries) {
if (!funcCreator.qualifier(entity)) continue;
const func = funcCreator.func(entity);
const fullPath = `${funcCreator.path || `${entityName}.${functionName}`}`;
set(
entityFunctionCollection,
fullPath,
pusher.bind(
{
TRIGGER_COLLECTOR: self.TRIGGER_COLLECTOR,
EVENT_TYPE: eventType,
},
funcOrFuncCreator,
func,
),
);
}
});
}
return clonedDT;
// if eval is not trigger based i.e., sync eval then we skip adding entity and platform function to evalContext
if (!isTriggerBased) return;
for (const [entityName, funcObj] of Object.entries(
entityFunctionCollection,
)) {
EVAL_CONTEXT[entityName] = Object.assign({}, dataTree[entityName], funcObj);
}
};
export const addPlatformFunctionsToEvalContext = (context: any) => {
for (const [funcName, fn] of platformFunctionEntries) {
context[funcName] = pusher.bind({}, fn);
}
};
export const getAllAsyncFunctions = (dataTree: DataTree) => {
const asyncFunctionNameMap: Record<string, true> = {};
const dataTreeEntries = Object.entries(dataTree);
for (const [entityName, entity] of dataTreeEntries) {
for (const [functionName, funcCreator] of entityFunctionEntries) {
if (!funcCreator.qualifier(entity)) continue;
const fullPath = `${funcCreator.path || `${entityName}.${functionName}`}`;
asyncFunctionNameMap[fullPath] = true;
}
}
for (const [name] of platformFunctionEntries) {
asyncFunctionNameMap[name] = true;
}
return asyncFunctionNameMap;
};
/**
@ -363,13 +402,17 @@ export const enhanceDataTreeWithFunctions = (
* **/
export const pusher = function(
this: {
TRIGGER_COLLECTOR: ActionDescription[];
EVENT_TYPE?: EventType;
},
action: ActionDispatcherWithExecutionType,
...args: any[]
) {
const actionDescription = action(...args);
if (!self.ALLOW_ASYNC) {
self.IS_ASYNC = true;
const actionName = ActionTriggerFunctionNames[actionDescription.type];
throw new ActionCalledInSyncFieldError(actionName);
}
const { executionType, payload, type } = actionDescription;
const actionPayload = {
type,
@ -377,7 +420,7 @@ export const pusher = function(
} as ActionDescription;
if (executionType && executionType === ExecutionType.TRIGGER) {
this.TRIGGER_COLLECTOR.push(actionPayload);
self.TRIGGER_COLLECTOR.push(actionPayload);
} else {
return promisifyAction(actionPayload, this.EVENT_TYPE);
}

View File

@ -52,7 +52,7 @@ export function makeEntityConfigsAsObjProperties(
for (const entityName of Object.keys(dataTree)) {
const entityConfig = Object.getPrototypeOf(dataTree[entityName]) || {};
const entity = dataTree[entityName];
newDataTree[entityName] = { ...entityConfig, ...entity };
newDataTree[entityName] = Object.assign({}, entityConfig, entity);
}
const dataTreeToReturn = sanitizeDataTree
? JSON.parse(JSON.stringify(newDataTree))

View File

@ -70,7 +70,7 @@ const getOptions = (type?: string, subType?: string) => {
CONTEXT_MENU_ACTIONS.INTERCOM,
];
case PropertyEvaluationErrorType.PARSE:
return [CONTEXT_MENU_ACTIONS.SNIPPET];
return [CONTEXT_MENU_ACTIONS.DOCS, CONTEXT_MENU_ACTIONS.SNIPPET];
case PropertyEvaluationErrorType.LINT:
return [CONTEXT_MENU_ACTIONS.SNIPPET];
default:

View File

@ -4,7 +4,7 @@ import { EvalErrorTypes } from "utils/DynamicBindingUtils";
import { JSUpdate, ParsedJSSubAction } from "utils/JSPaneUtils";
import { isTypeOfFunction, parseJSObjectWithAST } from "@shared/ast";
import DataTreeEvaluator from "workers/common/DataTreeEvaluator";
import evaluateSync, { isFunctionAsync } from "workers/Evaluation/evaluate";
import evaluateSync from "workers/Evaluation/evaluate";
import {
DataTreeDiff,
DataTreeDiffEvent,
@ -15,6 +15,7 @@ import {
removeFunctionsAndVariableJSCollection,
updateJSCollectionInUnEvalTree,
} from "workers/Evaluation/JSObject/utils";
import { functionDeterminer } from "../functionDeterminer";
/**
* Here we update our unEvalTree according to the change in JSObject's body
@ -246,16 +247,19 @@ export function parseJSActions(
});
}
functionDeterminer.setupEval(
unEvalDataTree,
dataTreeEvalRef.resolvedFunctions,
);
Object.keys(jsUpdates).forEach((entityName) => {
const parsedBody = jsUpdates[entityName].parsedBody;
if (!parsedBody) return;
parsedBody.actions = parsedBody.actions.map((action) => {
return {
...action,
isAsync: isFunctionAsync(
isAsync: functionDeterminer.isFunctionAsync(
action.parsedFunction,
unEvalDataTree,
dataTreeEvalRef.resolvedFunctions,
dataTreeEvalRef.logs,
),
// parsedFunction - used only to determine if function is async
@ -263,6 +267,9 @@ export function parseJSActions(
} as ParsedJSSubAction;
});
});
functionDeterminer.setOffEval();
return { jsUpdates };
}

View File

@ -1,4 +1,4 @@
import { createGlobalData } from "workers/Evaluation/evaluate";
import { createEvaluationContext } from "workers/Evaluation/evaluate";
const ctx: Worker = self as any;
/*
@ -19,15 +19,6 @@ export const promisifyAction = (
actionDescription: ActionDescription,
eventType?: EventType,
) => {
if (!self.ALLOW_ASYNC) {
/**
* To figure out if any function (JS action) is async, we do a dry run so that we can know if the function
* is using an async action. We set an IS_ASYNC flag to later indicate that a promise was called.
* @link isFunctionAsync
* */
self.IS_ASYNC = true;
throw new Error("Async function called in a sync field");
}
return new Promise((resolve, reject) => {
// We create a new sub request id for each request going on so that we can resolve the correct one later on
const messageId = _.uniqueId(`${actionDescription.type}_`);
@ -61,7 +52,7 @@ export const promisifyAction = (
} else {
self.ALLOW_ASYNC = true;
// Reset the global data with the correct request id for this promise
const globalData = createGlobalData({
const evalContext = createEvaluationContext({
dataTree: dataTreeEvaluator.evalTree,
resolvedFunctions: dataTreeEvaluator.resolvedFunctions,
isTriggerBased: true,
@ -69,10 +60,8 @@ export const promisifyAction = (
eventType,
},
});
for (const entity in globalData) {
// @ts-expect-error: Types are not available
self[entity] = globalData[entity];
}
Object.assign(self, evalContext);
// Resolve or reject the promise
if (success) {

View File

@ -1,4 +1,5 @@
import { createGlobalData } from "./evaluate";
import { ActionCalledInSyncFieldError } from "./errorModifier";
import { createEvaluationContext } from "./evaluate";
import { dataTreeEvaluator } from "./handlers/evalTree";
export const _internalSetTimeout = self.setTimeout;
@ -11,9 +12,9 @@ export default function overrideTimeout() {
value: function(cb: (...args: any) => any, delay: number, ...args: any) {
if (!self.ALLOW_ASYNC) {
self.IS_ASYNC = true;
throw new Error("Async function called in a sync field");
throw new ActionCalledInSyncFieldError("setTimeout");
}
const globalData = createGlobalData({
const evalContext = createEvaluationContext({
dataTree: dataTreeEvaluator?.evalTree || {},
resolvedFunctions: dataTreeEvaluator?.resolvedFunctions || {},
isTriggerBased: true,
@ -21,7 +22,7 @@ export default function overrideTimeout() {
return _internalSetTimeout(
function(...args: any) {
self.ALLOW_ASYNC = true;
Object.assign(self, globalData);
Object.assign(self, evalContext);
cb(...args);
},
delay,
@ -34,6 +35,10 @@ export default function overrideTimeout() {
writable: true,
configurable: true,
value: function(timerId: number) {
if (!self.ALLOW_ASYNC) {
self.IS_ASYNC = true;
throw new ActionCalledInSyncFieldError("clearTimeout");
}
return _internalClearTimeout(timerId);
},
});

View File

@ -1,8 +1,16 @@
import { DataTree, ENTITY_TYPE } from "entities/DataTree/dataTreeFactory";
import { PluginType } from "entities/Action";
import { createGlobalData } from "workers/Evaluation/evaluate";
import {
createEvaluationContext,
EvalContext,
} from "workers/Evaluation/evaluate";
import uniqueId from "lodash/uniqueId";
import { MessageType } from "utils/MessageUtil";
import {
addDataTreeToContext,
addPlatformFunctionsToEvalContext,
} from "@appsmith/workers/Evaluation/Actions";
jest.mock("lodash/uniqueId");
describe("Add functions", () => {
@ -31,15 +39,15 @@ describe("Add functions", () => {
},
};
self.TRIGGER_COLLECTOR = [];
const dataTreeWithFunctions = createGlobalData({
const evalContext = createEvaluationContext({
dataTree,
resolvedFunctions: {},
isTriggerBased: true,
context: {
requestId: "EVAL_TRIGGER",
},
context: {},
});
addPlatformFunctionsToEvalContext(evalContext);
const messageCreator = (type: string, body: unknown) => ({
messageId: expect.stringContaining(type),
messageType: MessageType.REQUEST,
@ -58,9 +66,9 @@ describe("Add functions", () => {
const actionParams = { param1: "value1" };
// Old syntax works with functions
expect(
dataTreeWithFunctions.action1.run(onSuccess, onError, actionParams),
).toBe(undefined);
expect(evalContext.action1.run(onSuccess, onError, actionParams)).toBe(
undefined,
);
expect(self.TRIGGER_COLLECTOR).toHaveLength(1);
expect(self.TRIGGER_COLLECTOR[0]).toStrictEqual({
payload: {
@ -77,9 +85,9 @@ describe("Add functions", () => {
self.TRIGGER_COLLECTOR.pop();
// Old syntax works with one undefined value
expect(
dataTreeWithFunctions.action1.run(onSuccess, undefined, actionParams),
).toBe(undefined);
expect(evalContext.action1.run(onSuccess, undefined, actionParams)).toBe(
undefined,
);
expect(self.TRIGGER_COLLECTOR).toHaveLength(1);
expect(self.TRIGGER_COLLECTOR[0]).toStrictEqual({
payload: {
@ -95,9 +103,9 @@ describe("Add functions", () => {
self.TRIGGER_COLLECTOR.pop();
expect(
dataTreeWithFunctions.action1.run(undefined, onError, actionParams),
).toBe(undefined);
expect(evalContext.action1.run(undefined, onError, actionParams)).toBe(
undefined,
);
expect(self.TRIGGER_COLLECTOR).toHaveLength(1);
expect(self.TRIGGER_COLLECTOR[0]).toStrictEqual({
payload: {
@ -123,9 +131,9 @@ describe("Add functions", () => {
});
// Old syntax works with null values is treated as new syntax
expect(
dataTreeWithFunctions.action1.run(null, null, actionParams),
).resolves.toBe({ a: "b" });
expect(evalContext.action1.run(null, null, actionParams)).resolves.toBe({
a: "b",
});
expect(workerEventMock).lastCalledWith(
messageCreator("RUN_PLUGIN_ACTION", {
data: {
@ -143,7 +151,7 @@ describe("Add functions", () => {
// Old syntax works with undefined values is treated as new syntax
expect(
dataTreeWithFunctions.action1.run(undefined, undefined, actionParams),
evalContext.action1.run(undefined, undefined, actionParams),
).resolves.toBe({ a: "b" });
expect(workerEventMock).lastCalledWith(
messageCreator("RUN_PLUGIN_ACTION", {
@ -162,7 +170,7 @@ describe("Add functions", () => {
// new syntax works
expect(
dataTreeWithFunctions.action1
evalContext.action1
.run(actionParams)
.then(onSuccess)
.catch(onError),
@ -182,7 +190,7 @@ describe("Add functions", () => {
}),
);
// New syntax without params
expect(dataTreeWithFunctions.action1.run()).resolves.toBe({ a: "b" });
expect(evalContext.action1.run()).resolves.toBe({ a: "b" });
expect(workerEventMock).lastCalledWith(
messageCreator("RUN_PLUGIN_ACTION", {
@ -201,7 +209,7 @@ describe("Add functions", () => {
});
it("action.clear works", () => {
expect(dataTreeWithFunctions.action1.clear()).resolves.toBe({});
expect(evalContext.action1.clear()).resolves.toBe({});
expect(workerEventMock).lastCalledWith(
messageCreator("CLEAR_PLUGIN_ACTION", {
data: {
@ -223,9 +231,9 @@ describe("Add functions", () => {
const params = "{ param1: value1 }";
const target = "NEW_WINDOW";
expect(
dataTreeWithFunctions.navigateTo(pageNameOrUrl, params, target),
).resolves.toBe({});
expect(evalContext.navigateTo(pageNameOrUrl, params, target)).resolves.toBe(
{},
);
expect(workerEventMock).lastCalledWith(
messageCreator("NAVIGATE_TO", {
data: {
@ -247,7 +255,7 @@ describe("Add functions", () => {
it("showAlert works", () => {
const message = "Alert message";
const style = "info";
expect(dataTreeWithFunctions.showAlert(message, style)).resolves.toBe({});
expect(evalContext.showAlert(message, style)).resolves.toBe({});
expect(workerEventMock).lastCalledWith(
messageCreator("SHOW_ALERT", {
data: {
@ -268,7 +276,7 @@ describe("Add functions", () => {
it("showModal works", () => {
const modalName = "Modal 1";
expect(dataTreeWithFunctions.showModal(modalName)).resolves.toBe({});
expect(evalContext.showModal(modalName)).resolves.toBe({});
expect(workerEventMock).lastCalledWith(
messageCreator("SHOW_MODAL_BY_NAME", {
data: {
@ -287,7 +295,7 @@ describe("Add functions", () => {
it("closeModal works", () => {
const modalName = "Modal 1";
expect(dataTreeWithFunctions.closeModal(modalName)).resolves.toBe({});
expect(evalContext.closeModal(modalName)).resolves.toBe({});
expect(workerEventMock).lastCalledWith(
messageCreator("CLOSE_MODAL", {
data: {
@ -313,9 +321,7 @@ describe("Add functions", () => {
// @ts-expect-error: mockReturnValueOnce is not available on uniqueId
uniqueId.mockReturnValueOnce(uniqueActionRequestId);
expect(dataTreeWithFunctions.storeValue(key, value, persist)).resolves.toBe(
{},
);
expect(evalContext.storeValue(key, value, persist)).resolves.toBe({});
expect(workerEventMock).lastCalledWith(
messageCreator("STORE_VALUE", {
data: {
@ -337,7 +343,7 @@ describe("Add functions", () => {
it("removeValue works", () => {
const key = "some";
expect(dataTreeWithFunctions.removeValue(key)).resolves.toBe({});
expect(evalContext.removeValue(key)).resolves.toBe({});
expect(workerEventMock).lastCalledWith(
messageCreator("REMOVE_VALUE", {
data: {
@ -355,7 +361,7 @@ describe("Add functions", () => {
});
it("clearStore works", () => {
expect(dataTreeWithFunctions.clearStore()).resolves.toBe({});
expect(evalContext.clearStore()).resolves.toBe({});
expect(workerEventMock).lastCalledWith(
messageCreator("CLEAR_STORE", {
data: {
@ -375,7 +381,7 @@ describe("Add functions", () => {
const name = "downloadedFile.txt";
const type = "text";
expect(dataTreeWithFunctions.download(data, name, type)).resolves.toBe({});
expect(evalContext.download(data, name, type)).resolves.toBe({});
expect(workerEventMock).lastCalledWith(
messageCreator("DOWNLOAD", {
data: {
@ -396,7 +402,7 @@ describe("Add functions", () => {
it("copyToClipboard works", () => {
const data = "file";
expect(dataTreeWithFunctions.copyToClipboard(data)).resolves.toBe({});
expect(evalContext.copyToClipboard(data)).resolves.toBe({});
expect(workerEventMock).lastCalledWith(
messageCreator("COPY_TO_CLIPBOARD", {
data: {
@ -418,9 +424,9 @@ describe("Add functions", () => {
const widgetName = "widget1";
const resetChildren = true;
expect(
dataTreeWithFunctions.resetWidget(widgetName, resetChildren),
).resolves.toBe({});
expect(evalContext.resetWidget(widgetName, resetChildren)).resolves.toBe(
{},
);
expect(workerEventMock).lastCalledWith(
messageCreator("RESET_WIDGET_META_RECURSIVE_BY_NAME", {
data: {
@ -443,9 +449,7 @@ describe("Add functions", () => {
const interval = 5000;
const id = "myInterval";
expect(dataTreeWithFunctions.setInterval(callback, interval, id)).toBe(
undefined,
);
expect(evalContext.setInterval(callback, interval, id)).toBe(undefined);
expect(self.TRIGGER_COLLECTOR).toEqual(
expect.arrayContaining([
expect.objectContaining({
@ -463,7 +467,7 @@ describe("Add functions", () => {
it("clearInterval works", () => {
const id = "myInterval";
expect(dataTreeWithFunctions.clearInterval(id)).toBe(undefined);
expect(evalContext.clearInterval(id)).toBe(undefined);
expect(self.TRIGGER_COLLECTOR).toEqual(
expect.arrayContaining([
expect.objectContaining({
@ -483,9 +487,9 @@ describe("Add functions", () => {
it("Post message with first argument (message) as a string", () => {
const message = "Hello world!";
expect(
dataTreeWithFunctions.postWindowMessage(message, source, targetOrigin),
).toBe(undefined);
expect(evalContext.postWindowMessage(message, source, targetOrigin)).toBe(
undefined,
);
expect(self.TRIGGER_COLLECTOR).toEqual(
expect.arrayContaining([
@ -504,9 +508,9 @@ describe("Add functions", () => {
it("Post message with first argument (message) as undefined", () => {
const message = undefined;
expect(
dataTreeWithFunctions.postWindowMessage(message, source, targetOrigin),
).toBe(undefined);
expect(evalContext.postWindowMessage(message, source, targetOrigin)).toBe(
undefined,
);
expect(self.TRIGGER_COLLECTOR).toEqual(
expect.arrayContaining([
@ -525,9 +529,9 @@ describe("Add functions", () => {
it("Post message with first argument (message) as null", () => {
const message = null;
expect(
dataTreeWithFunctions.postWindowMessage(message, source, targetOrigin),
).toBe(undefined);
expect(evalContext.postWindowMessage(message, source, targetOrigin)).toBe(
undefined,
);
expect(self.TRIGGER_COLLECTOR).toEqual(
expect.arrayContaining([
@ -546,9 +550,9 @@ describe("Add functions", () => {
it("Post message with first argument (message) as a number", () => {
const message = 1826;
expect(
dataTreeWithFunctions.postWindowMessage(message, source, targetOrigin),
).toBe(undefined);
expect(evalContext.postWindowMessage(message, source, targetOrigin)).toBe(
undefined,
);
expect(self.TRIGGER_COLLECTOR).toEqual(
expect.arrayContaining([
@ -567,9 +571,9 @@ describe("Add functions", () => {
it("Post message with first argument (message) as a boolean", () => {
const message = true;
expect(
dataTreeWithFunctions.postWindowMessage(message, source, targetOrigin),
).toBe(undefined);
expect(evalContext.postWindowMessage(message, source, targetOrigin)).toBe(
undefined,
);
expect(self.TRIGGER_COLLECTOR).toEqual(
expect.arrayContaining([
@ -588,9 +592,9 @@ describe("Add functions", () => {
it("Post message with first argument (message) as an array", () => {
const message = [1, 2, 3, [1, 2, 3, [1, 2, 3]]];
expect(
dataTreeWithFunctions.postWindowMessage(message, source, targetOrigin),
).toBe(undefined);
expect(evalContext.postWindowMessage(message, source, targetOrigin)).toBe(
undefined,
);
expect(self.TRIGGER_COLLECTOR).toEqual(
expect.arrayContaining([
@ -616,9 +620,9 @@ describe("Add functions", () => {
randomArr: [1, 2, 3],
};
expect(
dataTreeWithFunctions.postWindowMessage(message, source, targetOrigin),
).toBe(undefined);
expect(evalContext.postWindowMessage(message, source, targetOrigin)).toBe(
undefined,
);
expect(self.TRIGGER_COLLECTOR).toEqual(
expect.arrayContaining([
@ -642,3 +646,253 @@ describe("Add functions", () => {
});
});
});
const dataTree = {
Text1: {
widgetName: "Text1",
displayName: "Text",
type: "TEXT_WIDGET",
hideCard: false,
animateLoading: true,
overflow: "SCROLL",
fontFamily: "Nunito Sans",
parentColumnSpace: 15.0625,
dynamicTriggerPathList: [],
leftColumn: 4,
dynamicBindingPathList: [],
shouldTruncate: false,
text: '"2022-11-27T21:36:00.128Z"',
key: "gt93hhlp15",
isDeprecated: false,
rightColumn: 29,
textAlign: "LEFT",
dynamicHeight: "FIXED",
widgetId: "ajg9fjegvr",
isVisible: true,
fontStyle: "BOLD",
textColor: "#231F20",
version: 1,
parentId: "0",
renderMode: "CANVAS",
isLoading: false,
borderRadius: "0.375rem",
maxDynamicHeight: 9000,
fontSize: "1rem",
minDynamicHeight: 4,
value: '"2022-11-27T21:36:00.128Z"',
defaultProps: {},
defaultMetaProps: [],
logBlackList: {
value: true,
},
meta: {},
propertyOverrideDependency: {},
overridingPropertyPaths: {},
bindingPaths: {
text: "TEMPLATE",
isVisible: "TEMPLATE",
},
reactivePaths: {
value: "TEMPLATE",
fontFamily: "TEMPLATE",
},
triggerPaths: {},
validationPaths: {},
ENTITY_TYPE: "WIDGET",
privateWidgets: {},
__evaluation__: {
errors: {},
evaluatedValues: {},
},
backgroundColor: "",
borderColor: "",
},
pageList: [
{
pageName: "Page1",
pageId: "63349fb5d39f215f89b8245e",
isDefault: false,
isHidden: false,
slug: "page1",
},
{
pageName: "Page2",
pageId: "637cc6b4a3664a7fe679b7b0",
isDefault: true,
isHidden: false,
slug: "page2",
},
],
appsmith: {
store: {},
geolocation: {
canBeRequested: true,
currentPosition: {},
},
mode: "EDIT",
ENTITY_TYPE: "APPSMITH",
},
Api2: {
run: {},
clear: {},
name: "Api2",
pluginType: "API",
config: {},
dynamicBindingPathList: [
{
key: "config.path",
},
],
responseMeta: {
isExecutionSuccess: false,
},
ENTITY_TYPE: "ACTION",
isLoading: false,
bindingPaths: {
"config.path": "TEMPLATE",
"config.body": "SMART_SUBSTITUTE",
"config.pluginSpecifiedTemplates[1].value": "SMART_SUBSTITUTE",
"config.pluginSpecifiedTemplates[2].value.limitBased.limit.value":
"SMART_SUBSTITUTE",
},
reactivePaths: {
data: "TEMPLATE",
isLoading: "TEMPLATE",
datasourceUrl: "TEMPLATE",
"config.path": "TEMPLATE",
"config.body": "SMART_SUBSTITUTE",
"config.pluginSpecifiedTemplates[1].value": "SMART_SUBSTITUTE",
},
dependencyMap: {
"config.body": ["config.pluginSpecifiedTemplates[0].value"],
},
logBlackList: {},
datasourceUrl: "",
__evaluation__: {
errors: {
config: [],
},
evaluatedValues: {
"config.path": "/users/undefined",
config: {
timeoutInMillisecond: 10000,
paginationType: "NONE",
path: "/users/test",
headers: [],
encodeParamsToggle: true,
queryParameters: [],
bodyFormData: [],
httpMethod: "GET",
selfReferencingDataPaths: [],
pluginSpecifiedTemplates: [
{
value: true,
},
],
formData: {
apiContentType: "none",
},
},
},
},
},
JSObject1: {
name: "JSObject1",
actionId: "637cda3b2f8e175c6f5269d5",
pluginType: "JS",
ENTITY_TYPE: "JSACTION",
body:
"export default {\n\tstoreTest2: () => {\n\t\tlet values = [\n\t\t\t\t\tstoreValue('val1', 'number 1'),\n\t\t\t\t\tstoreValue('val2', 'number 2'),\n\t\t\t\t\tstoreValue('val3', 'number 3'),\n\t\t\t\t\tstoreValue('val4', 'number 4')\n\t\t\t\t];\n\t\treturn Promise.all(values)\n\t\t\t.then(() => {\n\t\t\tshowAlert(JSON.stringify(appsmith.store))\n\t\t})\n\t\t\t.catch((err) => {\n\t\t\treturn showAlert('Could not store values in store ' + err.toString());\n\t\t})\n\t},\n\tnewFunction: function() {\n\t\tJSObject1.storeTest()\n\t}\n}",
meta: {
newFunction: {
arguments: [],
isAsync: false,
confirmBeforeExecute: false,
},
storeTest2: {
arguments: [],
isAsync: true,
confirmBeforeExecute: false,
},
},
bindingPaths: {
body: "SMART_SUBSTITUTE",
newFunction: "SMART_SUBSTITUTE",
storeTest2: "SMART_SUBSTITUTE",
},
reactivePaths: {
body: "SMART_SUBSTITUTE",
newFunction: "SMART_SUBSTITUTE",
storeTest2: "SMART_SUBSTITUTE",
},
dynamicBindingPathList: [
{
key: "body",
},
{
key: "newFunction",
},
{
key: "storeTest2",
},
],
variables: [],
dependencyMap: {
body: ["newFunction", "storeTest2"],
},
__evaluation__: {
errors: {
storeTest2: [],
newFunction: [],
body: [],
},
},
},
};
describe("Test addDataTreeToContext method", () => {
const evalContext: EvalContext = {};
beforeAll(() => {
addDataTreeToContext({
EVAL_CONTEXT: evalContext,
dataTree: (dataTree as unknown) as DataTree,
isTriggerBased: true,
});
addPlatformFunctionsToEvalContext(evalContext);
});
it("1. Assert platform actions are added", () => {
const frameworkActions = {
navigateTo: true,
showAlert: true,
showModal: true,
closeModal: true,
storeValue: true,
removeValue: true,
clearStore: true,
download: true,
copyToClipboard: true,
resetWidget: true,
setInterval: true,
clearInterval: true,
postWindowMessage: true,
};
for (const actionName of Object.keys(frameworkActions)) {
expect(evalContext).toHaveProperty(actionName);
expect(typeof evalContext[actionName]).toBe("function");
}
});
it("2. Assert Api has run and clear method", () => {
expect(evalContext.Api2).toHaveProperty("run");
expect(evalContext.Api2).toHaveProperty("clear");
expect(typeof evalContext.Api2.run).toBe("function");
expect(typeof evalContext.Api2.clear).toBe("function");
});
it("3. Assert input dataTree is not mutated", () => {
expect(typeof dataTree.Api2.run).not.toBe("function");
});
});

View File

@ -1,6 +1,7 @@
import { createGlobalData } from "workers/Evaluation/evaluate";
import { createEvaluationContext } from "workers/Evaluation/evaluate";
import _ from "lodash";
import { MessageType } from "utils/MessageUtil";
import { addPlatformFunctionsToEvalContext } from "ce/workers/Evaluation/Actions";
jest.mock("../handlers/evalTree", () => {
return {
dataTreeEvaluator: {
@ -13,13 +14,15 @@ jest.mock("../handlers/evalTree", () => {
describe("promise execution", () => {
const postMessageMock = jest.fn();
const requestId = _.uniqueId("TEST_REQUEST");
const dataTreeWithFunctions = createGlobalData({
const evalContext = createEvaluationContext({
dataTree: {},
resolvedFunctions: {},
isTriggerBased: true,
context: { requestId },
context: {},
});
addPlatformFunctionsToEvalContext(evalContext);
const requestMessageCreator = (type: string, body: unknown) => ({
messageId: expect.stringContaining(`${type}_`),
messageType: MessageType.REQUEST,
@ -37,12 +40,12 @@ describe("promise execution", () => {
it("throws when allow async is not enabled", () => {
self.ALLOW_ASYNC = false;
self.IS_ASYNC = false;
expect(dataTreeWithFunctions.showAlert).toThrowError();
expect(evalContext.showAlert).toThrowError();
expect(self.IS_ASYNC).toBe(true);
expect(postMessageMock).not.toHaveBeenCalled();
});
it("sends an event from the worker", () => {
dataTreeWithFunctions.showAlert("test alert", "info");
evalContext.showAlert("test alert", "info");
expect(postMessageMock).toBeCalledWith(
requestMessageCreator("SHOW_ALERT", {
data: {
@ -60,12 +63,8 @@ describe("promise execution", () => {
});
it("returns a promise that resolves", async () => {
postMessageMock.mockReset();
const returnedPromise = dataTreeWithFunctions.showAlert(
"test alert",
"info",
);
const returnedPromise = evalContext.showAlert("test alert", "info");
const requestArgs = postMessageMock.mock.calls[0][0];
console.log(requestArgs);
const messageId = requestArgs.messageId;
self.dispatchEvent(
@ -91,10 +90,7 @@ describe("promise execution", () => {
it("returns a promise that rejects", async () => {
postMessageMock.mockReset();
const returnedPromise = dataTreeWithFunctions.showAlert(
"test alert",
"info",
);
const returnedPromise = evalContext.showAlert("test alert", "info");
const requestArgs = postMessageMock.mock.calls[0][0];
self.dispatchEvent(
new MessageEvent("message", {
@ -113,10 +109,7 @@ describe("promise execution", () => {
});
it("does not process till right event is triggered", async () => {
postMessageMock.mockReset();
const returnedPromise = dataTreeWithFunctions.showAlert(
"test alert",
"info",
);
const returnedPromise = evalContext.showAlert("test alert", "info");
const requestArgs = postMessageMock.mock.calls[0][0];
const correctId = requestArgs.messageId;
@ -161,10 +154,7 @@ describe("promise execution", () => {
});
it("same subRequestId is not accepted again", async () => {
postMessageMock.mockReset();
const returnedPromise = dataTreeWithFunctions.showAlert(
"test alert",
"info",
);
const returnedPromise = evalContext.showAlert("test alert", "info");
const requestArgs = postMessageMock.mock.calls[0][0];
const messageId = requestArgs.messageId;

View File

@ -0,0 +1,163 @@
import { DataTree } from "entities/DataTree/dataTreeFactory";
import { errorModifier } from "../errorModifier";
describe("Test error modifier", () => {
const dataTree = ({
Api2: {
run: {},
clear: {},
name: "Api2",
pluginType: "API",
config: {},
dynamicBindingPathList: [
{
key: "config.path",
},
],
responseMeta: {
isExecutionSuccess: false,
},
ENTITY_TYPE: "ACTION",
isLoading: false,
bindingPaths: {
"config.path": "TEMPLATE",
"config.body": "SMART_SUBSTITUTE",
"config.pluginSpecifiedTemplates[1].value": "SMART_SUBSTITUTE",
"config.pluginSpecifiedTemplates[2].value.limitBased.limit.value":
"SMART_SUBSTITUTE",
},
reactivePaths: {
data: "TEMPLATE",
isLoading: "TEMPLATE",
datasourceUrl: "TEMPLATE",
"config.path": "TEMPLATE",
"config.body": "SMART_SUBSTITUTE",
"config.pluginSpecifiedTemplates[1].value": "SMART_SUBSTITUTE",
},
dependencyMap: {
"config.body": ["config.pluginSpecifiedTemplates[0].value"],
},
logBlackList: {},
datasourceUrl: "",
__evaluation__: {
errors: {
config: [],
},
evaluatedValues: {
"config.path": "/users/undefined",
config: {
timeoutInMillisecond: 10000,
paginationType: "NONE",
path: "/users/test",
headers: [],
encodeParamsToggle: true,
queryParameters: [],
bodyFormData: [],
httpMethod: "GET",
selfReferencingDataPaths: [],
pluginSpecifiedTemplates: [
{
value: true,
},
],
formData: {
apiContentType: "none",
},
},
},
},
},
JSObject1: {
name: "JSObject1",
actionId: "637cda3b2f8e175c6f5269d5",
pluginType: "JS",
ENTITY_TYPE: "JSACTION",
body:
"export default {\n\tstoreTest2: () => {\n\t\tlet values = [\n\t\t\t\t\tstoreValue('val1', 'number 1'),\n\t\t\t\t\tstoreValue('val2', 'number 2'),\n\t\t\t\t\tstoreValue('val3', 'number 3'),\n\t\t\t\t\tstoreValue('val4', 'number 4')\n\t\t\t\t];\n\t\treturn Promise.all(values)\n\t\t\t.then(() => {\n\t\t\tshowAlert(JSON.stringify(appsmith.store))\n\t\t})\n\t\t\t.catch((err) => {\n\t\t\treturn showAlert('Could not store values in store ' + err.toString());\n\t\t})\n\t},\n\tnewFunction: function() {\n\t\tJSObject1.storeTest()\n\t}\n}",
meta: {
newFunction: {
arguments: [],
isAsync: false,
confirmBeforeExecute: false,
},
storeTest2: {
arguments: [],
isAsync: true,
confirmBeforeExecute: false,
},
},
bindingPaths: {
body: "SMART_SUBSTITUTE",
newFunction: "SMART_SUBSTITUTE",
storeTest2: "SMART_SUBSTITUTE",
},
reactivePaths: {
body: "SMART_SUBSTITUTE",
newFunction: "SMART_SUBSTITUTE",
storeTest2: "SMART_SUBSTITUTE",
},
dynamicBindingPathList: [
{
key: "body",
},
{
key: "newFunction",
},
{
key: "storeTest2",
},
],
variables: [],
dependencyMap: {
body: ["newFunction", "storeTest2"],
},
__evaluation__: {
errors: {
storeTest2: [],
newFunction: [],
body: [],
},
},
},
} as unknown) as DataTree;
beforeAll(() => {
errorModifier.updateAsyncFunctions(dataTree);
});
it("TypeError for defined Api in sync field ", () => {
const error = new Error();
error.name = "TypeError";
error.message = "Api2.run is not a function";
const result = errorModifier.run(error);
expect(result).toEqual(
"Found a reference to Api2.run() during evaluation. Sync fields cannot execute framework actions. Please remove any direct/indirect references to Api2.run() and try again.",
);
});
it("TypeError for undefined Api in sync field ", () => {
const error = new Error();
error.name = "TypeError";
error.message = "Api1.run is not a function";
const result = errorModifier.run(error);
expect(result).toEqual("TypeError: Api1.run is not a function");
});
it("ReferenceError for platform function in sync field", () => {
const error = new Error();
error.name = "ReferenceError";
error.message = "storeValue is not defined";
const result = errorModifier.run(error);
expect(result).toEqual(
"Found a reference to storeValue() during evaluation. Sync fields cannot execute framework actions. Please remove any direct/indirect references to storeValue() and try again.",
);
});
it("ReferenceError for undefined function in sync field", () => {
const error = new Error();
error.name = "ReferenceError";
error.message = "storeValue2 is not defined";
const result = errorModifier.run(error);
expect(result).toEqual("ReferenceError: storeValue2 is not defined");
});
});

View File

@ -1,7 +1,4 @@
import evaluate, {
evaluateAsync,
isFunctionAsync,
} from "workers/Evaluation/evaluate";
import evaluate, { evaluateAsync } from "workers/Evaluation/evaluate";
import {
DataTree,
DataTreeWidget,
@ -9,6 +6,8 @@ import {
} from "entities/DataTree/dataTreeFactory";
import { RenderModes } from "constants/WidgetConstants";
import setupEvalEnv from "../handlers/setupEvalEnv";
import { addPlatformFunctionsToEvalContext } from "@appsmith/workers/Evaluation/Actions";
import { functionDeterminer } from "../functionDeterminer";
describe("evaluateSync", () => {
const widget: DataTreeWidget = {
@ -251,7 +250,11 @@ describe("isFunctionAsync", () => {
if (typeof testFunc === "string") {
testFunc = eval(testFunc);
}
const actual = isFunctionAsync(testFunc, {}, {});
functionDeterminer.setupEval({}, {});
addPlatformFunctionsToEvalContext(self);
const actual = functionDeterminer.isFunctionAsync(testFunc);
expect(actual).toBe(testCase.expected);
}
});

View File

@ -1,8 +1,8 @@
import { PluginType } from "entities/Action";
import { DataTree, ENTITY_TYPE } from "entities/DataTree/dataTreeFactory";
import { createGlobalData } from "../evaluate";
import "../TimeoutOverride";
import { createEvaluationContext } from "../evaluate";
import overrideTimeout from "../TimeoutOverride";
import { addPlatformFunctionsToEvalContext } from "@appsmith/workers/Evaluation/Actions";
describe("Expects appsmith setTimeout to pass the following criteria", () => {
overrideTimeout();
@ -107,13 +107,16 @@ describe("Expects appsmith setTimeout to pass the following criteria", () => {
},
};
self.ALLOW_ASYNC = true;
const dataTreeWithFunctions = createGlobalData({
const evalContext = createEvaluationContext({
dataTree,
resolvedFunctions: {},
isTriggerBased: true,
context: {},
});
setTimeout(() => dataTreeWithFunctions.action1.run(), 1000);
addPlatformFunctionsToEvalContext(evalContext);
setTimeout(() => evalContext.action1.run(), 1000);
jest.runAllTimers();
expect(self.postMessage).toBeCalled();
});

View File

@ -0,0 +1,76 @@
import { DataTree } from "entities/DataTree/dataTreeFactory";
import { getAllAsyncFunctions } from "@appsmith/workers/Evaluation/Actions";
const UNDEFINED_ACTION_IN_SYNC_EVAL_ERROR =
"Found a reference to {{actionName}} during evaluation. Sync fields cannot execute framework actions. Please remove any direct/indirect references to {{actionName}} and try again.";
const ErrorNameType = {
ReferenceError: "ReferenceError",
TypeError: "TypeError",
};
class ErrorModifier {
private errorNamesToScan = [
ErrorNameType.ReferenceError,
ErrorNameType.TypeError,
];
// Note all regex below groups the async function name
private asyncFunctionsNameMap: Record<string, true> = {};
updateAsyncFunctions(dataTree: DataTree) {
this.asyncFunctionsNameMap = getAllAsyncFunctions(dataTree);
}
run(error: Error) {
const errorMessage = getErrorMessage(error);
if (!this.errorNamesToScan.includes(error.name)) return errorMessage;
for (const asyncFunctionFullPath of Object.keys(
this.asyncFunctionsNameMap,
)) {
const functionNameWithWhiteSpace = " " + asyncFunctionFullPath + " ";
if (errorMessage.match(functionNameWithWhiteSpace)) {
return UNDEFINED_ACTION_IN_SYNC_EVAL_ERROR.replaceAll(
"{{actionName}}",
asyncFunctionFullPath + "()",
);
}
}
return errorMessage;
}
}
export const errorModifier = new ErrorModifier();
export class FoundPromiseInSyncEvalError extends Error {
constructor() {
super();
this.name = "";
this.message =
"Found a Promise() during evaluation. Sync fields cannot execute asynchronous code.";
}
}
export class ActionCalledInSyncFieldError extends Error {
constructor(actionName: string) {
super(actionName);
if (!actionName) {
this.message = "Async function called in a sync field";
return;
}
this.name = "";
this.message = UNDEFINED_ACTION_IN_SYNC_EVAL_ERROR.replaceAll(
"{{actionName}}",
actionName + "()",
);
}
}
export const getErrorMessage = (error: Error) => {
return error.name ? `${error.name}: ${error.message}` : error.message;
};

View File

@ -6,8 +6,6 @@ import {
} from "utils/DynamicBindingUtils";
import unescapeJS from "unescape-js";
import { LogObject, Severity } from "entities/AppsmithConsole";
import { enhanceDataTreeWithFunctions } from "@appsmith/workers/Evaluation/Actions";
import { isEmpty } from "lodash";
import { ActionDescription } from "@appsmith/entities/DataTree/actionTriggers";
import userLogs from "./UserLog";
import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
@ -15,6 +13,11 @@ import { TriggerMeta } from "@appsmith/sagas/ActionExecution/ActionExecutionSaga
import indirectEval from "./indirectEval";
import { DOM_APIS } from "./SetupDOM";
import { JSLibraries, libraryReservedIdentifiers } from "../common/JSLibrary";
import { errorModifier, FoundPromiseInSyncEvalError } from "./errorModifier";
import {
PLATFORM_FUNCTIONS,
addDataTreeToContext,
} from "@appsmith/workers/Evaluation/Actions";
export type EvalResult = {
result: any;
@ -78,6 +81,7 @@ function resetWorkerGlobalScope() {
continue;
if (JSLibraries.find((lib) => lib.accessor.includes(key))) continue;
if (libraryReservedIdentifiers[key]) continue;
if (PLATFORM_FUNCTIONS[key]) continue;
try {
// @ts-expect-error: Types are not available
delete self[key];
@ -115,17 +119,25 @@ export const getScriptToEval = (
};
const beginsWithLineBreakRegex = /^\s+|\s+$/;
export interface createGlobalDataArgs {
export type EvalContext = Record<string, any>;
type ResolvedFunctions = Record<string, any>;
export interface createEvaluationContextArgs {
dataTree: DataTree;
resolvedFunctions: Record<string, any>;
resolvedFunctions: ResolvedFunctions;
context?: EvaluateContext;
evalArguments?: Array<unknown>;
isTriggerBased: boolean;
evalArguments?: Array<unknown>;
// Whether not to add functions like "run", "clear" to entity in global data
skipEntityFunctions?: boolean;
}
export const createGlobalData = (args: createGlobalDataArgs) => {
/**
* This method created an object with dataTree and appsmith's framework actions that needs to be added to worker global scope for the JS code evaluation to then consume it.
*
* Example:
* - For `eval("Table1.tableData")` code to work as expected, we define Table1.tableData in worker global scope and for that we use `createEvaluationContext` to get the object to set in global scope.
*/
export const createEvaluationContext = (args: createEvaluationContextArgs) => {
const {
context,
dataTree,
@ -135,63 +147,54 @@ export const createGlobalData = (args: createGlobalDataArgs) => {
skipEntityFunctions,
} = args;
const GLOBAL_DATA: Record<string, any> = {};
const EVAL_CONTEXT: EvalContext = {};
///// Adding callback data
GLOBAL_DATA.ARGUMENTS = evalArguments;
EVAL_CONTEXT.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;
});
EVAL_CONTEXT.THIS_CONTEXT = context?.thisContext || {};
if (context?.globalContext) {
Object.assign(EVAL_CONTEXT, context.globalContext);
}
addDataTreeToContext({
EVAL_CONTEXT,
dataTree,
skipEntityFunctions: !!skipEntityFunctions,
eventType: context?.eventType,
isTriggerBased,
});
assignJSFunctionsToContext(EVAL_CONTEXT, resolvedFunctions);
return EVAL_CONTEXT;
};
export const assignJSFunctionsToContext = (
EVAL_CONTEXT: EvalContext,
resolvedFunctions: ResolvedFunctions,
) => {
const jsObjectNames = Object.keys(resolvedFunctions || {});
for (const jsObjectName of jsObjectNames) {
const resolvedObject = resolvedFunctions[jsObjectName];
const jsObject = EVAL_CONTEXT[jsObjectName];
const jsObjectFunction: Record<string, Record<"data", unknown>> = {};
if (!jsObject) continue;
for (const fnName of Object.keys(resolvedObject)) {
const fn = resolvedObject[fnName];
if (typeof fn !== "function") continue;
// Investigate promisify of JSObject function confirmation
// Task: https://github.com/appsmithorg/appsmith/issues/13289
// Previous implementation commented code: https://github.com/appsmithorg/appsmith/pull/18471
const data = jsObject[fnName]?.data;
jsObjectFunction[fnName] = fn;
if (!!data) {
jsObjectFunction[fnName]["data"] = data;
}
}
EVAL_CONTEXT[jsObjectName] = Object.assign({}, jsObject, jsObjectFunction);
}
if (isTriggerBased) {
//// Add internal functions to dataTree;
const dataTreeWithFunctions = enhanceDataTreeWithFunctions(
dataTree,
skipEntityFunctions,
context?.eventType,
);
///// Adding Data tree with functions
Object.assign(GLOBAL_DATA, dataTreeWithFunctions);
} else {
// Object.assign removes prototypes of the entity object making sure configs are not shown to user.
Object.assign(GLOBAL_DATA, dataTree);
}
if (!isEmpty(resolvedFunctions)) {
Object.keys(resolvedFunctions).forEach((datum: any) => {
const resolvedObject = resolvedFunctions[datum];
Object.keys(resolvedObject).forEach((key: any) => {
const dataTreeKey = GLOBAL_DATA[datum];
if (dataTreeKey) {
const data = dataTreeKey[key]?.data;
//do not remove we will be investigating this
//const isAsync = dataTreeKey?.meta[key]?.isAsync || false;
//const confirmBeforeExecute = dataTreeKey?.meta[key]?.confirmBeforeExecute || false;
dataTreeKey[key] = resolvedObject[key];
// if (isAsync && confirmBeforeExecute) {
// dataTreeKey[key] = confirmationPromise.bind(
// {},
// context?.requestId,
// resolvedObject[key],
// dataTreeKey.name + "." + key,
// );
// } else {
// dataTreeKey[key] = resolvedObject[key];
// }
if (!!data) {
dataTreeKey[key]["data"] = data;
}
}
});
});
}
return GLOBAL_DATA;
};
export function sanitizeScript(js: string) {
@ -252,19 +255,22 @@ export default function evaluateSync(
userLogs.resetLogs();
}
/**** Setting the eval context ****/
const GLOBAL_DATA: Record<string, any> = createGlobalData({
const evalContext: EvalContext = createEvaluationContext({
dataTree,
resolvedFunctions,
isTriggerBased: isJSCollection,
context,
evalArguments,
isTriggerBased: isJSCollection,
});
GLOBAL_DATA.ALLOW_ASYNC = false;
evalContext.ALLOW_ASYNC = false;
const { script } = getUserScriptToEvaluate(
userScript,
false,
evalArguments,
);
// If nothing is present to evaluate, return instead of evaluating
if (!script.length) {
return {
@ -277,19 +283,20 @@ export default function evaluateSync(
// 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
for (const entity in GLOBAL_DATA) {
// @ts-expect-error: Types are not available
self[entity] = GLOBAL_DATA[entity];
}
Object.assign(self, evalContext);
try {
result = indirectEval(script);
if (result instanceof Promise) {
/**
* If a promise is returned in sync field then show the error to help understand sync field doesn't await to resolve promise.
* NOTE: Awaiting for promise will make sync field evaluation slower.
*/
throw new FoundPromiseInSyncEvalError();
}
} catch (error) {
const errorMessage = `${(error as Error).name}: ${
(error as Error).message
}`;
errors.push({
errorMessage: errorMessage,
errorMessage: errorModifier.run(error as Error),
severity: Severity.ERROR,
raw: script,
errorType: PropertyEvaluationErrorType.PARSE,
@ -297,9 +304,11 @@ export default function evaluateSync(
});
} finally {
if (!skipLogsOperations) logs = userLogs.flushLogs();
for (const entity in GLOBAL_DATA) {
// @ts-expect-error: Types are not available
delete self[entity];
for (const entityName in evalContext) {
if (evalContext.hasOwnProperty(entityName)) {
// @ts-expect-error: Types are not available
delete self[entityName];
}
}
}
@ -325,22 +334,20 @@ export async function evaluateAsync(
eventType: context?.eventType,
triggerMeta: context?.triggerMeta,
});
const GLOBAL_DATA: Record<string, any> = createGlobalData({
const evalContext: EvalContext = createEvaluationContext({
dataTree,
resolvedFunctions,
isTriggerBased: true,
context,
evalArguments,
isTriggerBased: true,
});
const { script } = getUserScriptToEvaluate(userScript, true, evalArguments);
GLOBAL_DATA.ALLOW_ASYNC = true;
evalContext.ALLOW_ASYNC = true;
// 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) => {
// @ts-expect-error: Types are not available
self[key] = GLOBAL_DATA[key];
});
Object.assign(self, evalContext);
try {
result = await indirectEval(script);
@ -370,72 +377,3 @@ export async function evaluateAsync(
}
})();
}
export function isFunctionAsync(
userFunction: unknown,
dataTree: DataTree,
resolvedFunctions: Record<string, any>,
logs: unknown[] = [],
) {
return (function() {
/**** Setting the eval context ****/
const GLOBAL_DATA: Record<string, any> = {
ALLOW_ASYNC: false,
IS_ASYNC: false,
};
//// Add internal functions to dataTree;
const dataTreeWithFunctions = enhanceDataTreeWithFunctions(dataTree);
///// Adding Data tree with functions
Object.keys(dataTreeWithFunctions).forEach((datum) => {
GLOBAL_DATA[datum] = dataTreeWithFunctions[datum];
});
if (!isEmpty(resolvedFunctions)) {
Object.keys(resolvedFunctions).forEach((datum: any) => {
const resolvedObject = resolvedFunctions[datum];
Object.keys(resolvedObject).forEach((key: any) => {
const dataTreeKey = GLOBAL_DATA[datum];
if (dataTreeKey) {
const data = dataTreeKey[key]?.data;
dataTreeKey[key] = resolvedObject[key];
if (!!data) {
dataTreeKey[key].data = data;
}
}
});
});
}
// 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) => {
// @ts-expect-error: Types are not available
self[key] = GLOBAL_DATA[key];
});
try {
if (typeof userFunction === "function") {
if (userFunction.constructor.name === "AsyncFunction") {
// functions declared with an async keyword
self.IS_ASYNC = true;
} else {
const returnValue = userFunction();
if (!!returnValue && returnValue instanceof Promise) {
self.IS_ASYNC = true;
}
if (self.TRIGGER_COLLECTOR.length) {
self.IS_ASYNC = true;
}
}
}
} catch (e) {
// We do not want to throw errors for internal operations, to users.
// logLevel should help us in debugging this.
logs.push({ error: "Error when determining async function" + e });
}
const isAsync = !!self.IS_ASYNC;
for (const entity in GLOBAL_DATA) {
// @ts-expect-error: Types are not available
delete self[entity];
}
return isAsync;
})();
}

View File

@ -0,0 +1,72 @@
import { addDataTreeToContext } from "@appsmith/workers/Evaluation/Actions";
import { EvalContext, assignJSFunctionsToContext } from "./evaluate";
import { DataTree } from "entities/DataTree/dataTreeFactory";
class FunctionDeterminer {
private evalContext: EvalContext = {};
setupEval(dataTree: DataTree, resolvedFunctions: Record<string, any>) {
/**** Setting the eval context ****/
const evalContext: EvalContext = {
ALLOW_ASYNC: false,
IS_ASYNC: false,
};
addDataTreeToContext({
dataTree,
EVAL_CONTEXT: evalContext,
isTriggerBased: true,
});
assignJSFunctionsToContext(evalContext, resolvedFunctions);
// 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.assign(self, evalContext);
this.evalContext = evalContext;
}
setOffEval() {
for (const entityName in this.evalContext) {
if (this.evalContext.hasOwnProperty(entityName)) {
// @ts-expect-error: Types are not available
delete self[entityName];
}
}
}
isFunctionAsync(userFunction: unknown, logs: unknown[] = []) {
self.TRIGGER_COLLECTOR = [];
self.IS_ASYNC = false;
return (function() {
try {
if (typeof userFunction === "function") {
if (userFunction.constructor.name === "AsyncFunction") {
// functions declared with an async keyword
self.IS_ASYNC = true;
} else {
const returnValue = userFunction();
if (!!returnValue && returnValue instanceof Promise) {
self.IS_ASYNC = true;
}
if (self.TRIGGER_COLLECTOR.length) {
self.IS_ASYNC = true;
}
}
}
} catch (e) {
// We do not want to throw errors for internal operations, to users.
// logLevel should help us in debugging this.
logs.push({ error: "Error when determining async function" + e });
}
const isAsync = !!self.IS_ASYNC;
return isAsync;
})();
}
}
export const functionDeterminer = new FunctionDeterminer();

View File

@ -5,6 +5,7 @@ import setupDOM from "../SetupDOM";
import overrideTimeout from "../TimeoutOverride";
import { EvalWorkerSyncRequest } from "../types";
import userLogs from "../UserLog";
import { addPlatformFunctionsToEvalContext } from "@appsmith/workers/Evaluation/Actions";
export default function() {
const libraries = resetJSLibraries();
@ -24,6 +25,7 @@ export default function() {
overrideTimeout();
interceptAndOverrideHttpRequest();
setupDOM();
addPlatformFunctionsToEvalContext(self);
return true;
}

View File

@ -35,7 +35,7 @@ import {
import { getDynamicBindings } from "utils/DynamicBindingUtils";
import {
createGlobalData,
createEvaluationContext,
EvaluationScripts,
EvaluationScriptType,
getScriptToEval,
@ -53,17 +53,30 @@ import { LintErrors } from "reducers/lintingReducers/lintErrorsReducers";
import { Severity } from "entities/AppsmithConsole";
import { JSLibraries } from "workers/common/JSLibrary";
import { MessageType, sendMessage } from "utils/MessageUtil";
import { addPlatformFunctionsToEvalContext } from "@appsmith/workers/Evaluation/Actions";
export function getlintErrorsFromTree(
pathsToLint: string[],
unEvalTree: DataTree,
): LintErrors {
const lintTreeErrors: LintErrors = {};
const GLOBAL_DATA_WITHOUT_FUNCTIONS = createGlobalData({
const evalContext = createEvaluationContext({
dataTree: unEvalTree,
resolvedFunctions: {},
isTriggerBased: false,
skipEntityFunctions: true,
});
addPlatformFunctionsToEvalContext(evalContext);
const evalContextWithOutFunctions = createEvaluationContext({
dataTree: unEvalTree,
resolvedFunctions: {},
isTriggerBased: true,
skipEntityFunctions: true,
});
// trigger paths
const triggerPaths = new Set<string>();
// Certain paths, like JS Object's body are binding paths where appsmith functions are needed in the global data
@ -91,7 +104,7 @@ export function getlintErrorsFromTree(
unEvalPropertyValue,
entity,
fullPropertyPath,
GLOBAL_DATA_WITHOUT_FUNCTIONS,
evalContextWithOutFunctions,
);
set(lintTreeErrors, `["${fullPropertyPath}"]`, lintErrors);
});
@ -99,12 +112,6 @@ export function getlintErrorsFromTree(
if (triggerPaths.size || bindingPathsRequiringFunctions.size) {
// we only create GLOBAL_DATA_WITH_FUNCTIONS if there are paths requiring it
// In trigger based fields, functions such as showAlert, storeValue, etc need to be added to the global data
const GLOBAL_DATA_WITH_FUNCTIONS = createGlobalData({
dataTree: unEvalTree,
resolvedFunctions: {},
isTriggerBased: true,
skipEntityFunctions: true,
});
// lint binding paths that need GLOBAL_DATA_WITH_FUNCTIONS
if (bindingPathsRequiringFunctions.size) {
@ -121,7 +128,7 @@ export function getlintErrorsFromTree(
unEvalPropertyValue,
entity,
fullPropertyPath,
GLOBAL_DATA_WITH_FUNCTIONS,
evalContext,
);
set(lintTreeErrors, `["${fullPropertyPath}"]`, lintErrors);
});
@ -141,7 +148,7 @@ export function getlintErrorsFromTree(
const lintErrors = lintTriggerPath(
unEvalPropertyValue,
entity,
GLOBAL_DATA_WITH_FUNCTIONS,
evalContext,
);
set(lintTreeErrors, `["${triggerPath}"]`, lintErrors);
});
@ -155,7 +162,7 @@ function lintBindingPath(
dynamicBinding: string,
entity: DataTreeEntity,
fullPropertyPath: string,
globalData: ReturnType<typeof createGlobalData>,
globalData: ReturnType<typeof createEvaluationContext>,
) {
let lintErrors: LintError[] = [];
@ -214,7 +221,7 @@ function lintBindingPath(
function lintTriggerPath(
userScript: string,
entity: DataTreeEntity,
globalData: ReturnType<typeof createGlobalData>,
globalData: ReturnType<typeof createEvaluationContext>,
) {
const { jsSnippets } = getDynamicBindings(userScript, entity);
const script = getScriptToEval(jsSnippets[0], EvaluationScriptType.TRIGGERS);

View File

@ -98,6 +98,7 @@ import {
validateActionProperty,
validateAndParseWidgetProperty,
} from "./validationUtils";
import { errorModifier } from "workers/Evaluation/errorModifier";
type SortedDependencies = Array<string>;
export type EvalProps = {
@ -661,6 +662,9 @@ export default class DataTreeEvaluator {
evalMetaUpdates: EvalMetaUpdates;
} {
const tree = klona(oldUnevalTree);
errorModifier.updateAsyncFunctions(tree);
const evalMetaUpdates: EvalMetaUpdates = [];
try {
const evaluatedTree = sortedDependencies.reduce(
@ -1033,7 +1037,7 @@ export default class DataTreeEvaluator {
js: string,
data: DataTree,
resolvedFunctions: Record<string, any>,
createGlobalData: boolean,
isJSObject: boolean,
contextData?: EvaluateContext,
callbackData?: Array<any>,
skipUserLogsOperations = false,
@ -1043,7 +1047,7 @@ export default class DataTreeEvaluator {
js,
data,
resolvedFunctions,
createGlobalData,
isJSObject,
contextData,
callbackData,
skipUserLogsOperations,