feat: file picker added and access token generation (#20778)

## Description

This PR includes following changes:

- In case of limiting google sheet access project, when user selects specific sheets as an option, they should be shown file picker UI once the authorisation is complete, In this file picker UI, users can select the google sheet files that they want to use with appsmith application and allow access to only those files.
- This PR contains the changes for file picker UI and updating datasource auth state based on the files selected by user.


TL;DR
Steps to test this PR:

- Create Google Sheet datasource
- In the datasource config form, select specific sheets as an option from the scope dropdown.
- Click on save and authorise
- This will take you to google oauth process

<img width="467" alt="Screenshot 2023-02-20 at 1 24 24 PM" src="https://user-images.githubusercontent.com/30018882/220045493-57b0ca6c-3f08-4963-af55-d603cf79bc43.png">

- Select the google account
- This will take you to google oauth2 consent screen

<img width="451" alt="Screenshot 2023-02-20 at 1 24 55 PM" src="https://user-images.githubusercontent.com/30018882/220045641-9f70dd29-6664-489a-b77b-df65445491df.png">

- Click on allow for all requested permissions
- This will take you back to appsmith's datasource config page in view mode and load the file picker UI
<img width="425" alt="Screenshot 2023-02-20 at 1 25 47 PM" src="https://user-images.githubusercontent.com/30018882/220045828-8b3e3e46-4ddc-4e30-b2f8-f12865395817.png">
- Select the files that you want to share with appsmith app
- Click on select
- You should see the new query button in enabled state, as datasource authorisation is complete

<img width="800" alt="Screenshot 2023-02-20 at 1 27 28 PM" src="https://user-images.githubusercontent.com/30018882/220046131-6ce99a85-cddc-4529-ae45-f9833aefd71b.png">

- In case you select cancel on google oauth2 consent screen, you should error message on datasource config page with new query button being disabled

<img width="810" alt="Screenshot 2023-02-20 at 1 28 49 PM" src="https://user-images.githubusercontent.com/30018882/220046385-6b8d636c-b517-44c3-a596-b52bc0084b94.png">

- In case you do give all the permissions but do not select any files in google file picker, then also you should see error message on datasource config page with new query button disabled.

Fixes #20163, #20290, #20160, #20162


Media
> A video or a GIF is preferred. when using Loom, don’t embed because it looks like it’s a GIF. instead, just link to the video


## Type of change

> Please delete options that are not relevant.

- New feature (non-breaking change which adds functionality)


## How Has This Been Tested?

- Manual

### 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
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] 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
- [x] 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: “sneha122” <“sneha@appsmith.com”>
This commit is contained in:
sneha122 2023-03-08 10:55:17 +05:30 committed by GitHub
parent 8a35c2d3ad
commit 0f838535c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 218 additions and 33 deletions

View File

