## 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>
300 lines
10 KiB
TypeScript
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);
|
|
}
|
|
}
|
|
}
|