PromucFlow_constructor/app/client/src/sagas/FormEvaluationSaga.ts
Ayush Pahwa 2cf98ffbb1
fix: trigger call for plugins without require datasource flag (#30566)
## Description

The trigger call for queries make calls to server for dynamic data using
a trigger URL. With workflows feature we introduced `requireDatasource`
flag and the plugins with the flag set as false call a different url
than the ones with the flag set as true. However, server seems to not
send the flag in all cases and hence the flag is treated as false
leading to failed trigger calls.

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

#### Type of change

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

## Testing
>
#### How Has This Been Tested?
> Please describe the tests that you ran to verify your changes. Also
list any relevant details for your test configuration.
> Delete anything that is not relevant
- [ ] Manual
- [ ] JUnit
- [ ] Jest
- [ ] Cypress
>
>
#### Test Plan
> Add Testsmith test cases links that relate to this PR
>
>
#### 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:
- [ ] [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
- [ ] 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


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Refactor**
- Improved the evaluation process for forms to ensure enhanced
performance and reliability.
- Modified the assignment of `dsConfig` to handle undefined
`datasourceStorages[currentEnvironment]`.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Aishwarya UR <aishwarya@appsmith.com>
2024-01-24 00:39:12 +05:30

300 lines
10 KiB
TypeScript

import type { ActionPattern } from "redux-saga/effects";
import { call, take, select, put, actionChannel } from "redux-saga/effects";
import type { ReduxAction } from "@appsmith/constants/ReduxActionConstants";
import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants";
import log from "loglevel";
import * as Sentry from "@sentry/react";
import { getFormEvaluationState } from "selectors/formSelectors";
import { evalFormConfig } from "./EvaluationsSaga";
import type {
ConditionalOutput,
DynamicValues,
FormEvaluationState,
} from "reducers/evaluationReducers/formEvaluationReducer";
import { FORM_EVALUATION_REDUX_ACTIONS } from "@appsmith/actions/evaluationActionsList";
import type { Action, ActionConfig } from "entities/Action";
import type { FormConfigType } from "components/formControls/BaseControl";
import PluginsApi from "api/PluginApi";
import type { ApiResponse } from "api/ApiResponses";
import { getAction, getPlugin } from "@appsmith/selectors/entitiesSelector";
import { getDataTreeActionConfigPath } from "entities/Action/actionProperties";
import { getDataTree } from "selectors/dataTreeSelectors";
import { getDynamicBindings, isDynamicValue } from "utils/DynamicBindingUtils";
import get from "lodash/get";
import { klona } from "klona/lite";
import type { DataTree } from "entities/DataTree/dataTreeTypes";
import {
extractFetchDynamicValueFormConfigs,
extractQueueOfValuesToBeFetched,
} from "./helper";
import type { DatasourceConfiguration } from "entities/Datasource";
import { buffers } from "redux-saga";
import type { Plugin } from "api/PluginApi";
import { doesPluginRequireDatasource } from "@appsmith/entities/Engine/actionHelpers";
export interface FormEvalActionPayload {
formId: string;
datasourceId?: string;
pluginId?: string;
actionConfiguration?: ActionConfig;
editorConfig?: FormConfigType[];
settingConfig?: FormConfigType[];
actionDiffPath?: string;
hasRouteChanged?: boolean;
datasourceConfiguration?: DatasourceConfiguration;
}
// This value holds an array of values that needs to be dynamically fetched
// when we run form evaluations we store dynamic values to be fetched in this array
// and when evaluations are finally done, we pick the last dynamic values and call it.
function* setFormEvaluationSagaAsync(
action: ReduxAction<FormEvalActionPayload>,
): any {
try {
// Get current state from redux
const currentEvalState: FormEvaluationState = yield select(
getFormEvaluationState,
);
// Trigger the worker to compute the new eval state
const workerResponse = yield call(evalFormConfig, {
...action,
currentEvalState,
});
if (action?.type === ReduxActionTypes.INIT_FORM_EVALUATION) {
const fetchDynamicValueFormConfigs = extractFetchDynamicValueFormConfigs(
workerResponse[action?.payload?.formId],
);
yield put({
type: ReduxActionTypes.INIT_TRIGGER_VALUES,
payload: {
[action?.payload?.formId]: klona(fetchDynamicValueFormConfigs),
},
});
}
// RUN_FORM_EVALUATION shouldn't be called before INIT_FORM_EVALUATION has been called with
// the same `formId` else `extractQueueOfValuesToBeFetched` will be sent an undefined value.
let queueOfValuesToBeFetched;
if (
action?.type === ReduxActionTypes.RUN_FORM_EVALUATION &&
workerResponse[action?.payload?.formId]
) {
queueOfValuesToBeFetched = extractQueueOfValuesToBeFetched(
workerResponse[action?.payload?.formId],
);
}
// Update the eval state in redux only if it is not empty
if (workerResponse) {
yield put({
type: ReduxActionTypes.SET_FORM_EVALUATION,
payload: workerResponse,
});
}
// If there are any actions in the queue, run them
// Once all the actions are done, extract the actions that need to be fetched dynamically
const formId = action.payload.formId;
const evalOutput = workerResponse[formId];
if (evalOutput && typeof evalOutput === "object") {
if (queueOfValuesToBeFetched) {
yield put({
type: ReduxActionTypes.FETCH_TRIGGER_VALUES_INIT,
payload: {
formId,
values: queueOfValuesToBeFetched,
},
});
// Pass the queue to the saga to fetch the dynamic values
yield call(
fetchDynamicValuesSaga,
queueOfValuesToBeFetched,
formId,
action.payload.datasourceId ? action.payload.datasourceId : "",
action.payload.pluginId ? action.payload.pluginId : "",
);
}
}
} catch (e) {
log.error(e);
}
}
// Function to fetch the dynamic values one by one from the queue
export function* fetchDynamicValuesSaga(
queueOfValuesToBeFetched: Record<string, ConditionalOutput>,
formId: string,
datasourceId: string,
pluginId: string,
) {
for (const key of Object.keys(queueOfValuesToBeFetched)) {
queueOfValuesToBeFetched[key].fetchDynamicValues = yield call(
fetchDynamicValueSaga,
queueOfValuesToBeFetched[key],
Object.assign(
{},
queueOfValuesToBeFetched[key].fetchDynamicValues as DynamicValues,
),
formId,
datasourceId,
pluginId,
key,
);
}
// Set the values to the state once all values are fetched
yield put({
type: ReduxActionTypes.FETCH_TRIGGER_VALUES_SUCCESS,
payload: {
formId,
values: queueOfValuesToBeFetched,
},
});
}
function* fetchDynamicValueSaga(
value: ConditionalOutput,
dynamicFetchedValues: DynamicValues,
actionId: string,
datasourceId: string,
pluginId: string,
configProperty: string,
) {
try {
const { config, evaluatedConfig } =
value.fetchDynamicValues as DynamicValues;
const { params } = evaluatedConfig;
dynamicFetchedValues.hasStarted = true;
const plugin: Plugin = yield select(getPlugin, pluginId);
let url = PluginsApi.defaultDynamicTriggerURL(datasourceId);
if (!doesPluginRequireDatasource(plugin)) {
url = PluginsApi.dynamicTriggerURLForInternalPlugins(pluginId);
}
if (
"url" in evaluatedConfig &&
!!evaluatedConfig.url &&
evaluatedConfig.url.length > 0
)
url = evaluatedConfig.url;
// Eval Action is the current action as it is stored in the dataTree
let evalAction: any;
// Evaluated params is the object that will hold the evaluated values of the parameters as computed in the dataTree
let evaluatedParams;
// this is a temporary variable, used to derive the evaluated value of the current parameters before being stored in the evaluated params
let substitutedParameters = {};
const action: Action = yield select(getAction, actionId);
const { workspaceId } = action;
const dataTree: DataTree = yield select(getDataTree);
if (!!action) {
evalAction = dataTree[action.name];
}
// we use the config parameters to get the action diff path value of the parameters i.e. {{actionConfiguration.formData.sheetUrl.data}. Note that it is enclosed within dynamic bindings
if ("parameters" in config?.params && !!evalAction) {
Object.entries(config?.params.parameters).forEach(([key, value]) => {
// we extract the action diff path of the param value from the dynamic binding i.e. actionConfiguration.formData.sheetUrl.data
const dynamicBindingValue = getDynamicBindings(value as string)
?.jsSnippets[0];
// we convert this action Diff path into the same format as it is stored in the dataTree i.e. config.formData.sheetUrl.data
const dataTreeActionConfigPath =
getDataTreeActionConfigPath(dynamicBindingValue);
// then we get the value of the current parameter from the evaluatedValues in the action object stored in the dataTree.
// TODOD: Find a better way to pass the workspaceId
const evaluatedValue = get(
{ ...evalAction?.__evaluation__?.evaluatedValues, workspaceId },
dataTreeActionConfigPath,
);
// if it exists, we store it in the substituted params object.
// we check if that value is enclosed in dynamic bindings i.e the value has not been evaluated or somehow still contains a js expression
// if it is, we return an empty string since we don't want to send dynamic bindings to the server.
// if it contains a value, we send the value to the server
if (!!evaluatedValue) {
substitutedParameters = {
...substitutedParameters,
[key]: isDynamicValue(evaluatedValue) ? "" : evaluatedValue,
};
}
});
}
// we destructure the values back to the appropriate places.
if ("parameters" in params) {
evaluatedParams = {
...params,
parameters: {
...params.parameters,
...substitutedParameters,
},
};
} else {
evaluatedParams = {
...params,
};
}
// Call the API to fetch the dynamic values
const response: ApiResponse<{ trigger?: unknown }> = yield call(
PluginsApi.fetchDynamicFormValues,
url,
{
actionId,
configProperty,
datasourceId,
pluginId,
...evaluatedParams,
},
);
dynamicFetchedValues.isLoading = false;
if (response.responseMeta.status === 200 && "trigger" in response.data) {
dynamicFetchedValues.data = response.data.trigger;
dynamicFetchedValues.hasFetchFailed = false;
} else {
dynamicFetchedValues.hasFetchFailed = true;
dynamicFetchedValues.data = [];
}
} catch (e) {
log.error(e);
dynamicFetchedValues.hasFetchFailed = true;
dynamicFetchedValues.isLoading = false;
dynamicFetchedValues.data = [];
}
return dynamicFetchedValues;
}
function* formEvaluationChangeListenerSaga() {
const buffer = buffers.fixed();
const formEvalChannel: ActionPattern<ReduxAction<FormEvalActionPayload>> =
yield actionChannel(FORM_EVALUATION_REDUX_ACTIONS, buffer as any);
while (true) {
if (buffer.isEmpty()) {
yield put({
type: ReduxActionTypes.FORM_EVALUATION_EMPTY_BUFFER,
});
}
const action: ReduxAction<FormEvalActionPayload> =
yield take(formEvalChannel);
yield call(setFormEvaluationSagaAsync, action);
}
}
export default function* formEvaluationChangeListener() {
yield take(ReduxActionTypes.START_EVALUATION);
while (true) {
try {
yield call(formEvaluationChangeListenerSaga);
} catch (e) {
log.error(e);
Sentry.captureException(e);
}
}
}