@ -120,9 +120,9 @@ describe("Validate CRUD queries for Amazon S3 along with UI flow verifications",
cy.onlyQueryRun();
cy.wait("@postExecute").then(({ response }) => {
expect(response.body.data.isExecutionSuccess).to.eq(false);
expect(response.body.data.pluginErrorDetails.appsmithErrorMessage).to.contains(
"File content is not base64 encoded.",
);
expect(
response.body.data.pluginErrorDetails.appsmithErrorMessage,
).to.contains("File content is not base64 encoded.");
});
cy.ValidateAndSelectDropdownOption(
formControls.s3CreateFileDataType,
@ -253,7 +253,9 @@ describe("Validate CRUD queries for Amazon S3 along with UI flow verifications",
cy.onlyQueryRun();
cy.wait("@postExecute").then(({ response }) => {
expect(response.body.data.isExecutionSuccess).to.eq(false);
expect(response.body.data.pluginErrorDetails.appsmithErrorMessage).to.contain(
expect(
response.body.data.pluginErrorDetails.appsmithErrorMessage,
).to.contain(
"Your S3 query failed to execute. To know more please check the error details.",
);
});
@ -263,7 +265,9 @@ describe("Validate CRUD queries for Amazon S3 along with UI flow verifications",
cy.onlyQueryRun();
cy.wait("@postExecute").then(({ response }) => {
expect(response.body.data.isExecutionSuccess).to.eq(false);
expect(response.body.data.pluginErrorDetails.appsmithErrorMessage).to.contain(
expect(
response.body.data.pluginErrorDetails.appsmithErrorMessage,
).to.contain(
"Your S3 query failed to execute. To know more please check the error details.",
);
});

View File

@ -46,6 +46,14 @@
}
</script>
<!-- Adding this Library to access google file picker API in case of limiting google sheet access -->
<script
async
defer
id="googleapis"
src="https://apis.google.com/js/api.js"
onload="gapiLoaded()"
></script>
</head>
<body class="appsmith-light-theme">
@ -166,6 +174,10 @@
hideWatermark: parseConfig("__APPSMITH_HIDE_WATERMARK__"),
disableIframeWidgetSandbox: parseConfig("__APPSMITH_DISABLE_IFRAME_WIDGET_SANDBOX__"),
};
gapiLoaded = () => {
window.googleAPIsLoaded = true;
}
</script>
</body>

View File

@ -366,6 +366,19 @@ export const initializeDatasourceFormDefaults = (pluginType: string) => {
};
};
// In case of access to specific sheets in google sheet datasource, this action
// is used for handling file picker callback, when user selects files/cancels the selection
// this callback action will be triggered
export const filePickerCallbackAction = (data: {
action: string;
datasourceId: string;
}) => {
return {
type: ReduxActionTypes.FILE_PICKER_CALLBACK_ACTION,
payload: data,
};
};
export default {
fetchDatasources,
initDatasourcePane,

View File

@ -760,6 +760,8 @@ export const ReduxActionTypes = {
AUTOLAYOUT_REORDER_WIDGETS: "AUTOLAYOUT_REORDER_WIDGETS",
AUTOLAYOUT_ADD_NEW_WIDGETS: "AUTOLAYOUT_ADD_NEW_WIDGETS",
RECALCULATE_COLUMNS: "RECALCULATE_COLUMNS",
SET_GSHEET_TOKEN: "SET_GSHEET_TOKEN",
FILE_PICKER_CALLBACK_ACTION: "FILE_PICKER_CALLBACK_ACTION",
};
export type ReduxActionType = typeof ReduxActionTypes[keyof typeof ReduxActionTypes];

View File

@ -11,6 +11,7 @@ export enum AuthenticationStatus {
NONE = "NONE",
IN_PROGRESS = "IN_PROGRESS",
SUCCESS = "SUCCESS",
FAILURE = "FAILURE",
}
export interface DatasourceAuthentication {
authType?: string;
@ -104,6 +105,11 @@ export interface Datasource extends BaseDatasource {
success?: boolean;
}
export interface TokenResponse {
datasource: Datasource;
token: string;
}
export interface MockDatasource {
name: string;
description: string;

View File

@ -34,6 +34,7 @@ import Connected from "../DataSourceEditor/Connected";
import {
getCurrentApplicationId,
getGsheetToken,
getPagePermissions,
} from "selectors/editorSelectors";
import DatasourceAuth from "pages/common/datasourceAuth";
@ -88,6 +89,7 @@ interface StateProps extends JSONtoFormProps {
isDatasourceBeingSaved: boolean;
isDatasourceBeingSavedFromPopup: boolean;
isFormDirty: boolean;
gsheetToken?: string;
}
interface DatasourceFormFunctions {
discardTempDatasource: () => void;
@ -260,6 +262,7 @@ class DatasourceSaaSEditor extends JSONtoForm<Props, State> {
datasourceButtonConfiguration,
datasourceId,
formData,
gsheetToken,
hiddenHeader,
pageId,
plugin,
@ -376,6 +379,7 @@ class DatasourceSaaSEditor extends JSONtoForm<Props, State> {
datasourceDeleteTrigger={this.datasourceDeleteTrigger}
formData={formData}
getSanitizedFormData={_.memoize(this.getSanitizedData)}
gsheetToken={gsheetToken}
isInvalid={this.validate()}
pageId={pageId}
shouldDisplayAuthMessage={!isGoogleSheetPlugin}
@ -436,6 +440,8 @@ const mapStateToProps = (state: AppState, props: any) => {
...pagePermissions,
]);
const gsheetToken = getGsheetToken(state);
return {
datasource,
datasourceButtonConfiguration,
@ -464,6 +470,7 @@ const mapStateToProps = (state: AppState, props: any) => {
isFormDirty,
canCreateDatasourceActions,
featureFlags: selectFeatureFlags(state),
gsheetToken,
};
};

View File

@ -16,6 +16,7 @@ import {
setDatasourceViewMode,
createDatasourceFromForm,
toggleSaveActionFlag,
filePickerCallbackAction,
} from "actions/datasourceActions";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { getCurrentApplicationId } from "selectors/editorSelectors";
@ -46,6 +47,7 @@ import {
hasDeleteDatasourcePermission,
hasManageDatasourcePermission,
} from "@appsmith/utils/permissionHelpers";
import { getAppsmithConfigs } from "ce/configs";
interface Props {
datasource: Datasource;
@ -59,6 +61,7 @@ interface Props {
triggerSave?: boolean;
isFormDirty?: boolean;
datasourceDeleteTrigger: () => void;
gsheetToken?: string;
}
export type DatasourceFormButtonTypes = Record<string, string[]>;
@ -119,6 +122,7 @@ function DatasourceAuth({
shouldDisplayAuthMessage = true,
triggerSave,
isFormDirty,
gsheetToken,
}: Props) {
const authType =
formData && "authType" in formData
@ -148,9 +152,18 @@ function DatasourceAuth({
const pageId = (pageIdQuery || pageIdProp) as string;
const [confirmDelete, setConfirmDelete] = useState(false);
const [scriptLoadedFlag] = useState<boolean>(
(window as any).googleAPIsLoaded,
);
const [pickerInitiated, setPickerInitiated] = useState<boolean>(false);
const dsName = datasource?.name;
const orgId = datasource?.workspaceId;
// objects gapi and google are set, when google apis script is loaded
const gapi: any = (window as any).gapi;
const google: any = (window as any).google;
useEffect(() => {
if (confirmDelete) {
delayConfirmDeleteToFalse();
@ -285,6 +298,50 @@ function DatasourceAuth({
}
};
useEffect(() => {
// This loads the picker object in gapi script
if (!!gsheetToken && !!gapi) {
gapi.load("client:picker", async () => {
await gapi.client.load(
"https://www.googleapis.com/discovery/v1/apis/drive/v3/rest",
);
setPickerInitiated(true);
});
}
}, [scriptLoadedFlag, gsheetToken]);
useEffect(() => {
if (!!gsheetToken && scriptLoadedFlag && pickerInitiated && !!google) {
createPicker(gsheetToken);
}
}, [gsheetToken, scriptLoadedFlag, pickerInitiated]);
const createPicker = async (accessToken: string) => {
const { enableGoogleOAuth } = getAppsmithConfigs();
const googleOAuthClientId: string = enableGoogleOAuth + "";
const APP_ID = googleOAuthClientId.split("-")[0];
const view = new google.picker.View(google.picker.ViewId.SPREADSHEETS);
view.setMimeTypes("application/vnd.google-apps.spreadsheet");
const picker = new google.picker.PickerBuilder()
.enableFeature(google.picker.Feature.NAV_HIDDEN)
.enableFeature(google.picker.Feature.MULTISELECT_ENABLED)
.setAppId(APP_ID)
.setOAuthToken(accessToken)
.addView(view)
.setCallback(pickerCallback)
.build();
picker.setVisible(true);
};
const pickerCallback = async (data: any) => {
dispatch(
filePickerCallbackAction({
action: data.action,
datasourceId: datasourceId,
}),
);
};
const createMode = datasourceId === TEMP_DATASOURCE_ID;
const datasourceButtonsComponentMap = (buttonType: string): JSX.Element => {

View File

@ -26,6 +26,7 @@ export interface DatasourceDataState {
unconfiguredList: Datasource[];
isDatasourceBeingSaved: boolean;
isDatasourceBeingSavedFromPopup: boolean;
gsheetToken: string;
}
const initialState: DatasourceDataState = {
@ -43,6 +44,7 @@ const initialState: DatasourceDataState = {
unconfiguredList: [],
isDatasourceBeingSaved: false,
isDatasourceBeingSavedFromPopup: false,
gsheetToken: "",
};
const datasourceReducer = createReducer(initialState, {
@ -455,6 +457,15 @@ const datasourceReducer = createReducer(initialState, {
isDatasourceBeingSavedFromPopup: action.payload.isDSSavedFromPopup,
};
},
[ReduxActionTypes.SET_GSHEET_TOKEN]: (
state: DatasourceDataState,
action: ReduxAction<{ gsheetToken: string }>,
) => {
return {
...state,
gsheetToken: action.payload.gsheetToken,
};
},
});
export default datasourceReducer;

View File

@ -13,7 +13,7 @@ import {
getFormValues,
initialize,
} from "redux-form";
import _, { merge, isEmpty, get, set } from "lodash";
import { merge, isEmpty, get, set, partition, omit } from "lodash";
import equal from "fast-deep-equal/es6";
import {
ReduxAction,
@ -47,10 +47,15 @@ import {
removeTempDatasource,
createDatasourceSuccess,
resetDefaultKeyValPairFlag,
updateDatasource,
} from "actions/datasourceActions";
import { ApiResponse } from "api/ApiResponses";
import DatasourcesApi, { CreateDatasourceConfig } from "api/DatasourcesApi";
import { Datasource } from "entities/Datasource";
import {
AuthenticationStatus,
Datasource,
TokenResponse,
} from "entities/Datasource";
import { INTEGRATION_EDITOR_MODES, INTEGRATION_TABS } from "constants/routes";
import history from "utils/history";
@ -332,7 +337,7 @@ function* updateDatasourceSaga(
) {
try {
const queryParams = getQueryParams();
const datasourcePayload = _.omit(actionPayload.payload, "name");
const datasourcePayload = omit(actionPayload.payload, "name");
datasourcePayload.isConfigured = true; // when clicking save button, it should be changed as configured
const response: ApiResponse<Datasource> = yield DatasourcesApi.updateDatasource(
@ -458,7 +463,7 @@ function* getOAuthAccessTokenSaga(
}
try {
// Get access token for datasource
const response: ApiResponse<Datasource> = yield OAuthApi.getAccessToken(
const response: ApiResponse<TokenResponse> = yield OAuthApi.getAccessToken(
datasourceId,
appsmithToken,
);
@ -466,12 +471,22 @@ function* getOAuthAccessTokenSaga(
// Update the datasource object
yield put({
type: ReduxActionTypes.UPDATE_DATASOURCE_SUCCESS,
payload: response.data,
});
Toaster.show({
text: OAUTH_AUTHORIZATION_SUCCESSFUL,
variant: Variant.success,
payload: response.data.datasource,
});
if (!!response.data.token) {
yield put({
type: ReduxActionTypes.SET_GSHEET_TOKEN,
payload: {
gsheetToken: response.data.token,
},
});
} else {
Toaster.show({
text: OAUTH_AUTHORIZATION_SUCCESSFUL,
variant: Variant.success,
});
}
// Remove the token because it is supposed to be short lived
localStorage.removeItem(APPSMITH_TOKEN_STORAGE_KEY);
}
@ -702,7 +717,7 @@ function* createDatasourceFromFormSaga(
formConfig,
);
const payload = _.omit(merge(initialValues, actionPayload.payload), [
const payload = omit(merge(initialValues, actionPayload.payload), [
"id",
"new",
"type",
@ -778,12 +793,12 @@ function* changeDatasourceSaga(
const draft: Record<string, unknown> = yield select(getDatasourceDraft, id);
const pageId: string = yield select(getCurrentPageId);
let data;
if (_.isEmpty(draft)) {
if (isEmpty(draft)) {
data = datasource;
} else {
data = draft;
}
yield put(initialize(DATASOURCE_DB_FORM, _.omit(data, ["name"])));
yield put(initialize(DATASOURCE_DB_FORM, omit(data, ["name"])));
// on reconnect modal, it shouldn't be redirected to datasource edit page
if (shouldNotRedirect) return;
// this redirects to the same route, so checking first.
@ -804,7 +819,7 @@ function* changeDatasourceSaga(
);
yield put(
// @ts-expect-error: data is of type unknown
updateReplayEntity(data.id, _.omit(data, ["name"]), ENTITY_TYPE.DATASOURCE),
updateReplayEntity(data.id, omit(data, ["name"]), ENTITY_TYPE.DATASOURCE),
);
}
@ -859,10 +874,10 @@ function* storeAsDatasourceSaga() {
const { values } = yield select(getFormData, API_EDITOR_FORM_NAME);
const applicationId: string = yield select(getCurrentApplicationId);
const pageId: string | undefined = yield select(getCurrentPageId);
let datasource = _.get(values, "datasource");
datasource = _.omit(datasource, ["name"]);
const originalHeaders = _.get(values, "actionConfiguration.headers", []);
const [datasourceHeaders, actionHeaders] = _.partition(
let datasource = get(values, "datasource");
datasource = omit(datasource, ["name"]);
const originalHeaders = get(values, "actionConfiguration.headers", []);
const [datasourceHeaders, actionHeaders] = partition(
originalHeaders,
({ key, value }: { key: string; value: string }) => {
return !(isDynamicValue(key) || isDynamicValue(value));
@ -881,11 +896,7 @@ function* storeAsDatasourceSaga() {
(d) => !(d.key === "" && d.key === ""),
);
_.set(
datasource,
"datasourceConfiguration.headers",
filteredDatasourceHeaders,
);
set(datasource, "datasourceConfiguration.headers", filteredDatasourceHeaders);
yield put(createTempDatasourceFromForm(datasource));
const createDatasourceSuccessAction: unknown = yield take(
@ -912,7 +923,7 @@ function* storeAsDatasourceSaga() {
function* updateDatasourceSuccessSaga(action: UpdateDatasourceSuccessAction) {
const state: AppState = yield select();
const actionRouteInfo = _.get(state, "ui.datasourcePane.actionRouteInfo");
const actionRouteInfo = get(state, "ui.datasourcePane.actionRouteInfo");
const generateCRUDSupportedPlugin: GenerateCRUDEnabledPluginMap = yield select(
getGenerateCRUDEnabledPluginMap,
);
@ -1153,6 +1164,30 @@ function* initializeFormWithDefaults(
}
}
function* filePickerActionCallbackSaga(
actionPayload: ReduxAction<{ action: string; datasourceId: string }>,
) {
try {
const { action, datasourceId } = actionPayload.payload;
yield put({
type: ReduxActionTypes.SET_GSHEET_TOKEN,
payload: {
gsheetToken: "",
},
});
if (action === "cancel") {
const datasource: Datasource = yield select(getDatasource, datasourceId);
set(
datasource,
"datasourceConfiguration.authentication.authenticationStatus",
AuthenticationStatus.FAILURE,
);
yield put(updateDatasource(datasource));
}
} catch (error) {}
}
export function* watchDatasourcesSagas() {
yield all([
takeEvery(ReduxActionTypes.FETCH_DATASOURCES_INIT, fetchDatasourcesSaga),
@ -1215,5 +1250,9 @@ export function* watchDatasourcesSagas() {
takeEvery(ReduxFormActionTypes.VALUE_CHANGE, formValueChangeSaga),
takeEvery(ReduxFormActionTypes.ARRAY_PUSH, formValueChangeSaga),
takeEvery(ReduxFormActionTypes.ARRAY_REMOVE, formValueChangeSaga),
takeEvery(
ReduxActionTypes.FILE_PICKER_CALLBACK_ACTION,
filePickerActionCallbackSaga,
),
]);
}

View File

@ -856,3 +856,6 @@ export const showCanvasTopSectionSelector = createSelector(
return true;
},
);
export const getGsheetToken = (state: AppState) =>
state.entities.datasources.gsheetToken;

View File

@ -0,0 +1,15 @@
package com.appsmith.external.models;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
@NoArgsConstructor
public class OAuthResponseDTO {
Datasource datasource;
String token;
}

View File

@ -15,6 +15,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import com.appsmith.external.models.OAuthResponseDTO;
@Slf4j
@RequestMapping(Url.SAAS_URL)
@ -40,7 +41,7 @@ public class SaasControllerCE {
}
@PostMapping("/{datasourceId}/token")
public Mono<ResponseDTO<Datasource>> getAccessToken(@PathVariable String datasourceId, @RequestParam String appsmithToken, ServerWebExchange serverWebExchange) {
public Mono<ResponseDTO<OAuthResponseDTO>> getAccessToken(@PathVariable String datasourceId, @RequestParam String appsmithToken, ServerWebExchange serverWebExchange) {
log.debug("Received callback for an OAuth2 authorization request");
return authenticationService.getAccessTokenFromCloud(datasourceId, appsmithToken)

View File

@ -1,6 +1,7 @@
package com.appsmith.server.solutions.ce;
import com.appsmith.external.models.Datasource;
import com.appsmith.external.models.OAuthResponseDTO;
import com.appsmith.server.dtos.AuthorizationCodeCallbackDTO;
import org.springframework.http.server.reactive.ServerHttpRequest;
import reactor.core.publisher.Mono;
@ -30,7 +31,7 @@ public interface AuthenticationServiceCE {
Mono<String> getAppsmithToken(String datasourceId, String pageId, String branchName, ServerHttpRequest request, String importForGit);
Mono<Datasource> getAccessTokenFromCloud(String datasourceId, String appsmithToken);
Mono<OAuthResponseDTO> getAccessTokenFromCloud(String datasourceId, String appsmithToken);
Mono<Datasource> refreshAuthentication(Datasource datasource);

View File

@ -8,6 +8,7 @@ import com.appsmith.external.helpers.SSLHelper;
import com.appsmith.external.models.AuthenticationDTO;
import com.appsmith.external.models.AuthenticationResponse;
import com.appsmith.external.models.Datasource;
import com.appsmith.external.models.OAuthResponseDTO;
import com.appsmith.external.models.DefaultResources;
import com.appsmith.external.models.OAuth2;
import com.appsmith.server.configurations.CloudServicesConfig;
@ -85,6 +86,8 @@ public class AuthenticationServiceCEImpl implements AuthenticationServiceCE {
private final ConfigService configService;
private final DatasourcePermission datasourcePermission;
private final PagePermission pagePermission;
private static final String FILE_SPECIFIC_DRIVE_SCOPE = "https://www.googleapis.com/auth/drive.file";
private static final String ACCESS_TOKEN_KEY = "access_token";
/**
* This method is used by the generic OAuth2 implementation that is used by REST APIs. Here, we only populate all the required fields
@ -385,7 +388,7 @@ public class AuthenticationServiceCEImpl implements AuthenticationServiceCE {
}));
}
public Mono<Datasource> getAccessTokenFromCloud(String datasourceId, String appsmithToken) {
public Mono<OAuthResponseDTO> getAccessTokenFromCloud(String datasourceId, String appsmithToken) {
// Check if user has access to manage datasource
// If yes, check if datasource is in intermediate state
// If yes, request for token and store in datasource
@ -437,10 +440,21 @@ public class AuthenticationServiceCEImpl implements AuthenticationServiceCE {
}
}
datasource.getDatasourceConfiguration().setAuthentication(oAuth2);
return Mono.just(datasource);
String accessToken = "";
if (oAuth2.getScope() != null && oAuth2.getScope().contains(FILE_SPECIFIC_DRIVE_SCOPE)) {
accessToken = (String) tokenResponse.get(ACCESS_TOKEN_KEY);
}
return Mono.zip(Mono.just(datasource), Mono.just(accessToken));
});
})
.flatMap(datasource -> datasourceService.update(datasource.getId(), datasource))
.flatMap(tuple -> {
Datasource datasource = tuple.getT1();
String accessToken = tuple.getT2();
OAuthResponseDTO response = new OAuthResponseDTO();
response.setDatasource(datasource);
response.setToken(accessToken);
return datasourceService.update(datasource.getId(), datasource).thenReturn(response);
})
.onErrorMap(ConnectException.class,
error -> new AppsmithException(
AppsmithError.AUTHENTICATION_FAILURE,