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:
parent
bfd242f627
commit
6b751d914e
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
76
app/client/src/workers/Evaluation/errorModifier.ts
Normal file
76
app/client/src/workers/Evaluation/errorModifier.ts
Normal 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;
|
||||
};
|
||||
|
|
@ -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;
|
||||
})();
|
||||
}
|
||||
|
|
|
|||
72
app/client/src/workers/Evaluation/functionDeterminer.ts
Normal file
72
app/client/src/workers/Evaluation/functionDeterminer.ts
Normal 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();
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user