PromucFlow_constructor/app/client/src/sagas/EvaluationsSaga.ts
Rishabh Rathod 55985c2e08
fix: revalidation logic (#25420)
## Description

Debugger logs were only updated according to the evaluation order
earlier this led to an issue as after revalidation we need to update the
debugger errors.

#### PR fixes following issue(s)
Fixes #24905


#### Type of change

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

## Testing

#### Test Plan

- Validation sanity test

#### Issues raised during DP testing


## 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:
- [ ] [Speedbreak
features](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#speedbreakers-)
have been covered
- [ ] Test plan covers all impacted features and [areas of
interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#areas-of-interest-)
- [ ] Test plan has been peer reviewed by project stakeholders and other
QA members
- [x] Manually tested functionality on DP
- [ ] We had an implementation alignment call with stakeholders post QA
Round 2
- [ ] Cypress test cases have been added and approved by SDET/manual QA
- [ ] Added `Test Plan Approved` label after Cypress tests were reviewed
- [ ] Added `Test Plan Approved` label after JUnit tests were reviewed
2023-07-21 16:41:50 +05:30

729 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { ActionPattern, CallEffect, ForkEffect } from "redux-saga/effects";
import {
actionChannel,
all,
call,
delay,
fork,
put,
select,
spawn,
take,
} from "redux-saga/effects";
import type {
EvaluationReduxAction,
ReduxAction,
ReduxActionType,
AnyReduxAction,
} from "@appsmith/constants/ReduxActionConstants";
import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants";
import {
getDataTree,
getUnevaluatedDataTree,
} from "selectors/dataTreeSelectors";
import { getMetaWidgets, getWidgets } from "sagas/selectors";
import type { WidgetTypeConfigMap } from "utils/WidgetFactory";
import WidgetFactory from "utils/WidgetFactory";
import { GracefulWorkerService } from "utils/WorkerUtil";
import type { EvalError, EvaluationError } from "utils/DynamicBindingUtils";
import { PropertyEvaluationErrorType } from "utils/DynamicBindingUtils";
import { EVAL_WORKER_ACTIONS } from "@appsmith/workers/Evaluation/evalWorkerActions";
import log from "loglevel";
import type { WidgetProps } from "widgets/BaseWidget";
import PerformanceTracker, {
PerformanceTransactionName,
} from "utils/PerformanceTracker";
import * as Sentry from "@sentry/react";
import type { Action } from "redux";
import {
EVAL_AND_LINT_REDUX_ACTIONS,
FIRST_EVAL_REDUX_ACTIONS,
setDependencyMap,
setEvaluatedTree,
shouldForceEval,
shouldLog,
shouldProcessAction,
shouldTriggerEvaluation,
shouldTriggerLinting,
} from "actions/evaluationActions";
import ConfigTreeActions from "utils/configTree";
import {
dynamicTriggerErrorHandler,
evalErrorHandler,
handleJSFunctionExecutionErrorLog,
logJSVarCreatedEvent,
logSuccessfulBindings,
postEvalActionDispatcher,
updateTernDefinitions,
} from "./PostEvaluationSagas";
import type { JSAction } from "entities/JSCollection";
import { getAppMode } from "@appsmith/selectors/applicationSelectors";
import { APP_MODE } from "entities/App";
import { get, isEmpty } from "lodash";
import type { TriggerMeta } from "@appsmith/sagas/ActionExecution/ActionExecutionSagas";
import { executeActionTriggers } from "@appsmith/sagas/ActionExecution/ActionExecutionSagas";
import {
EventType,
TriggerKind,
} from "constants/AppsmithActionConstants/ActionConstants";
import { validate } from "workers/Evaluation/validations";
import { diff } from "deep-diff";
import { REPLAY_DELAY } from "entities/Replay/replayUtils";
import type { EvaluationVersion } from "@appsmith/api/ApplicationApi";
import type { LogObject } from "entities/AppsmithConsole";
import { ENTITY_TYPE } from "entities/AppsmithConsole";
import type { Replayable } from "entities/Replay/ReplayEntity/ReplayEditor";
import type { FormEvaluationState } from "reducers/evaluationReducers/formEvaluationReducer";
import type { FormEvalActionPayload } from "./FormEvaluationSaga";
import { getSelectedAppTheme } from "selectors/appThemingSelectors";
import { resetWidgetsMetaState, updateMetaState } from "actions/metaActions";
import {
getAllActionValidationConfig,
getAllJSActionsData,
} from "selectors/entitiesSelector";
import type {
DataTree,
UnEvalTree,
WidgetEntityConfig,
} from "entities/DataTree/dataTreeFactory";
import { initiateLinting, lintWorker } from "./LintingSagas";
import type {
EvalTreeRequestData,
EvalTreeResponseData,
} from "workers/Evaluation/types";
import type { ActionDescription } from "@appsmith/workers/Evaluation/fns";
import { handleEvalWorkerRequestSaga } from "./EvalWorkerActionSagas";
import { getAppsmithConfigs } from "@appsmith/configs";
import { executeJSUpdates } from "actions/pluginActionActions";
import { setEvaluatedActionSelectorField } from "actions/actionSelectorActions";
import { waitForWidgetConfigBuild } from "./InitSagas";
const APPSMITH_CONFIGS = getAppsmithConfigs();
export const evalWorker = new GracefulWorkerService(
new Worker(
new URL("../workers/Evaluation/evaluation.worker.ts", import.meta.url),
{
type: "module",
// Note: the `Worker` part of the name is slightly important LinkRelPreload_spec.js
// relies on it to find workers in the list of all requests.
name: "evalWorker",
},
),
);
let widgetTypeConfigMap: WidgetTypeConfigMap;
export function* updateDataTreeHandler(
data: {
evalTreeResponse: EvalTreeResponseData;
unevalTree: UnEvalTree;
requiresLogging: boolean;
},
postEvalActions?: Array<AnyReduxAction>,
) {
const { evalTreeResponse, requiresLogging, unevalTree } = data;
const postEvalActionsToDispatch: Array<AnyReduxAction> =
postEvalActions || [];
const {
configTree,
dataTree,
dependencies,
errors,
evalMetaUpdates = [],
evaluationOrder,
reValidatedPaths,
isCreateFirstTree = false,
isNewWidgetAdded,
jsUpdates,
logs,
pathsToClearErrorsFor,
staleMetaIds,
undefinedEvalValuesMap,
unEvalUpdates,
jsVarsCreatedEvent,
} = evalTreeResponse;
const appMode: ReturnType<typeof getAppMode> = yield select(getAppMode);
PerformanceTracker.stopAsyncTracking(
PerformanceTransactionName.DATA_TREE_EVALUATION,
);
PerformanceTracker.startAsyncTracking(
PerformanceTransactionName.SET_EVALUATED_TREE,
);
const oldDataTree: ReturnType<typeof getDataTree> = yield select(getDataTree);
const updates = diff(oldDataTree, dataTree) || [];
if (!isEmpty(staleMetaIds)) {
yield put(resetWidgetsMetaState(staleMetaIds));
}
yield put(setEvaluatedTree(updates));
ConfigTreeActions.setConfigTree(configTree);
PerformanceTracker.stopAsyncTracking(
PerformanceTransactionName.SET_EVALUATED_TREE,
);
// if evalMetaUpdates are present only then dispatch updateMetaState
if (evalMetaUpdates.length) {
yield put(updateMetaState(evalMetaUpdates));
}
log.debug({ evalMetaUpdatesLength: evalMetaUpdates.length });
const updatedDataTree: DataTree = yield select(getDataTree);
log.debug({ jsUpdates: jsUpdates });
log.debug({ dataTree: updatedDataTree });
logs?.forEach((evalLog: any) => log.debug(evalLog));
yield call(
evalErrorHandler,
errors,
updatedDataTree,
evaluationOrder,
reValidatedPaths,
configTree,
pathsToClearErrorsFor,
);
if (appMode !== APP_MODE.PUBLISHED) {
const jsData: Record<string, unknown> = yield select(getAllJSActionsData);
postEvalActionsToDispatch.push(executeJSUpdates(jsUpdates));
if (requiresLogging) {
yield fork(
logSuccessfulBindings,
unevalTree,
updatedDataTree,
evaluationOrder,
isCreateFirstTree,
isNewWidgetAdded,
configTree,
undefinedEvalValuesMap,
);
}
yield fork(
updateTernDefinitions,
updatedDataTree,
configTree,
unEvalUpdates,
isCreateFirstTree,
jsData,
);
}
yield put(setDependencyMap(dependencies));
if (postEvalActionsToDispatch && postEvalActionsToDispatch.length) {
yield call(postEvalActionDispatcher, postEvalActionsToDispatch);
}
yield call(logJSVarCreatedEvent, jsVarsCreatedEvent);
}
/**
* This saga is responsible for evaluating the data tree
* @param postEvalActions
* @param shouldReplay
* @param requiresLinting
* @param forceEvaluation - if true, will re-evaluate the entire tree
* @returns
* @example
* yield call(evaluateTreeSaga, postEvalActions, shouldReplay, requiresLinting, forceEvaluation)
*/
export function* evaluateTreeSaga(
unEvalAndConfigTree: ReturnType<typeof getUnevaluatedDataTree>,
postEvalActions?: Array<AnyReduxAction>,
shouldReplay = true,
forceEvaluation = false,
requiresLogging = false,
) {
const allActionValidationConfig: ReturnType<
typeof getAllActionValidationConfig
> = yield select(getAllActionValidationConfig);
const unevalTree = unEvalAndConfigTree.unEvalTree;
const widgets: ReturnType<typeof getWidgets> = yield select(getWidgets);
const metaWidgets: ReturnType<typeof getMetaWidgets> = yield select(
getMetaWidgets,
);
const theme: ReturnType<typeof getSelectedAppTheme> = yield select(
getSelectedAppTheme,
);
const appMode: ReturnType<typeof getAppMode> = yield select(getAppMode);
const toPrintConfigTree = unEvalAndConfigTree.configTree;
log.debug({ unevalTree, configTree: toPrintConfigTree });
PerformanceTracker.startAsyncTracking(
PerformanceTransactionName.DATA_TREE_EVALUATION,
);
const evalTreeRequestData: EvalTreeRequestData = {
unevalTree: unEvalAndConfigTree,
widgetTypeConfigMap,
widgets,
theme,
shouldReplay,
allActionValidationConfig,
forceEvaluation,
metaWidgets,
appMode,
};
const workerResponse: EvalTreeResponseData = yield call(
evalWorker.request,
EVAL_WORKER_ACTIONS.EVAL_TREE,
evalTreeRequestData,
);
yield call(
updateDataTreeHandler,
{ evalTreeResponse: workerResponse, unevalTree, requiresLogging },
postEvalActions,
);
}
export function* evaluateActionBindings(
bindings: string[],
executionParams: Record<string, any> | string = {},
) {
const workerResponse: { errors: EvalError[]; values: unknown } = yield call(
evalWorker.request,
EVAL_WORKER_ACTIONS.EVAL_ACTION_BINDINGS,
{
bindings,
executionParams,
},
);
const { errors, values } = workerResponse;
yield call(evalErrorHandler, errors);
return values;
}
export function* evaluateAndExecuteDynamicTrigger(
dynamicTrigger: string,
eventType: EventType,
triggerMeta: TriggerMeta,
callbackData?: Array<any>,
globalContext?: Record<string, unknown>,
) {
const unEvalTree: ReturnType<typeof getUnevaluatedDataTree> = yield select(
getUnevaluatedDataTree,
);
// const unEvalTree = unEvalAndConfigTree.unEvalTree;
log.debug({ execute: dynamicTrigger });
const response: { errors: EvaluationError[]; result: unknown } = yield call(
evalWorker.request,
EVAL_WORKER_ACTIONS.EVAL_TRIGGER,
{
unEvalTree,
dynamicTrigger,
callbackData,
globalContext,
eventType,
triggerMeta,
},
);
const { errors = [] } = response as any;
yield call(dynamicTriggerErrorHandler, errors);
return response;
}
export interface ResponsePayload {
data: {
reason?: string;
resolve?: unknown;
};
success: boolean;
}
/*
* It is necessary to respond back as the worker is waiting with a pending promise and wanting to know if it should
* resolve or reject it with the data the execution has provided
*/
export function* executeTriggerRequestSaga(
trigger: ActionDescription,
eventType: EventType,
triggerMeta: TriggerMeta,
) {
const responsePayload = {
data: null,
error: null,
};
try {
responsePayload.data = yield call(
executeActionTriggers,
trigger,
eventType,
triggerMeta,
);
} catch (error) {
// When error occurs in execution of triggers,
// a success: false is sent to reject the promise
// @ts-expect-error: reason is of type string
responsePayload.error = {
// @ts-expect-error: reason is of type string
message: error.responseData?.[0] || error.message,
};
}
return responsePayload;
}
export function* clearEvalCache() {
yield put({ type: ReduxActionTypes.RESET_DATA_TREE });
yield call(evalWorker.request, EVAL_WORKER_ACTIONS.CLEAR_CACHE);
return true;
}
interface JSFunctionExecutionResponse {
errors: unknown[];
result: unknown;
logs?: LogObject[];
}
function* executeAsyncJSFunction(
collectionName: string,
action: JSAction,
collectionId: string,
) {
const functionCall = `${collectionName}.${action.name}()`;
const triggerMeta = {
source: {
id: collectionId,
name: `${collectionName}.${action.name}`,
type: ENTITY_TYPE.JSACTION,
},
triggerPropertyName: `${collectionName}.${action.name}`,
triggerKind: TriggerKind.JS_FUNCTION_EXECUTION,
};
const eventType = EventType.ON_JS_FUNCTION_EXECUTE;
const response: JSFunctionExecutionResponse = yield call(
evaluateAndExecuteDynamicTrigger,
functionCall,
eventType,
triggerMeta,
);
return response;
}
export function* executeJSFunction(
collectionName: string,
action: JSAction,
collectionId: string,
) {
const response: {
errors: unknown[];
result: unknown;
logs?: LogObject[];
} = yield call(executeAsyncJSFunction, collectionName, action, collectionId);
const { errors, result } = response;
const isDirty = !!errors.length;
// After every function execution, log execution errors if present
yield call(
handleJSFunctionExecutionErrorLog,
collectionId,
collectionName,
action,
errors,
);
return { result, isDirty };
}
export function* validateProperty(
property: string,
value: any,
props: WidgetProps,
) {
const unEvalAndConfigTree: ReturnType<typeof getUnevaluatedDataTree> =
yield select(getUnevaluatedDataTree);
const configTree = unEvalAndConfigTree.configTree;
const entityConfig = configTree[props.widgetName] as WidgetEntityConfig;
const validation = entityConfig?.validationPaths[property];
const response: unknown = yield call(
evalWorker.request,
EVAL_WORKER_ACTIONS.VALIDATE_PROPERTY,
{
property,
value,
props,
validation,
},
);
return response;
}
function evalQueueBuffer() {
let canTake = false;
let collectedPostEvalActions: any = [];
const take = () => {
if (canTake) {
const resp = collectedPostEvalActions;
collectedPostEvalActions = [];
canTake = false;
return { postEvalActions: resp, type: ReduxActionTypes.BUFFERED_ACTION };
}
};
const flush = () => {
if (canTake) {
return [take() as Action];
}
return [];
};
const put = (action: EvaluationReduxAction<unknown | unknown[]>) => {
if (!shouldProcessAction(action)) {
return;
}
canTake = true;
const postEvalActions = getPostEvalActions(action);
collectedPostEvalActions.push(...postEvalActions);
};
return {
take,
put,
isEmpty: () => {
return !canTake;
},
flush,
};
}
/**
* Extract the post eval actions from an evaluation action
* Batched actions have post eval actions inside them, extract that
*
* **/
function getPostEvalActions(
action: EvaluationReduxAction<unknown | unknown[]>,
): AnyReduxAction[] {
const postEvalActions: AnyReduxAction[] = [];
if (action.postEvalActions) {
postEvalActions.push(...action.postEvalActions);
}
if (
action.type === ReduxActionTypes.BATCH_UPDATES_SUCCESS &&
Array.isArray(action.payload)
) {
action.payload.forEach((batchedAction) => {
if (batchedAction.postEvalActions) {
postEvalActions.push(
...(batchedAction.postEvalActions as AnyReduxAction[]),
);
}
});
}
return postEvalActions;
}
function* evalAndLintingHandler(
isBlockingCall = true,
action: ReduxAction<unknown>,
options: Partial<{
shouldReplay: boolean;
forceEvaluation: boolean;
requiresLogging: boolean;
}>,
) {
const { forceEvaluation, requiresLogging, shouldReplay } = options;
const appMode: ReturnType<typeof getAppMode> = yield select(getAppMode);
const requiresLinting =
appMode === APP_MODE.EDIT && shouldTriggerLinting(action);
const requiresEval = shouldTriggerEvaluation(action);
log.debug({
action,
triggeredLinting: requiresLinting,
triggeredEvaluation: requiresEval,
});
if (!requiresEval && !requiresLinting) return;
// Generate all the data needed for both eval and linting
const unEvalAndConfigTree: ReturnType<typeof getUnevaluatedDataTree> =
yield select(getUnevaluatedDataTree);
const postEvalActions = getPostEvalActions(action);
const fn: (...args: unknown[]) => CallEffect<unknown> | ForkEffect<unknown> =
isBlockingCall ? call : fork;
const effects = [];
if (requiresEval) {
effects.push(
fn(
evaluateTreeSaga,
unEvalAndConfigTree,
postEvalActions,
shouldReplay,
forceEvaluation,
requiresLogging,
),
);
}
if (requiresLinting) {
effects.push(fn(initiateLinting, unEvalAndConfigTree, forceEvaluation));
}
yield all(effects);
}
function* evaluationChangeListenerSaga(): any {
// Explicitly shutdown old worker if present
yield all([call(evalWorker.shutdown), call(lintWorker.shutdown)]);
const [evalWorkerListenerChannel] = yield all([
call(evalWorker.start),
call(lintWorker.start),
]);
yield call(evalWorker.request, EVAL_WORKER_ACTIONS.SETUP, {
cloudHosting: !!APPSMITH_CONFIGS.cloudHosting,
});
yield spawn(handleEvalWorkerRequestSaga, evalWorkerListenerChannel);
const initAction: EvaluationReduxAction<unknown> = yield take(
FIRST_EVAL_REDUX_ACTIONS,
);
yield call(waitForWidgetConfigBuild);
widgetTypeConfigMap = WidgetFactory.getWidgetTypeConfigMap();
yield fork(evalAndLintingHandler, false, initAction, {
shouldReplay: false,
forceEvaluation: false,
});
const evtActionChannel: ActionPattern<Action<any>> = yield actionChannel(
EVAL_AND_LINT_REDUX_ACTIONS,
evalQueueBuffer(),
);
while (true) {
const action: EvaluationReduxAction<unknown | unknown[]> = yield take(
evtActionChannel,
);
yield call(evalAndLintingHandler, true, action, {
shouldReplay: get(action, "payload.shouldReplay"),
forceEvaluation: shouldForceEval(action),
requiresLogging: shouldLog(action),
});
}
}
export function* evaluateActionSelectorFieldSaga(action: any) {
const { id, type, value } = action.payload;
try {
const workerResponse: {
errors: Array<unknown>;
result: unknown;
} = yield call(evalWorker.request, EVAL_WORKER_ACTIONS.EVAL_EXPRESSION, {
expression: value,
});
const lintErrors = (workerResponse.errors || []).filter(
(error: any) => error.errorType !== PropertyEvaluationErrorType.LINT,
);
if (workerResponse.result) {
const validation = validate({ type }, workerResponse.result, {}, "");
if (!validation.isValid)
validation.messages?.map((message) => {
lintErrors.unshift({
...validation,
...{
errorType: PropertyEvaluationErrorType.VALIDATION,
errorMessage: message,
},
});
});
}
yield put(
setEvaluatedActionSelectorField({
id,
evaluatedValue: {
value: workerResponse.result as string,
errors: lintErrors,
},
}),
);
} catch (e) {
log.error(e);
Sentry.captureException(e);
}
}
export function* updateReplayEntitySaga(
actionPayload: ReduxAction<{
entityId: string;
entity: Replayable;
entityType: ENTITY_TYPE;
}>,
) {
//Delay updates to replay object to not persist every keystroke
yield delay(REPLAY_DELAY);
const { entity, entityId, entityType } = actionPayload.payload;
const workerResponse: unknown = yield call(
evalWorker.request,
EVAL_WORKER_ACTIONS.UPDATE_REPLAY_OBJECT,
{
entityId,
entity,
entityType,
},
);
return workerResponse;
}
export function* workerComputeUndoRedo(operation: string, entityId: string) {
const workerResponse: unknown = yield call(evalWorker.request, operation, {
entityId,
});
return workerResponse;
}
// Type to represent the state of the evaluation reducer
export interface FormEvaluationConfig
extends ReduxAction<FormEvalActionPayload> {
currentEvalState: FormEvaluationState;
}
// Function to trigger the form eval job in the worker
export function* evalFormConfig(formEvaluationConfigObj: FormEvaluationConfig) {
const workerResponse: unknown = yield call(
evalWorker.request,
EVAL_WORKER_ACTIONS.INIT_FORM_EVAL,
formEvaluationConfigObj,
);
return workerResponse;
}
export function* setAppVersionOnWorkerSaga(action: {
type: ReduxActionType;
payload: EvaluationVersion;
}) {
const version: EvaluationVersion = action.payload;
yield call(evalWorker.request, EVAL_WORKER_ACTIONS.SET_EVALUATION_VERSION, {
version,
});
}
export default function* evaluationSagaListeners() {
yield take(ReduxActionTypes.START_EVALUATION);
while (true) {
try {
yield call(evaluationChangeListenerSaga);
} catch (e) {
log.error(e);
Sentry.captureException(e);
}
}
}
export { evalWorker as EvalWorker };