From 2f2f5a6bf4e6965240a58318b62903590a3ee929 Mon Sep 17 00:00:00 2001 From: Pawan Kumar Date: Wed, 25 Sep 2024 16:29:21 +0530 Subject: [PATCH] chore: Refactor API (#36412) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #36481 /ok-to-test tags="@tag.All" > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: > Commit: ed537d3958a3eba4502cbc32daf60c4cd814002d > Cypress dashboard. > Tags: `@tag.All` > Spec: >
Wed, 25 Sep 2024 08:56:31 UTC ## Summary by CodeRabbit ## Summary by CodeRabbit - **New Features** - Enhanced API interaction with new default configurations for requests. - Improved error handling with a centralized interceptor for managing various API response errors. - Introduced access control for specific API endpoints through blocked and enabled route management. - Streamlined environment-specific configurations for better maintainability. - Added functionalities for managing application themes, including fetching, updating, and deleting themes. - Introduced new API functions for retrieving consolidated page load data for viewing and editing. - Centralized access point for API services related to theming and consolidated page load data. - New modular structure for API request and response interceptors to improve organization and maintainability. - **Tests** - Added unit tests for both API response and request interceptors to ensure correct functionality and error handling. --------- Co-authored-by: Pawan Kumar --- .../packages/utils/src/compose/compose.ts | 4 + .../packages/utils/src/compose/index.ts | 1 + app/client/packages/utils/src/index.ts | 1 + app/client/src/api/Api.ts | 25 +- .../src/api/ConsolidatedPageLoadApi.tsx | 26 -- .../apiFailureResponseInterceptors.ts | 225 ++++++++++++++ .../api/__tests__/apiRequestInterceptors.ts | 121 ++++++++ .../apiSucessResponseInterceptors.ts | 40 +++ app/client/src/api/core/api.ts | 31 ++ app/client/src/api/core/factory.ts | 17 ++ app/client/src/api/core/index.ts | 2 + .../api/helpers/addExecutionMetaProperties.ts | 12 + app/client/src/api/helpers/index.ts | 3 + app/client/src/api/helpers/is404orAuthPath.ts | 5 + .../api/helpers/validateJsonResponseMeta.ts | 14 + app/client/src/api/index.ts | 1 + app/client/src/api/interceptors/index.ts | 2 + .../request/addAnonymousUserIdHeader.ts | 16 + .../request/addEnvironmentHeader.ts | 19 ++ .../request/addGitBranchHeader.ts | 16 + .../addPerformanceMonitoringHeaders.ts | 10 + .../request/addRequestedByHeader.ts | 13 + .../request/apiRequestInterceptor.ts | 64 ++++ .../request/blockAirgappedRoutes.ts | 29 ++ .../request/increaseGitApiTimeout.ts | 9 + .../src/api/interceptors/request/index.ts | 8 + .../response/apiFailureResponseInterceptor.ts | 33 ++ .../response/apiSuccessResponseInterceptor.ts | 16 + .../failureHandlers/handle413Error.ts | 25 ++ .../failureHandlers/handleCancelError.ts | 11 + .../handleExecuteActionError.ts | 14 + .../handleMissingResponseMeta.ts | 17 ++ .../failureHandlers/handleNotFoundError.ts | 34 +++ .../failureHandlers/handleOfflineError.ts | 13 + .../failureHandlers/handleServerError.ts | 15 + .../failureHandlers/handleTimeoutError.ts | 22 ++ .../handleUnauthorizedError.ts | 34 +++ .../response/failureHandlers/index.ts | 9 + .../src/api/interceptors/response/index.ts | 2 + .../src/api/services/AppThemingApi/api.ts | 38 +++ .../src/api/services/AppThemingApi/index.ts | 1 + .../services/ConsolidatedPageLoadApi/api.ts | 20 ++ .../services/ConsolidatedPageLoadApi/index.ts | 1 + app/client/src/api/services/index.ts | 2 + app/client/src/api/types.ts | 24 ++ app/client/src/ce/api/ApiUtils.test.ts | 127 -------- app/client/src/ce/api/ApiUtils.ts | 282 ------------------ app/client/src/ce/constants/ApiConstants.tsx | 42 +++ app/client/src/ce/sagas/PageSagas.tsx | 2 +- app/client/src/sagas/AppThemingSaga.tsx | 43 +-- app/client/src/sagas/DatasourcesSagas.ts | 4 +- app/client/src/sagas/ErrorSagas.tsx | 4 +- app/client/src/sagas/InitSagas.ts | 6 +- app/client/src/sagas/helper.ts | 6 +- 54 files changed, 1079 insertions(+), 482 deletions(-) create mode 100644 app/client/packages/utils/src/compose/compose.ts create mode 100644 app/client/packages/utils/src/compose/index.ts delete mode 100644 app/client/src/api/ConsolidatedPageLoadApi.tsx create mode 100644 app/client/src/api/__tests__/apiFailureResponseInterceptors.ts create mode 100644 app/client/src/api/__tests__/apiRequestInterceptors.ts create mode 100644 app/client/src/api/__tests__/apiSucessResponseInterceptors.ts create mode 100644 app/client/src/api/core/api.ts create mode 100644 app/client/src/api/core/factory.ts create mode 100644 app/client/src/api/core/index.ts create mode 100644 app/client/src/api/helpers/addExecutionMetaProperties.ts create mode 100644 app/client/src/api/helpers/index.ts create mode 100644 app/client/src/api/helpers/is404orAuthPath.ts create mode 100644 app/client/src/api/helpers/validateJsonResponseMeta.ts create mode 100644 app/client/src/api/index.ts create mode 100644 app/client/src/api/interceptors/index.ts create mode 100644 app/client/src/api/interceptors/request/addAnonymousUserIdHeader.ts create mode 100644 app/client/src/api/interceptors/request/addEnvironmentHeader.ts create mode 100644 app/client/src/api/interceptors/request/addGitBranchHeader.ts create mode 100644 app/client/src/api/interceptors/request/addPerformanceMonitoringHeaders.ts create mode 100644 app/client/src/api/interceptors/request/addRequestedByHeader.ts create mode 100644 app/client/src/api/interceptors/request/apiRequestInterceptor.ts create mode 100644 app/client/src/api/interceptors/request/blockAirgappedRoutes.ts create mode 100644 app/client/src/api/interceptors/request/increaseGitApiTimeout.ts create mode 100644 app/client/src/api/interceptors/request/index.ts create mode 100644 app/client/src/api/interceptors/response/apiFailureResponseInterceptor.ts create mode 100644 app/client/src/api/interceptors/response/apiSuccessResponseInterceptor.ts create mode 100644 app/client/src/api/interceptors/response/failureHandlers/handle413Error.ts create mode 100644 app/client/src/api/interceptors/response/failureHandlers/handleCancelError.ts create mode 100644 app/client/src/api/interceptors/response/failureHandlers/handleExecuteActionError.ts create mode 100644 app/client/src/api/interceptors/response/failureHandlers/handleMissingResponseMeta.ts create mode 100644 app/client/src/api/interceptors/response/failureHandlers/handleNotFoundError.ts create mode 100644 app/client/src/api/interceptors/response/failureHandlers/handleOfflineError.ts create mode 100644 app/client/src/api/interceptors/response/failureHandlers/handleServerError.ts create mode 100644 app/client/src/api/interceptors/response/failureHandlers/handleTimeoutError.ts create mode 100644 app/client/src/api/interceptors/response/failureHandlers/handleUnauthorizedError.ts create mode 100644 app/client/src/api/interceptors/response/failureHandlers/index.ts create mode 100644 app/client/src/api/interceptors/response/index.ts create mode 100644 app/client/src/api/services/AppThemingApi/api.ts create mode 100644 app/client/src/api/services/AppThemingApi/index.ts create mode 100644 app/client/src/api/services/ConsolidatedPageLoadApi/api.ts create mode 100644 app/client/src/api/services/ConsolidatedPageLoadApi/index.ts create mode 100644 app/client/src/api/services/index.ts create mode 100644 app/client/src/api/types.ts delete mode 100644 app/client/src/ce/api/ApiUtils.test.ts diff --git a/app/client/packages/utils/src/compose/compose.ts b/app/client/packages/utils/src/compose/compose.ts new file mode 100644 index 0000000000..3fd17574c1 --- /dev/null +++ b/app/client/packages/utils/src/compose/compose.ts @@ -0,0 +1,4 @@ +export const compose = + (...fns: Array<(arg: T) => T>) => + (x: T) => + fns.reduce((acc, fn) => fn(acc), x); diff --git a/app/client/packages/utils/src/compose/index.ts b/app/client/packages/utils/src/compose/index.ts new file mode 100644 index 0000000000..cbcc744120 --- /dev/null +++ b/app/client/packages/utils/src/compose/index.ts @@ -0,0 +1 @@ +export { compose } from "./compose"; diff --git a/app/client/packages/utils/src/index.ts b/app/client/packages/utils/src/index.ts index 7ea2e7134f..442a65511e 100644 --- a/app/client/packages/utils/src/index.ts +++ b/app/client/packages/utils/src/index.ts @@ -1 +1,2 @@ export * from "./object"; +export * from "./compose"; diff --git a/app/client/src/api/Api.ts b/app/client/src/api/Api.ts index 206f38a1f5..1eff364c4d 100644 --- a/app/client/src/api/Api.ts +++ b/app/client/src/api/Api.ts @@ -1,15 +1,13 @@ -import type { AxiosInstance, AxiosRequestConfig } from "axios"; import axios from "axios"; +import type { AxiosInstance, AxiosRequestConfig } from "axios"; +import { + apiRequestInterceptor, + apiFailureResponseInterceptor, + apiSuccessResponseInterceptor, +} from "./interceptors"; import { REQUEST_TIMEOUT_MS } from "ee/constants/ApiConstants"; import { convertObjectToQueryParams } from "utils/URLUtils"; -import { - apiFailureResponseInterceptor, - apiRequestInterceptor, - apiSuccessResponseInterceptor, - blockedApiRoutesForAirgapInterceptor, -} from "ee/api/ApiUtils"; -//TODO(abhinav): Refactor this to make more composable. export const apiRequestConfig = { baseURL: "/api/", timeout: REQUEST_TIMEOUT_MS, @@ -21,16 +19,7 @@ export const apiRequestConfig = { const axiosInstance: AxiosInstance = axios.create(); -const requestInterceptors = [ - blockedApiRoutesForAirgapInterceptor, - apiRequestInterceptor, -]; - -requestInterceptors.forEach((interceptor) => { - // TODO: Fix this the next time the file is edited - // eslint-disable-next-line @typescript-eslint/no-explicit-any - axiosInstance.interceptors.request.use(interceptor as any); -}); +axiosInstance.interceptors.request.use(apiRequestInterceptor); axiosInstance.interceptors.response.use( apiSuccessResponseInterceptor, diff --git a/app/client/src/api/ConsolidatedPageLoadApi.tsx b/app/client/src/api/ConsolidatedPageLoadApi.tsx deleted file mode 100644 index 473b9a0930..0000000000 --- a/app/client/src/api/ConsolidatedPageLoadApi.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import Api from "./Api"; -import type { AxiosPromise } from "axios"; -import type { ApiResponse } from "api/ApiResponses"; - -import type { InitConsolidatedApi } from "sagas/InitSagas"; - -class ConsolidatedPageLoadApi extends Api { - static url = "v1/consolidated-api"; - static consolidatedApiViewUrl = `${ConsolidatedPageLoadApi.url}/view`; - static consolidatedApiEditUrl = `${ConsolidatedPageLoadApi.url}/edit`; - - static async getConsolidatedPageLoadDataView(params: { - applicationId?: string; - defaultPageId?: string; - }): Promise>> { - return Api.get(ConsolidatedPageLoadApi.consolidatedApiViewUrl, params); - } - static async getConsolidatedPageLoadDataEdit(params: { - applicationId?: string; - defaultPageId?: string; - }): Promise>> { - return Api.get(ConsolidatedPageLoadApi.consolidatedApiEditUrl, params); - } -} - -export default ConsolidatedPageLoadApi; diff --git a/app/client/src/api/__tests__/apiFailureResponseInterceptors.ts b/app/client/src/api/__tests__/apiFailureResponseInterceptors.ts new file mode 100644 index 0000000000..7802d608b8 --- /dev/null +++ b/app/client/src/api/__tests__/apiFailureResponseInterceptors.ts @@ -0,0 +1,225 @@ +import axios from "axios"; +import type { AxiosError } from "axios"; + +import { + apiFailureResponseInterceptor, + apiSuccessResponseInterceptor, +} from "api/interceptors"; +import type { ApiResponse } from "api/types"; +import { + createMessage, + ERROR_0, + ERROR_413, + ERROR_500, + SERVER_API_TIMEOUT_ERROR, +} from "ee/constants/messages"; +import { ERROR_CODES } from "ee/constants/ApiConstants"; +import { UserCancelledActionExecutionError } from "sagas/ActionExecution/errorUtils"; + +describe("Api success response interceptors", () => { + beforeAll(() => { + axios.interceptors.response.use( + apiSuccessResponseInterceptor, + apiFailureResponseInterceptor, + ); + }); + + it("checks 413 error", async () => { + axios.defaults.adapter = async () => { + return Promise.reject({ + response: { + status: 413, + }, + } as AxiosError); + }; + + try { + await axios.get("https://example.com"); + } catch (error) { + expect((error as AxiosError).response?.status).toBe(413); + expect((error as AxiosError).message).toBe( + createMessage(ERROR_413, 100), + ); + expect( + (error as AxiosError & { statusCode?: string }).statusCode, + ).toBe("AE-APP-4013"); + } + + axios.defaults.adapter = undefined; + }); + + it("checks the response message when request is made when user is offline", async () => { + const onlineGetter: jest.SpyInstance = jest.spyOn( + window.navigator, + "onLine", + "get", + ); + + onlineGetter.mockReturnValue(false); + axios.defaults.adapter = async () => { + return new Promise((resolve, reject) => { + reject({ + message: "Network Error", + } as AxiosError); + }); + }; + + try { + await axios.get("https://example.com"); + } catch (error) { + expect((error as AxiosError).message).toBe( + createMessage(ERROR_0), + ); + } + + onlineGetter.mockRestore(); + axios.defaults.adapter = undefined; + }); + + it("checks if it throws UserCancelledActionExecutionError user cancels the request ", async () => { + const cancelToken = axios.CancelToken.source(); + + axios.defaults.adapter = async () => { + return new Promise((resolve, reject) => { + cancelToken.cancel("User cancelled the request"); + + reject({ + message: "User cancelled the request", + } as AxiosError); + }); + }; + + try { + await axios.get("https://example.com", { + cancelToken: cancelToken.token, + }); + } catch (error) { + expect(error).toBeInstanceOf(UserCancelledActionExecutionError); + } + + axios.defaults.adapter = undefined; + }); + + it("checks the response message when request fails for exeuction action urls", async () => { + axios.defaults.adapter = async () => { + return Promise.reject({ + response: { + status: 500, + statusText: "Internal Server Error", + headers: { + "content-length": 1, + }, + config: { + headers: { + timer: "1000", + }, + }, + }, + config: { + url: "/v1/actions/execute", + }, + }); + }; + + const url = "/v1/actions/execute"; + const response = await axios.get(url); + + expect(response).toHaveProperty("clientMeta"); + + axios.defaults.adapter = undefined; + }); + + it("checks the error response in case of timeout", async () => { + axios.defaults.adapter = async () => { + return Promise.reject({ + code: "ECONNABORTED", + message: "timeout of 1000ms exceeded", + }); + }; + + try { + await axios.get("https://example.com"); + } catch (error) { + expect((error as AxiosError).message).toBe( + createMessage(SERVER_API_TIMEOUT_ERROR), + ); + expect((error as AxiosError).code).toBe( + ERROR_CODES.REQUEST_TIMEOUT, + ); + } + + axios.defaults.adapter = undefined; + }); + + it("checks the error response in case of server error", async () => { + axios.defaults.adapter = async () => { + return Promise.reject({ + response: { + status: 502, + }, + }); + }; + + try { + await axios.get("https://example.com"); + } catch (error) { + expect((error as AxiosError).message).toBe( + createMessage(ERROR_500), + ); + expect((error as AxiosError).code).toBe( + ERROR_CODES.SERVER_ERROR, + ); + } + + axios.defaults.adapter = undefined; + }); + + it("checks error response in case of unauthorized error", async () => { + axios.defaults.adapter = async () => { + return Promise.reject({ + response: { + status: 401, + }, + }); + }; + + try { + await axios.get("https://example.com"); + } catch (error) { + expect((error as AxiosError).message).toBe( + "Unauthorized. Redirecting to login page...", + ); + expect((error as AxiosError).code).toBe( + ERROR_CODES.REQUEST_NOT_AUTHORISED, + ); + } + }); + + it("checks error response in case of not found error", async () => { + axios.defaults.adapter = async () => { + return Promise.reject({ + response: { + data: { + responseMeta: { + status: 404, + error: { + code: "AE-ACL-4004", + }, + }, + }, + }, + }); + }; + + try { + await axios.get("https://example.com"); + } catch (error) { + expect((error as AxiosError).message).toBe( + "Resource Not Found", + ); + expect((error as AxiosError).code).toBe( + ERROR_CODES.PAGE_NOT_FOUND, + ); + } + }); +}); diff --git a/app/client/src/api/__tests__/apiRequestInterceptors.ts b/app/client/src/api/__tests__/apiRequestInterceptors.ts new file mode 100644 index 0000000000..8a90efc3d4 --- /dev/null +++ b/app/client/src/api/__tests__/apiRequestInterceptors.ts @@ -0,0 +1,121 @@ +import axios from "axios"; +import { + addGitBranchHeader, + blockAirgappedRoutes, + addRequestedByHeader, + addEnvironmentHeader, + increaseGitApiTimeout, + addAnonymousUserIdHeader, + addPerformanceMonitoringHeaders, +} from "api/interceptors/request"; + +describe("Api request interceptors", () => { + beforeAll(() => { + axios.defaults.adapter = async (config) => { + return new Promise((resolve) => { + resolve({ + data: "Test data", + status: 200, + statusText: "OK", + headers: { + "content-length": 123, + "content-type": "application/json", + }, + config, + }); + }); + }; + }); + + it("checks if the request config has timer in the headers", async () => { + const url = "v1/actions/execute"; + const identifier = axios.interceptors.request.use( + addPerformanceMonitoringHeaders, + ); + const response = await axios.get(url); + + expect(response.config.headers).toHaveProperty("timer"); + + axios.interceptors.request.eject(identifier); + }); + + it("checks if the request config has anonymousUserId in the headers", async () => { + const url = "v1/actions/execute"; + const identifier = axios.interceptors.request.use((config) => + addAnonymousUserIdHeader(config, { + segmentEnabled: true, + anonymousId: "anonymousUserId", + }), + ); + const response = await axios.get(url); + + expect(response.config.headers).toHaveProperty("x-anonymous-user-id"); + expect(response.config.headers["x-anonymous-user-id"]).toBe( + "anonymousUserId", + ); + + axios.interceptors.request.eject(identifier); + }); + + it("checks if the request config has csrfToken in the headers", async () => { + const url = "v1/actions/execute"; + const identifier = axios.interceptors.request.use(addRequestedByHeader); + const response = await axios.post(url); + + expect(response.config.headers).toHaveProperty("X-Requested-By"); + expect(response.config.headers["X-Requested-By"]).toBe("Appsmith"); + + axios.interceptors.request.eject(identifier); + }); + + it("checks if the request config has gitBranch in the headers", async () => { + const url = "v1/"; + const identifier = axios.interceptors.request.use((config) => { + return addGitBranchHeader(config, { branch: "master" }); + }); + const response = await axios.get(url); + + expect(response.config.headers).toHaveProperty("branchName"); + expect(response.config.headers["branchName"]).toBe("master"); + + axios.interceptors.request.eject(identifier); + }); + + it("checks if the request config has environmentId in the headers", async () => { + const url = "v1/saas"; + const identifier = axios.interceptors.request.use((config) => { + return addEnvironmentHeader(config, { env: "default" }); + }); + + const response = await axios.get(url); + + expect(response.config.headers).toHaveProperty("X-Appsmith-EnvironmentId"); + expect(response.config.headers["X-Appsmith-EnvironmentId"]).toBe("default"); + + axios.interceptors.request.eject(identifier); + }); + + it("checks if request is 200 ok when isAirgapped is true", async () => { + const url = "v1/saas"; + const identifier = axios.interceptors.request.use((config) => { + return blockAirgappedRoutes(config, { isAirgapped: true }); + }); + const response = await axios.get(url); + + expect(response.data).toBeNull(); + expect(response.status).toBe(200); + expect(response.statusText).toBe("OK"); + + axios.interceptors.request.eject(identifier); + }); + + it("checks if the request config has a timeout of 120s", async () => { + const url = "v1/git/"; + const identifier = axios.interceptors.request.use(increaseGitApiTimeout); + const response = await axios.get(url); + + expect(response.config.timeout).toBe(120000); + + axios.interceptors.request.eject(identifier); + }); +}); diff --git a/app/client/src/api/__tests__/apiSucessResponseInterceptors.ts b/app/client/src/api/__tests__/apiSucessResponseInterceptors.ts new file mode 100644 index 0000000000..1b8963577d --- /dev/null +++ b/app/client/src/api/__tests__/apiSucessResponseInterceptors.ts @@ -0,0 +1,40 @@ +import axios from "axios"; +import { apiSuccessResponseInterceptor } from "api/interceptors"; + +describe("Api success response interceptors", () => { + beforeAll(() => { + axios.interceptors.response.use(apiSuccessResponseInterceptor); + axios.defaults.adapter = async (config) => { + return new Promise((resolve) => { + resolve({ + data: { + data: "Test data", + }, + status: 200, + statusText: "OK", + headers: { + "content-length": 123, + "content-type": "application/json", + }, + config, + }); + }); + }; + }); + + it("checks response for non-action-execution url", async () => { + const url = "/v1/sass"; + const response = await axios.get(url); + + expect(response.data).toBe("Test data"); + }); + + it("checks response for action-execution url", async () => { + const url = "/v1/actions/execute"; + const response = await axios.get(url); + + expect(response).toHaveProperty("data"); + expect(response.data).toBe("Test data"); + expect(response).toHaveProperty("clientMeta"); + }); +}); diff --git a/app/client/src/api/core/api.ts b/app/client/src/api/core/api.ts new file mode 100644 index 0000000000..ecfb9e74f0 --- /dev/null +++ b/app/client/src/api/core/api.ts @@ -0,0 +1,31 @@ +import type { AxiosResponseData } from "api/types"; + +import { apiFactory } from "./factory"; + +const apiInstance = apiFactory(); + +export async function get(...args: Parameters) { + // Note: we are passing AxiosResponseData as the second type argument to set the default type of the response data.The reason + // is we modify the response data in the responseSuccessInterceptor to return `.data` property from the axios response so that we can + // just `response.data` instead of `response.data.data`. So we have to make sure that the response data's type matches what we do in the interceptor. + return apiInstance.get>(...args); +} + +export async function post(...args: Parameters) { + return apiInstance.post>(...args); +} + +export async function put(...args: Parameters) { + return apiInstance.put>(...args); +} + +// Note: _delete is used instead of delete because delete is a reserved keyword in JavaScript +async function _delete(...args: Parameters) { + return apiInstance.delete>(...args); +} + +export { _delete as delete }; + +export async function patch(...args: Parameters) { + return apiInstance.patch>(...args); +} diff --git a/app/client/src/api/core/factory.ts b/app/client/src/api/core/factory.ts new file mode 100644 index 0000000000..b0b75ada38 --- /dev/null +++ b/app/client/src/api/core/factory.ts @@ -0,0 +1,17 @@ +import axios from "axios"; +import { DEFAULT_AXIOS_CONFIG } from "ee/constants/ApiConstants"; +import { apiRequestInterceptor } from "api/interceptors/request/apiRequestInterceptor"; +import { apiSuccessResponseInterceptor } from "api/interceptors/response/apiSuccessResponseInterceptor"; +import { apiFailureResponseInterceptor } from "api/interceptors/response/apiFailureResponseInterceptor"; + +export function apiFactory() { + const axiosInstance = axios.create(DEFAULT_AXIOS_CONFIG); + + axiosInstance.interceptors.request.use(apiRequestInterceptor); + axiosInstance.interceptors.response.use( + apiSuccessResponseInterceptor, + apiFailureResponseInterceptor, + ); + + return axiosInstance; +} diff --git a/app/client/src/api/core/index.ts b/app/client/src/api/core/index.ts new file mode 100644 index 0000000000..ed3655362f --- /dev/null +++ b/app/client/src/api/core/index.ts @@ -0,0 +1,2 @@ +export * as api from "./api"; +export { apiFactory } from "./factory"; diff --git a/app/client/src/api/helpers/addExecutionMetaProperties.ts b/app/client/src/api/helpers/addExecutionMetaProperties.ts new file mode 100644 index 0000000000..e4fba5c615 --- /dev/null +++ b/app/client/src/api/helpers/addExecutionMetaProperties.ts @@ -0,0 +1,12 @@ +import type { AxiosResponse } from "axios"; + +export const addExecutionMetaProperties = (response: AxiosResponse) => { + const clientMeta = { + size: response.headers["content-length"], + duration: Number( + performance.now() - response.config.headers.timer, + ).toFixed(), + }; + + return { ...response.data, clientMeta }; +}; diff --git a/app/client/src/api/helpers/index.ts b/app/client/src/api/helpers/index.ts new file mode 100644 index 0000000000..1cd96da7b8 --- /dev/null +++ b/app/client/src/api/helpers/index.ts @@ -0,0 +1,3 @@ +export { is404orAuthPath } from "./is404orAuthPath"; +export { validateJsonResponseMeta } from "./validateJsonResponseMeta"; +export { addExecutionMetaProperties } from "./addExecutionMetaProperties"; diff --git a/app/client/src/api/helpers/is404orAuthPath.ts b/app/client/src/api/helpers/is404orAuthPath.ts new file mode 100644 index 0000000000..10d4193ebb --- /dev/null +++ b/app/client/src/api/helpers/is404orAuthPath.ts @@ -0,0 +1,5 @@ +export const is404orAuthPath = () => { + const pathName = window.location.pathname; + + return /^\/404/.test(pathName) || /^\/user\/\w+/.test(pathName); +}; diff --git a/app/client/src/api/helpers/validateJsonResponseMeta.ts b/app/client/src/api/helpers/validateJsonResponseMeta.ts new file mode 100644 index 0000000000..de295660ab --- /dev/null +++ b/app/client/src/api/helpers/validateJsonResponseMeta.ts @@ -0,0 +1,14 @@ +import * as Sentry from "@sentry/react"; +import type { AxiosResponse } from "axios"; +import { CONTENT_TYPE_HEADER_KEY } from "constants/ApiEditorConstants/CommonApiConstants"; + +export const validateJsonResponseMeta = (response: AxiosResponse) => { + if ( + response.headers[CONTENT_TYPE_HEADER_KEY] === "application/json" && + !response.data.responseMeta + ) { + Sentry.captureException(new Error("Api responded without response meta"), { + contexts: { response: response.data }, + }); + } +}; diff --git a/app/client/src/api/index.ts b/app/client/src/api/index.ts new file mode 100644 index 0000000000..b2221a94a8 --- /dev/null +++ b/app/client/src/api/index.ts @@ -0,0 +1 @@ +export * from "./services"; diff --git a/app/client/src/api/interceptors/index.ts b/app/client/src/api/interceptors/index.ts new file mode 100644 index 0000000000..346dac3b38 --- /dev/null +++ b/app/client/src/api/interceptors/index.ts @@ -0,0 +1,2 @@ +export * from "./request"; +export * from "./response"; diff --git a/app/client/src/api/interceptors/request/addAnonymousUserIdHeader.ts b/app/client/src/api/interceptors/request/addAnonymousUserIdHeader.ts new file mode 100644 index 0000000000..d87e95c630 --- /dev/null +++ b/app/client/src/api/interceptors/request/addAnonymousUserIdHeader.ts @@ -0,0 +1,16 @@ +import type { InternalAxiosRequestConfig } from "axios"; + +export const addAnonymousUserIdHeader = ( + config: InternalAxiosRequestConfig, + options: { anonymousId?: string; segmentEnabled?: boolean }, +) => { + const { anonymousId, segmentEnabled } = options; + + config.headers = config.headers || {}; + + if (segmentEnabled && anonymousId) { + config.headers["x-anonymous-user-id"] = anonymousId; + } + + return config; +}; diff --git a/app/client/src/api/interceptors/request/addEnvironmentHeader.ts b/app/client/src/api/interceptors/request/addEnvironmentHeader.ts new file mode 100644 index 0000000000..82c9d89fc4 --- /dev/null +++ b/app/client/src/api/interceptors/request/addEnvironmentHeader.ts @@ -0,0 +1,19 @@ +import type { InternalAxiosRequestConfig } from "axios"; +import { ENV_ENABLED_ROUTES_REGEX } from "ee/constants/ApiConstants"; + +export const addEnvironmentHeader = ( + config: InternalAxiosRequestConfig, + options: { env: string }, +) => { + const { env } = options; + + config.headers = config.headers || {}; + + if (ENV_ENABLED_ROUTES_REGEX.test(config.url?.split("?")[0] || "")) { + if (env) { + config.headers["X-Appsmith-EnvironmentId"] = env; + } + } + + return config; +}; diff --git a/app/client/src/api/interceptors/request/addGitBranchHeader.ts b/app/client/src/api/interceptors/request/addGitBranchHeader.ts new file mode 100644 index 0000000000..b15105bcb8 --- /dev/null +++ b/app/client/src/api/interceptors/request/addGitBranchHeader.ts @@ -0,0 +1,16 @@ +import type { InternalAxiosRequestConfig } from "axios"; + +export const addGitBranchHeader = ( + config: InternalAxiosRequestConfig, + options: { branch?: string }, +) => { + const { branch } = options; + + config.headers = config.headers || {}; + + if (branch) { + config.headers.branchName = branch; + } + + return config; +}; diff --git a/app/client/src/api/interceptors/request/addPerformanceMonitoringHeaders.ts b/app/client/src/api/interceptors/request/addPerformanceMonitoringHeaders.ts new file mode 100644 index 0000000000..53ec7ad1bd --- /dev/null +++ b/app/client/src/api/interceptors/request/addPerformanceMonitoringHeaders.ts @@ -0,0 +1,10 @@ +import type { InternalAxiosRequestConfig } from "axios"; + +export const addPerformanceMonitoringHeaders = ( + config: InternalAxiosRequestConfig, +) => { + config.headers = config.headers || {}; + config.headers["timer"] = performance.now(); + + return config; +}; diff --git a/app/client/src/api/interceptors/request/addRequestedByHeader.ts b/app/client/src/api/interceptors/request/addRequestedByHeader.ts new file mode 100644 index 0000000000..c64a363dc8 --- /dev/null +++ b/app/client/src/api/interceptors/request/addRequestedByHeader.ts @@ -0,0 +1,13 @@ +import type { InternalAxiosRequestConfig } from "axios"; + +export const addRequestedByHeader = (config: InternalAxiosRequestConfig) => { + config.headers = config.headers || {}; + + const methodUpper = config.method?.toUpperCase(); + + if (methodUpper && methodUpper !== "GET" && methodUpper !== "HEAD") { + config.headers["X-Requested-By"] = "Appsmith"; + } + + return config; +}; diff --git a/app/client/src/api/interceptors/request/apiRequestInterceptor.ts b/app/client/src/api/interceptors/request/apiRequestInterceptor.ts new file mode 100644 index 0000000000..8f7919c067 --- /dev/null +++ b/app/client/src/api/interceptors/request/apiRequestInterceptor.ts @@ -0,0 +1,64 @@ +import store from "store"; +import { compose } from "@appsmith/utils"; +import { getAppsmithConfigs } from "ee/configs"; +import AnalyticsUtil from "ee/utils/AnalyticsUtil"; +import { isAirgapped } from "ee/utils/airgapHelpers"; +import type { InternalAxiosRequestConfig } from "axios"; + +import getQueryParamsObject from "utils/getQueryParamsObject"; +import { addRequestedByHeader } from "./addRequestedByHeader"; +import { increaseGitApiTimeout } from "./increaseGitApiTimeout"; +import { getCurrentGitBranch } from "selectors/gitSyncSelectors"; +import { getCurrentEnvironmentId } from "ee/selectors/environmentSelectors"; +import { addGitBranchHeader as _addGitBranchHeader } from "./addGitBranchHeader"; +import { addPerformanceMonitoringHeaders } from "./addPerformanceMonitoringHeaders"; +import { addEnvironmentHeader as _addEnvironmentHeader } from "./addEnvironmentHeader"; +import { blockAirgappedRoutes as _blockAirgappedRoutes } from "./blockAirgappedRoutes"; +import { addAnonymousUserIdHeader as _addAnonymousUserIdHeader } from "./addAnonymousUserIdHeader"; + +/** + * Note: Why can't we use store.getState() or isGapgapped() directly in the interceptor? + * The main reason is to easily test the interceptor. When we use store.getState() or isAirgapped() directly in the interceptor, + * we need to mock the store or isAirgapped() in the test file and it becomes difficult and messy mocking things just to test the interceptor. + */ +const blockAirgappedRoutes = (config: InternalAxiosRequestConfig) => { + const isAirgappedInstance = isAirgapped(); + + return _blockAirgappedRoutes(config, { isAirgapped: isAirgappedInstance }); +}; + +const addGitBranchHeader = (config: InternalAxiosRequestConfig) => { + const state = store.getState(); + const branch = getCurrentGitBranch(state) || getQueryParamsObject().branch; + + return _addGitBranchHeader(config, { branch }); +}; + +const addEnvironmentHeader = (config: InternalAxiosRequestConfig) => { + const state = store.getState(); + const activeEnv = getCurrentEnvironmentId(state); + + return _addEnvironmentHeader(config, { env: activeEnv }); +}; + +const addAnonymousUserIdHeader = (config: InternalAxiosRequestConfig) => { + const appsmithConfig = getAppsmithConfigs(); + const anonymousId = AnalyticsUtil.getAnonymousId(); + const segmentEnabled = appsmithConfig.segment.enabled; + + return _addAnonymousUserIdHeader(config, { anonymousId, segmentEnabled }); +}; + +export const apiRequestInterceptor = (config: InternalAxiosRequestConfig) => { + const interceptorPipeline = compose( + blockAirgappedRoutes, + addRequestedByHeader, + addGitBranchHeader, + increaseGitApiTimeout, + addEnvironmentHeader, + addAnonymousUserIdHeader, + addPerformanceMonitoringHeaders, + ); + + return interceptorPipeline(config); +}; diff --git a/app/client/src/api/interceptors/request/blockAirgappedRoutes.ts b/app/client/src/api/interceptors/request/blockAirgappedRoutes.ts new file mode 100644 index 0000000000..d8d4d54ee8 --- /dev/null +++ b/app/client/src/api/interceptors/request/blockAirgappedRoutes.ts @@ -0,0 +1,29 @@ +import type { InternalAxiosRequestConfig } from "axios"; +import { BLOCKED_ROUTES_REGEX } from "ee/constants/ApiConstants"; + +const blockAirgappedRoutes = ( + request: InternalAxiosRequestConfig, + options: { isAirgapped: boolean }, +) => { + const { url } = request; + const { isAirgapped } = options; + + if (isAirgapped && url && BLOCKED_ROUTES_REGEX.test(url)) { + request.adapter = async (config) => { + return new Promise((resolve) => { + resolve({ + data: null, + status: 200, + statusText: "OK", + headers: {}, + config, + request, + }); + }); + }; + } + + return request; +}; + +export { blockAirgappedRoutes }; diff --git a/app/client/src/api/interceptors/request/increaseGitApiTimeout.ts b/app/client/src/api/interceptors/request/increaseGitApiTimeout.ts new file mode 100644 index 0000000000..4aa20d7611 --- /dev/null +++ b/app/client/src/api/interceptors/request/increaseGitApiTimeout.ts @@ -0,0 +1,9 @@ +import type { InternalAxiosRequestConfig } from "axios"; + +export const increaseGitApiTimeout = (config: InternalAxiosRequestConfig) => { + if (config.url?.indexOf("/git/") !== -1) { + config.timeout = 1000 * 120; + } + + return config; +}; diff --git a/app/client/src/api/interceptors/request/index.ts b/app/client/src/api/interceptors/request/index.ts new file mode 100644 index 0000000000..6f9957f00e --- /dev/null +++ b/app/client/src/api/interceptors/request/index.ts @@ -0,0 +1,8 @@ +export { addGitBranchHeader } from "./addGitBranchHeader"; +export { blockAirgappedRoutes } from "./blockAirgappedRoutes"; +export { addRequestedByHeader } from "./addRequestedByHeader"; +export { addEnvironmentHeader } from "./addEnvironmentHeader"; +export { apiRequestInterceptor } from "./apiRequestInterceptor"; +export { increaseGitApiTimeout } from "./increaseGitApiTimeout"; +export { addAnonymousUserIdHeader } from "./addAnonymousUserIdHeader"; +export { addPerformanceMonitoringHeaders } from "./addPerformanceMonitoringHeaders"; diff --git a/app/client/src/api/interceptors/response/apiFailureResponseInterceptor.ts b/app/client/src/api/interceptors/response/apiFailureResponseInterceptor.ts new file mode 100644 index 0000000000..eae6f61621 --- /dev/null +++ b/app/client/src/api/interceptors/response/apiFailureResponseInterceptor.ts @@ -0,0 +1,33 @@ +import type { AxiosError } from "axios"; +import type { ApiResponse, ErrorHandler } from "api/types"; + +import * as failureHandlers from "./failureHandlers"; + +export const apiFailureResponseInterceptor = async ( + error: AxiosError, +) => { + const handlers: ErrorHandler[] = [ + failureHandlers.handle413Error, + failureHandlers.handleOfflineError, + failureHandlers.handleCancelError, + failureHandlers.handleExecuteActionError, + failureHandlers.handleTimeoutError, + failureHandlers.handleServerError, + failureHandlers.handleUnauthorizedError, + failureHandlers.handleNotFoundError, + ]; + + for (const handler of handlers) { + const result = await handler(error); + + if (result !== null) { + return result; + } + } + + if (error?.response?.data.responseMeta) { + return Promise.resolve(error.response.data); + } + + return Promise.resolve(error); +}; diff --git a/app/client/src/api/interceptors/response/apiSuccessResponseInterceptor.ts b/app/client/src/api/interceptors/response/apiSuccessResponseInterceptor.ts new file mode 100644 index 0000000000..ca18a9ea43 --- /dev/null +++ b/app/client/src/api/interceptors/response/apiSuccessResponseInterceptor.ts @@ -0,0 +1,16 @@ +import { + validateJsonResponseMeta, + addExecutionMetaProperties, +} from "api/helpers"; +import type { AxiosResponse } from "axios"; +import { EXECUTION_ACTION_REGEX } from "ee/constants/ApiConstants"; + +export const apiSuccessResponseInterceptor = (response: AxiosResponse) => { + if (response?.config?.url?.match(EXECUTION_ACTION_REGEX)) { + return addExecutionMetaProperties(response); + } + + validateJsonResponseMeta(response); + + return response.data; +}; diff --git a/app/client/src/api/interceptors/response/failureHandlers/handle413Error.ts b/app/client/src/api/interceptors/response/failureHandlers/handle413Error.ts new file mode 100644 index 0000000000..301b1c2c97 --- /dev/null +++ b/app/client/src/api/interceptors/response/failureHandlers/handle413Error.ts @@ -0,0 +1,25 @@ +import type { AxiosError } from "axios"; +import { + createMessage, + ERROR_413, + GENERIC_API_EXECUTION_ERROR, +} from "ee/constants/messages"; + +export const handle413Error = async (error: AxiosError) => { + if (error?.response?.status === 413) { + return Promise.reject({ + ...error, + clientDefinedError: true, + statusCode: "AE-APP-4013", + message: createMessage(ERROR_413, 100), + pluginErrorDetails: { + appsmithErrorCode: "AE-APP-4013", + appsmithErrorMessage: createMessage(ERROR_413, 100), + errorType: "INTERNAL_ERROR", // this value is from the server, hence cannot construct enum type. + title: createMessage(GENERIC_API_EXECUTION_ERROR), + }, + }); + } + + return null; +}; diff --git a/app/client/src/api/interceptors/response/failureHandlers/handleCancelError.ts b/app/client/src/api/interceptors/response/failureHandlers/handleCancelError.ts new file mode 100644 index 0000000000..f579d8bc90 --- /dev/null +++ b/app/client/src/api/interceptors/response/failureHandlers/handleCancelError.ts @@ -0,0 +1,11 @@ +import axios from "axios"; +import type { AxiosError } from "axios"; +import { UserCancelledActionExecutionError } from "sagas/ActionExecution/errorUtils"; + +export async function handleCancelError(error: AxiosError) { + if (axios.isCancel(error)) { + throw new UserCancelledActionExecutionError(); + } + + return null; +} diff --git a/app/client/src/api/interceptors/response/failureHandlers/handleExecuteActionError.ts b/app/client/src/api/interceptors/response/failureHandlers/handleExecuteActionError.ts new file mode 100644 index 0000000000..6a1e0c4f27 --- /dev/null +++ b/app/client/src/api/interceptors/response/failureHandlers/handleExecuteActionError.ts @@ -0,0 +1,14 @@ +import type { AxiosError, AxiosResponse } from "axios"; +import { addExecutionMetaProperties } from "api/helpers"; +import { EXECUTION_ACTION_REGEX } from "ee/constants/ApiConstants"; + +export function handleExecuteActionError(error: AxiosError) { + const isExecutionActionURL = + error.config && error?.config?.url?.match(EXECUTION_ACTION_REGEX); + + if (isExecutionActionURL) { + return addExecutionMetaProperties(error?.response as AxiosResponse); + } + + return null; +} diff --git a/app/client/src/api/interceptors/response/failureHandlers/handleMissingResponseMeta.ts b/app/client/src/api/interceptors/response/failureHandlers/handleMissingResponseMeta.ts new file mode 100644 index 0000000000..4153a6f868 --- /dev/null +++ b/app/client/src/api/interceptors/response/failureHandlers/handleMissingResponseMeta.ts @@ -0,0 +1,17 @@ +import type { AxiosError } from "axios"; +import * as Sentry from "@sentry/react"; +import type { ApiResponse } from "api/types"; + +export const handleMissingResponseMeta = async ( + error: AxiosError, +) => { + if (error.response?.data && !error.response.data.responseMeta) { + Sentry.captureException(new Error("Api responded without response meta"), { + contexts: { response: { ...error.response.data } }, + }); + + return Promise.reject(error.response.data); + } + + return null; +}; diff --git a/app/client/src/api/interceptors/response/failureHandlers/handleNotFoundError.ts b/app/client/src/api/interceptors/response/failureHandlers/handleNotFoundError.ts new file mode 100644 index 0000000000..21e2aa42f3 --- /dev/null +++ b/app/client/src/api/interceptors/response/failureHandlers/handleNotFoundError.ts @@ -0,0 +1,34 @@ +import { + API_STATUS_CODES, + ERROR_CODES, + SERVER_ERROR_CODES, +} from "ee/constants/ApiConstants"; +import * as Sentry from "@sentry/react"; +import type { AxiosError } from "axios"; +import type { ApiResponse } from "api/types"; +import { is404orAuthPath } from "api/helpers"; + +export async function handleNotFoundError(error: AxiosError) { + if (is404orAuthPath()) return null; + + const errorData = + error?.response?.data.responseMeta ?? ({} as ApiResponse["responseMeta"]); + + if ( + errorData.status === API_STATUS_CODES.RESOURCE_NOT_FOUND && + errorData.error?.code && + (SERVER_ERROR_CODES.RESOURCE_NOT_FOUND.includes(errorData.error?.code) || + SERVER_ERROR_CODES.UNABLE_TO_FIND_PAGE.includes(errorData?.error?.code)) + ) { + Sentry.captureException(error); + + return Promise.reject({ + ...error, + code: ERROR_CODES.PAGE_NOT_FOUND, + message: "Resource Not Found", + show: false, + }); + } + + return null; +} diff --git a/app/client/src/api/interceptors/response/failureHandlers/handleOfflineError.ts b/app/client/src/api/interceptors/response/failureHandlers/handleOfflineError.ts new file mode 100644 index 0000000000..2fa9fa1649 --- /dev/null +++ b/app/client/src/api/interceptors/response/failureHandlers/handleOfflineError.ts @@ -0,0 +1,13 @@ +import type { AxiosError } from "axios"; +import { createMessage, ERROR_0 } from "ee/constants/messages"; + +export const handleOfflineError = async (error: AxiosError) => { + if (!window.navigator.onLine) { + return Promise.reject({ + ...error, + message: createMessage(ERROR_0), + }); + } + + return null; +}; diff --git a/app/client/src/api/interceptors/response/failureHandlers/handleServerError.ts b/app/client/src/api/interceptors/response/failureHandlers/handleServerError.ts new file mode 100644 index 0000000000..2092d72b00 --- /dev/null +++ b/app/client/src/api/interceptors/response/failureHandlers/handleServerError.ts @@ -0,0 +1,15 @@ +import type { AxiosError } from "axios"; +import { createMessage, ERROR_500 } from "ee/constants/messages"; +import { API_STATUS_CODES, ERROR_CODES } from "ee/constants/ApiConstants"; + +export const handleServerError = async (error: AxiosError) => { + if (error.response?.status === API_STATUS_CODES.SERVER_ERROR) { + return Promise.reject({ + ...error, + code: ERROR_CODES.SERVER_ERROR, + message: createMessage(ERROR_500), + }); + } + + return null; +}; diff --git a/app/client/src/api/interceptors/response/failureHandlers/handleTimeoutError.ts b/app/client/src/api/interceptors/response/failureHandlers/handleTimeoutError.ts new file mode 100644 index 0000000000..ffff3eafe7 --- /dev/null +++ b/app/client/src/api/interceptors/response/failureHandlers/handleTimeoutError.ts @@ -0,0 +1,22 @@ +import type { AxiosError } from "axios"; +import { + ERROR_CODES, + TIMEOUT_ERROR_REGEX, + AXIOS_CONNECTION_ABORTED_CODE, +} from "ee/constants/ApiConstants"; +import { createMessage, SERVER_API_TIMEOUT_ERROR } from "ee/constants/messages"; + +export const handleTimeoutError = async (error: AxiosError) => { + if ( + error.code === AXIOS_CONNECTION_ABORTED_CODE && + error.message?.match(TIMEOUT_ERROR_REGEX) + ) { + return Promise.reject({ + ...error, + message: createMessage(SERVER_API_TIMEOUT_ERROR), + code: ERROR_CODES.REQUEST_TIMEOUT, + }); + } + + return null; +}; diff --git a/app/client/src/api/interceptors/response/failureHandlers/handleUnauthorizedError.ts b/app/client/src/api/interceptors/response/failureHandlers/handleUnauthorizedError.ts new file mode 100644 index 0000000000..343ec7022e --- /dev/null +++ b/app/client/src/api/interceptors/response/failureHandlers/handleUnauthorizedError.ts @@ -0,0 +1,34 @@ +import store from "store"; +import type { AxiosError } from "axios"; +import * as Sentry from "@sentry/react"; +import { is404orAuthPath } from "api/helpers"; +import { logoutUser } from "actions/userActions"; +import { AUTH_LOGIN_URL } from "constants/routes"; +import { API_STATUS_CODES, ERROR_CODES } from "ee/constants/ApiConstants"; + +export const handleUnauthorizedError = async (error: AxiosError) => { + if (is404orAuthPath()) return null; + + if (error.response?.status === API_STATUS_CODES.REQUEST_NOT_AUTHORISED) { + const currentUrl = `${window.location.href}`; + + store.dispatch( + logoutUser({ + redirectURL: `${AUTH_LOGIN_URL}?redirectUrl=${encodeURIComponent( + currentUrl, + )}`, + }), + ); + + Sentry.captureException(error); + + return Promise.reject({ + ...error, + code: ERROR_CODES.REQUEST_NOT_AUTHORISED, + message: "Unauthorized. Redirecting to login page...", + show: false, + }); + } + + return null; +}; diff --git a/app/client/src/api/interceptors/response/failureHandlers/index.ts b/app/client/src/api/interceptors/response/failureHandlers/index.ts new file mode 100644 index 0000000000..2352095f1d --- /dev/null +++ b/app/client/src/api/interceptors/response/failureHandlers/index.ts @@ -0,0 +1,9 @@ +export { handle413Error } from "./handle413Error"; +export { handleServerError } from "./handleServerError"; +export { handleCancelError } from "./handleCancelError"; +export { handleOfflineError } from "./handleOfflineError"; +export { handleTimeoutError } from "./handleTimeoutError"; +export { handleNotFoundError } from "./handleNotFoundError"; +export { handleUnauthorizedError } from "./handleUnauthorizedError"; +export { handleExecuteActionError } from "./handleExecuteActionError"; +export { handleMissingResponseMeta } from "./handleMissingResponseMeta"; diff --git a/app/client/src/api/interceptors/response/index.ts b/app/client/src/api/interceptors/response/index.ts new file mode 100644 index 0000000000..5177d54592 --- /dev/null +++ b/app/client/src/api/interceptors/response/index.ts @@ -0,0 +1,2 @@ +export { apiFailureResponseInterceptor } from "./apiFailureResponseInterceptor"; +export { apiSuccessResponseInterceptor } from "./apiSuccessResponseInterceptor"; diff --git a/app/client/src/api/services/AppThemingApi/api.ts b/app/client/src/api/services/AppThemingApi/api.ts new file mode 100644 index 0000000000..e7d22dfed8 --- /dev/null +++ b/app/client/src/api/services/AppThemingApi/api.ts @@ -0,0 +1,38 @@ +import { api } from "api/core"; +import type { AppTheme } from "entities/AppTheming"; + +const baseURL = "/v1"; + +export async function fetchThemes(applicationId: string) { + const url = `${baseURL}/themes/applications/${applicationId}`; + + return api.get(url); +} + +export async function fetchSelected(applicationId: string, mode = "EDIT") { + const url = `${baseURL}/themes/applications/${applicationId}/current`; + + return api.get(url, { params: { mode } }); +} + +export async function updateTheme(applicationId: string, theme: AppTheme) { + const url = `${baseURL}/themes/applications/${applicationId}`; + const payload = { + ...theme, + new: undefined, + }; + + return api.put(url, payload); +} + +export async function changeTheme(applicationId: string, theme: AppTheme) { + const url = `${baseURL}/applications/${applicationId}/themes/${theme.id}`; + + return api.patch(url, theme); +} + +export async function deleteTheme(themeId: string) { + const url = `${baseURL}/themes/${themeId}`; + + return api.delete(url); +} diff --git a/app/client/src/api/services/AppThemingApi/index.ts b/app/client/src/api/services/AppThemingApi/index.ts new file mode 100644 index 0000000000..d158c57640 --- /dev/null +++ b/app/client/src/api/services/AppThemingApi/index.ts @@ -0,0 +1 @@ +export * from "./api"; diff --git a/app/client/src/api/services/ConsolidatedPageLoadApi/api.ts b/app/client/src/api/services/ConsolidatedPageLoadApi/api.ts new file mode 100644 index 0000000000..4772853c90 --- /dev/null +++ b/app/client/src/api/services/ConsolidatedPageLoadApi/api.ts @@ -0,0 +1,20 @@ +import { api } from "api/core"; +import type { InitConsolidatedApi } from "sagas/InitSagas"; + +const BASE_URL = "v1/consolidated-api"; +const VIEW_URL = `${BASE_URL}/view`; +const EDIT_URL = `${BASE_URL}/edit`; + +export const getConsolidatedPageLoadDataView = async (params: { + applicationId?: string; + defaultPageId?: string; +}) => { + return api.get(VIEW_URL, { params }); +}; + +export const getConsolidatedPageLoadDataEdit = async (params: { + applicationId?: string; + defaultPageId?: string; +}) => { + return api.get(EDIT_URL, { params }); +}; diff --git a/app/client/src/api/services/ConsolidatedPageLoadApi/index.ts b/app/client/src/api/services/ConsolidatedPageLoadApi/index.ts new file mode 100644 index 0000000000..d158c57640 --- /dev/null +++ b/app/client/src/api/services/ConsolidatedPageLoadApi/index.ts @@ -0,0 +1 @@ +export * from "./api"; diff --git a/app/client/src/api/services/index.ts b/app/client/src/api/services/index.ts new file mode 100644 index 0000000000..b1e3b87748 --- /dev/null +++ b/app/client/src/api/services/index.ts @@ -0,0 +1,2 @@ +export * as AppThemingApi from "./AppThemingApi"; +export * as ConsolidatedPageLoadApi from "./ConsolidatedPageLoadApi"; diff --git a/app/client/src/api/types.ts b/app/client/src/api/types.ts new file mode 100644 index 0000000000..4ce60997ef --- /dev/null +++ b/app/client/src/api/types.ts @@ -0,0 +1,24 @@ +import type { AxiosError, AxiosResponse } from "axios"; + +export interface ApiResponseError { + code: string; + message: string; +} + +export interface ApiResponseMeta { + status: number; + success: boolean; + error?: ApiResponseError; +} + +export interface ApiResponse { + responseMeta: ApiResponseMeta; + data: T; + code?: string; +} + +export type AxiosResponseData = AxiosResponse>["data"]; + +export type ErrorHandler = ( + error: AxiosError, +) => Promise; diff --git a/app/client/src/ce/api/ApiUtils.test.ts b/app/client/src/ce/api/ApiUtils.test.ts deleted file mode 100644 index c1848f7760..0000000000 --- a/app/client/src/ce/api/ApiUtils.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { - apiRequestInterceptor, - apiSuccessResponseInterceptor, - apiFailureResponseInterceptor, - axiosConnectionAbortedCode, -} from "./ApiUtils"; -import type { AxiosRequestConfig, AxiosResponse } from "axios"; -import type { ActionExecutionResponse } from "api/ActionAPI"; -import { - createMessage, - ERROR_0, - SERVER_API_TIMEOUT_ERROR, -} from "ee/constants/messages"; -import { ERROR_CODES } from "ee/constants/ApiConstants"; -import * as Sentry from "@sentry/react"; - -describe("axios api interceptors", () => { - describe("Axios api request interceptor", () => { - it("adds timer to the request object", () => { - const request: AxiosRequestConfig = { - url: "https://app.appsmith.com/v1/api/actions/execute", - }; - const interceptedRequest = apiRequestInterceptor(request); - - expect(interceptedRequest).toHaveProperty("timer"); - }); - }); - - describe("Axios api response success interceptor", () => { - it("transforms an action execution response", () => { - const response: AxiosResponse = { - data: "Test data", - headers: { - "content-length": 123, - "content-type": "application/json", - }, - config: { - url: "https://app.appsmith.com/v1/api/actions/execute", - // @ts-expect-error: type mismatch - timer: 0, - }, - }; - - const interceptedResponse: ActionExecutionResponse = - apiSuccessResponseInterceptor(response); - - expect(interceptedResponse).toHaveProperty("clientMeta"); - expect(interceptedResponse.clientMeta).toHaveProperty("size"); - expect(interceptedResponse.clientMeta.size).toBe(123); - expect(interceptedResponse.clientMeta).toHaveProperty("duration"); - }); - - it("just returns the response data for other requests", () => { - const response: AxiosResponse = { - data: "Test data", - headers: { - "content-type": "application/json", - }, - config: { - url: "https://app.appsmith.com/v1/api/actions", - //@ts-expect-error: type mismatch - timer: 0, - }, - }; - - const interceptedResponse: ActionExecutionResponse = - apiSuccessResponseInterceptor(response); - - expect(interceptedResponse).toBe("Test data"); - }); - }); - - describe("Api response failure interceptor", () => { - beforeEach(() => { - jest.restoreAllMocks(); - }); - - it("checks for no internet errors", () => { - jest.spyOn(navigator, "onLine", "get").mockReturnValue(false); - const interceptedResponse = apiFailureResponseInterceptor({}); - - expect(interceptedResponse).rejects.toStrictEqual({ - message: createMessage(ERROR_0), - }); - }); - - it.todo("handles axios cancel gracefully"); - - it("handles timeout errors", () => { - const error = { - code: axiosConnectionAbortedCode, - message: "timeout of 10000ms exceeded", - }; - const interceptedResponse = apiFailureResponseInterceptor(error); - - expect(interceptedResponse).rejects.toStrictEqual({ - message: createMessage(SERVER_API_TIMEOUT_ERROR), - code: ERROR_CODES.REQUEST_TIMEOUT, - }); - }); - - it("checks for response meta", () => { - const sentrySpy = jest.spyOn(Sentry, "captureException"); - const response: AxiosResponse = { - data: "Test data", - headers: { - "content-type": "application/json", - }, - config: { - url: "https://app.appsmith.com/v1/api/user", - //@ts-expect-error: type mismatch - timer: 0, - }, - }; - - apiSuccessResponseInterceptor(response); - expect(sentrySpy).toHaveBeenCalled(); - - const interceptedFailureResponse = apiFailureResponseInterceptor({ - response, - }); - - expect(interceptedFailureResponse).rejects.toStrictEqual("Test data"); - expect(sentrySpy).toHaveBeenCalled(); - }); - }); -}); diff --git a/app/client/src/ce/api/ApiUtils.ts b/app/client/src/ce/api/ApiUtils.ts index b157db3668..9db9f45e1d 100644 --- a/app/client/src/ce/api/ApiUtils.ts +++ b/app/client/src/ce/api/ApiUtils.ts @@ -1,289 +1,7 @@ -import { - createMessage, - ERROR_0, - ERROR_413, - ERROR_500, - GENERIC_API_EXECUTION_ERROR, - SERVER_API_TIMEOUT_ERROR, -} from "ee/constants/messages"; -import type { AxiosRequestConfig, AxiosResponse } from "axios"; -import axios from "axios"; -import { - API_STATUS_CODES, - ERROR_CODES, - SERVER_ERROR_CODES, -} from "ee/constants/ApiConstants"; -import log from "loglevel"; -import type { ActionExecutionResponse } from "api/ActionAPI"; -import store from "store"; -import { logoutUser } from "actions/userActions"; -import { AUTH_LOGIN_URL } from "constants/routes"; -import { getCurrentGitBranch } from "selectors/gitSyncSelectors"; -import getQueryParamsObject from "utils/getQueryParamsObject"; -import { UserCancelledActionExecutionError } from "sagas/ActionExecution/errorUtils"; -import AnalyticsUtil from "ee/utils/AnalyticsUtil"; -import { getAppsmithConfigs } from "ee/configs"; -import * as Sentry from "@sentry/react"; -import { CONTENT_TYPE_HEADER_KEY } from "constants/ApiEditorConstants/CommonApiConstants"; -import { isAirgapped } from "ee/utils/airgapHelpers"; -import { getCurrentEnvironmentId } from "ee/selectors/environmentSelectors"; import { UNUSED_ENV_ID } from "constants/EnvironmentContants"; -import { ID_EXTRACTION_REGEX } from "ee/constants/routes/appRoutes"; - -const executeActionRegex = /actions\/execute/; -const timeoutErrorRegex = /timeout of (\d+)ms exceeded/; - -export const axiosConnectionAbortedCode = "ECONNABORTED"; -const appsmithConfig = getAppsmithConfigs(); export const DEFAULT_ENV_ID = UNUSED_ENV_ID; -export const BLOCKED_ROUTES = [ - "v1/app-templates", - "v1/datasources/mocks", - "v1/usage-pulse", - "v1/applications/releaseItems", - "v1/saas", -]; - -export const BLOCKED_ROUTES_REGEX = new RegExp( - `^(${BLOCKED_ROUTES.join("|")})($|/)`, -); - -export const ENV_ENABLED_ROUTES = [ - `v1/datasources/${ID_EXTRACTION_REGEX}/structure`, - `/v1/datasources/${ID_EXTRACTION_REGEX}/trigger`, - "v1/actions/execute", - "v1/saas", -]; - -export const ENV_ENABLED_ROUTES_REGEX = new RegExp( - `^(${ENV_ENABLED_ROUTES.join("|")})($|/)`, -); - -// TODO: Fix this the next time the file is edited -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const makeExecuteActionResponse = (response: any): ActionExecutionResponse => ({ - ...response.data, - clientMeta: { - size: response.headers["content-length"], - duration: Number(performance.now() - response.config.timer).toFixed(), - }, -}); - -const is404orAuthPath = () => { - const pathName = window.location.pathname; - - return /^\/404/.test(pathName) || /^\/user\/\w+/.test(pathName); -}; - -export const blockedApiRoutesForAirgapInterceptor = async ( - config: AxiosRequestConfig, -) => { - const { url } = config; - - const isAirgappedInstance = isAirgapped(); - - if (isAirgappedInstance && url && BLOCKED_ROUTES_REGEX.test(url)) { - return Promise.resolve({ data: null, status: 200 }); - } - - return config; -}; - -// Request interceptor will add a timer property to the request. -// this will be used to calculate the time taken for an action -// execution request -export const apiRequestInterceptor = (config: AxiosRequestConfig) => { - config.headers = config.headers ?? {}; - - // Add header for CSRF protection. - const methodUpper = config.method?.toUpperCase(); - - if (methodUpper && methodUpper !== "GET" && methodUpper !== "HEAD") { - config.headers["X-Requested-By"] = "Appsmith"; - } - - const state = store.getState(); - const branch = getCurrentGitBranch(state) || getQueryParamsObject().branch; - - if (branch && config.headers) { - config.headers.branchName = branch; - } - - if (config.url?.indexOf("/git/") !== -1) { - config.timeout = 1000 * 120; // increase timeout for git specific APIs - } - - if (ENV_ENABLED_ROUTES_REGEX.test(config.url?.split("?")[0] || "")) { - // Add header for environment name - const activeEnv = getCurrentEnvironmentId(state); - - if (activeEnv && config.headers) { - config.headers["X-Appsmith-EnvironmentId"] = activeEnv; - } - } - - const anonymousId = AnalyticsUtil.getAnonymousId(); - - appsmithConfig.segment.enabled && - anonymousId && - (config.headers["x-anonymous-user-id"] = anonymousId); - - return { ...config, timer: performance.now() }; -}; - -// On success of an API, if the api is an action execution, -// add the client meta object with size and time taken info -// otherwise just return the data -export const apiSuccessResponseInterceptor = ( - response: AxiosResponse, -): AxiosResponse["data"] => { - if (response.config.url) { - if (response.config.url.match(executeActionRegex)) { - return makeExecuteActionResponse(response); - } - } - - if ( - response.headers[CONTENT_TYPE_HEADER_KEY] === "application/json" && - !response.data.responseMeta - ) { - Sentry.captureException(new Error("Api responded without response meta"), { - contexts: { response: response.data }, - }); - } - - return response.data; -}; - -// Handle different api failure scenarios -// TODO: Fix this the next time the file is edited -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const apiFailureResponseInterceptor = async (error: any) => { - // this can be extended to other errors we want to catch. - // in this case it is 413. - if (error && error?.response && error?.response.status === 413) { - return Promise.reject({ - ...error, - clientDefinedError: true, - statusCode: "AE-APP-4013", - message: createMessage(ERROR_413, 100), - pluginErrorDetails: { - appsmithErrorCode: "AE-APP-4013", - appsmithErrorMessage: createMessage(ERROR_413, 100), - errorType: "INTERNAL_ERROR", // this value is from the server, hence cannot construct enum type. - title: createMessage(GENERIC_API_EXECUTION_ERROR), - }, - }); - } - - // Return error when there is no internet - if (!window.navigator.onLine) { - return Promise.reject({ - ...error, - message: createMessage(ERROR_0), - }); - } - - // Return if the call was cancelled via cancel token - if (axios.isCancel(error)) { - throw new UserCancelledActionExecutionError(); - } - - // Return modified response if action execution failed - if (error.config && error.config.url.match(executeActionRegex)) { - return makeExecuteActionResponse(error.response); - } - - // Return error if any timeout happened in other api calls - if ( - error.code === axiosConnectionAbortedCode && - error.message && - error.message.match(timeoutErrorRegex) - ) { - return Promise.reject({ - ...error, - message: createMessage(SERVER_API_TIMEOUT_ERROR), - code: ERROR_CODES.REQUEST_TIMEOUT, - }); - } - - if (error.response) { - if (error.response.status === API_STATUS_CODES.SERVER_ERROR) { - return Promise.reject({ - ...error, - code: ERROR_CODES.SERVER_ERROR, - message: createMessage(ERROR_500), - }); - } - - // The request was made and the server responded with a status code - // that falls out of the range of 2xx - if (!is404orAuthPath()) { - const currentUrl = `${window.location.href}`; - - if (error.response.status === API_STATUS_CODES.REQUEST_NOT_AUTHORISED) { - // Redirect to login and set a redirect url. - store.dispatch( - logoutUser({ - redirectURL: `${AUTH_LOGIN_URL}?redirectUrl=${encodeURIComponent( - currentUrl, - )}`, - }), - ); - Sentry.captureException(error); - - return Promise.reject({ - ...error, - code: ERROR_CODES.REQUEST_NOT_AUTHORISED, - message: "Unauthorized. Redirecting to login page...", - show: false, - }); - } - - const errorData = error.response.data.responseMeta ?? {}; - - if ( - errorData.status === API_STATUS_CODES.RESOURCE_NOT_FOUND && - (SERVER_ERROR_CODES.RESOURCE_NOT_FOUND.includes(errorData.error.code) || - SERVER_ERROR_CODES.UNABLE_TO_FIND_PAGE.includes(errorData.error.code)) - ) { - Sentry.captureException(error); - - return Promise.reject({ - ...error, - code: ERROR_CODES.PAGE_NOT_FOUND, - message: "Resource Not Found", - show: false, - }); - } - } - - if (error.response.data.responseMeta) { - return Promise.resolve(error.response.data); - } - - Sentry.captureException(new Error("Api responded without response meta"), { - contexts: { response: error.response.data }, - }); - - return Promise.reject(error.response.data); - } else if (error.request) { - // The request was made but no response was received - // `error.request` is an instance of XMLHttpRequest in the browser and an instance of - // http.ClientRequest in node.js - log.error(error.request); - } else { - // Something happened in setting up the request that triggered an Error - log.error("Error", error.message); - } - - log.debug(error.config); - - return Promise.resolve(error); -}; - // function to get the default environment export const getDefaultEnvId = () => { return DEFAULT_ENV_ID; diff --git a/app/client/src/ce/constants/ApiConstants.tsx b/app/client/src/ce/constants/ApiConstants.tsx index 75ad958e78..b1c7a6c7ee 100644 --- a/app/client/src/ce/constants/ApiConstants.tsx +++ b/app/client/src/ce/constants/ApiConstants.tsx @@ -1,3 +1,7 @@ +import type { CreateAxiosDefaults } from "axios"; +import { ID_EXTRACTION_REGEX } from "constants/routes"; +import { UNUSED_ENV_ID } from "constants/EnvironmentContants"; + export const REQUEST_TIMEOUT_MS = 20000; export const DEFAULT_ACTION_TIMEOUT = 10000; export const DEFAULT_EXECUTE_ACTION_TIMEOUT_MS = 15000; @@ -5,6 +9,44 @@ export const DEFAULT_TEST_DATA_SOURCE_TIMEOUT_MS = 30000; export const DEFAULT_APPSMITH_AI_QUERY_TIMEOUT_MS = 60000; export const FILE_UPLOAD_TRIGGER_TIMEOUT_MS = 60000; +export const DEFAULT_AXIOS_CONFIG: CreateAxiosDefaults = { + baseURL: "/api/", + timeout: REQUEST_TIMEOUT_MS, + headers: { + "Content-Type": "application/json", + }, + withCredentials: true, +}; + +export const EXECUTION_ACTION_REGEX = /actions\/execute/; +export const TIMEOUT_ERROR_REGEX = /timeout of (\d+)ms exceeded/; +export const AXIOS_CONNECTION_ABORTED_CODE = "ECONNABORTED"; + +export const DEFAULT_ENV_ID = UNUSED_ENV_ID; + +export const BLOCKED_ROUTES = [ + "v1/app-templates", + "v1/datasources/mocks", + "v1/usage-pulse", + "v1/applications/releaseItems", + "v1/saas", +]; + +export const BLOCKED_ROUTES_REGEX = new RegExp( + `^(${BLOCKED_ROUTES.join("|")})($|/)`, +); + +export const ENV_ENABLED_ROUTES = [ + `v1/datasources/${ID_EXTRACTION_REGEX}/structure`, + `/v1/datasources/${ID_EXTRACTION_REGEX}/trigger`, + "v1/actions/execute", + "v1/saas", +]; + +export const ENV_ENABLED_ROUTES_REGEX = new RegExp( + `^(${ENV_ENABLED_ROUTES.join("|")})($|/)`, +); + export enum API_STATUS_CODES { REQUEST_NOT_AUTHORISED = 401, RESOURCE_NOT_FOUND = 404, diff --git a/app/client/src/ce/sagas/PageSagas.tsx b/app/client/src/ce/sagas/PageSagas.tsx index 2ab7c28753..12e52ba930 100644 --- a/app/client/src/ce/sagas/PageSagas.tsx +++ b/app/client/src/ce/sagas/PageSagas.tsx @@ -149,7 +149,7 @@ import type { LayoutSystemTypes } from "layoutSystems/types"; import { getIsAnvilLayout } from "layoutSystems/anvil/integrations/selectors"; import { convertToBasePageIdSelector } from "selectors/pageListSelectors"; import type { Page } from "entities/Page"; -import ConsolidatedPageLoadApi from "api/ConsolidatedPageLoadApi"; +import { ConsolidatedPageLoadApi } from "api"; export const checkIfMigrationIsNeeded = ( fetchPageResponse?: FetchPageResponse, diff --git a/app/client/src/sagas/AppThemingSaga.tsx b/app/client/src/sagas/AppThemingSaga.tsx index 5a4de787d4..3f57b894e4 100644 --- a/app/client/src/sagas/AppThemingSaga.tsx +++ b/app/client/src/sagas/AppThemingSaga.tsx @@ -11,8 +11,15 @@ import { ReduxActionErrorTypes, ReduxActionTypes, } from "ee/constants/ReduxActionConstants"; -import ThemingApi from "api/AppThemingApi"; -import { all, takeLatest, put, select, call } from "redux-saga/effects"; +import { AppThemingApi } from "api"; +import { + all, + takeLatest, + put, + select, + call, + type SagaReturnType, +} from "redux-saga/effects"; import { toast } from "@appsmith/ads"; import { CHANGE_APP_THEME, @@ -31,8 +38,6 @@ import { getBetaFlag, setBetaFlag, STORAGE_KEYS } from "utils/storage"; import type { UpdateWidgetPropertyPayload } from "actions/controlActions"; import { batchUpdateMultipleWidgetProperties } from "actions/controlActions"; import { getPropertiesToUpdateForReset } from "entities/AppTheming/utils"; -import type { ApiResponse } from "api/ApiResponses"; -import type { AppTheme } from "entities/AppTheming"; import type { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer"; import { getCurrentApplicationId, @@ -75,11 +80,10 @@ export function* fetchAppThemes(action: ReduxAction) { try { const { applicationId, themes } = action.payload; - const response: ApiResponse = yield call( - getFromServerWhenNoPrefetchedResult, - themes, - async () => ThemingApi.fetchThemes(applicationId), - ); + const response: SagaReturnType = + yield call(getFromServerWhenNoPrefetchedResult, themes, async () => + AppThemingApi.fetchThemes(applicationId), + ); yield put({ type: ReduxActionTypes.FETCH_APP_THEMES_SUCCESS, @@ -112,11 +116,10 @@ export function* fetchAppSelectedTheme( const applicationVersion = yield select(selectApplicationVersion); try { - const response: ApiResponse = yield call( - getFromServerWhenNoPrefetchedResult, - currentTheme, - async () => ThemingApi.fetchSelected(applicationId, mode), - ); + const response: SagaReturnType = + yield call(getFromServerWhenNoPrefetchedResult, currentTheme, async () => + AppThemingApi.fetchSelected(applicationId, mode), + ); if (response?.data) { yield put({ @@ -161,7 +164,7 @@ export function* updateSelectedTheme( const canvasWidgets: CanvasWidgetsReduxState = yield select(getCanvasWidgets); try { - yield ThemingApi.updateTheme(applicationId, theme); + yield AppThemingApi.updateTheme(applicationId, theme); yield put({ type: ReduxActionTypes.UPDATE_SELECTED_APP_THEME_SUCCESS, @@ -197,7 +200,7 @@ export function* changeSelectedTheme( const canvasWidgets: CanvasWidgetsReduxState = yield select(getCanvasWidgets); try { - yield ThemingApi.changeTheme(applicationId, theme); + yield AppThemingApi.changeTheme(applicationId, theme); yield put({ type: ReduxActionTypes.CHANGE_SELECTED_APP_THEME_SUCCESS, @@ -235,7 +238,7 @@ export function* deleteTheme(action: ReduxAction) { const { name, themeId } = action.payload; try { - yield ThemingApi.deleteTheme(themeId); + yield AppThemingApi.deleteTheme(themeId); yield put({ type: ReduxActionTypes.DELETE_APP_THEME_SUCCESS, @@ -289,15 +292,15 @@ function* setDefaultSelectedThemeOnError() { try { // Fetch all system themes - const response: ApiResponse = - yield ThemingApi.fetchThemes(applicationId); + const response: SagaReturnType = + yield AppThemingApi.fetchThemes(applicationId); // Gets default theme const theme = find(response.data, { name: "Default" }); if (theme) { // Update API call to set current theme to default - yield ThemingApi.changeTheme(applicationId, theme); + yield AppThemingApi.changeTheme(applicationId, theme); yield put({ type: ReduxActionTypes.FETCH_SELECTED_APP_THEME_SUCCESS, payload: theme, diff --git a/app/client/src/sagas/DatasourcesSagas.ts b/app/client/src/sagas/DatasourcesSagas.ts index bbe7de399c..40cff1e2c2 100644 --- a/app/client/src/sagas/DatasourcesSagas.ts +++ b/app/client/src/sagas/DatasourcesSagas.ts @@ -401,7 +401,9 @@ function* handleDatasourceDeleteRedirect(deletedDatasourceId: string) { // Go to the add datasource if the last item is deleted if (remainingDatasources.length === 0) { - history.push(integrationEditorURL({ selectedTab: INTEGRATION_TABS.NEW })); + yield call(() => + history.push(integrationEditorURL({ selectedTab: INTEGRATION_TABS.NEW })), + ); return; } diff --git a/app/client/src/sagas/ErrorSagas.tsx b/app/client/src/sagas/ErrorSagas.tsx index 3e3e06854c..1669fb5c4c 100644 --- a/app/client/src/sagas/ErrorSagas.tsx +++ b/app/client/src/sagas/ErrorSagas.tsx @@ -29,7 +29,7 @@ import { import store from "store"; import * as Sentry from "@sentry/react"; -import { axiosConnectionAbortedCode } from "ee/api/ApiUtils"; +import { AXIOS_CONNECTION_ABORTED_CODE } from "ee/constants/ApiConstants"; import { getLoginUrl } from "ee/utils/adminSettingsHelpers"; import type { PluginErrorDetails } from "api/ActionAPI"; import showToast from "sagas/ToastSagas"; @@ -104,7 +104,7 @@ export function* validateResponse( } // letting `apiFailureResponseInterceptor` handle it this case - if (response?.code === axiosConnectionAbortedCode) { + if (response?.code === AXIOS_CONNECTION_ABORTED_CODE) { return false; } diff --git a/app/client/src/sagas/InitSagas.ts b/app/client/src/sagas/InitSagas.ts index fc6a0343c3..497222db19 100644 --- a/app/client/src/sagas/InitSagas.ts +++ b/app/client/src/sagas/InitSagas.ts @@ -81,8 +81,8 @@ import type { FetchPageResponse, FetchPageResponseData } from "api/PageApi"; import type { AppTheme } from "entities/AppTheming"; import type { Datasource } from "entities/Datasource"; import type { Plugin, PluginFormPayload } from "api/PluginApi"; -import ConsolidatedPageLoadApi from "api/ConsolidatedPageLoadApi"; -import { axiosConnectionAbortedCode } from "ee/api/ApiUtils"; +import { ConsolidatedPageLoadApi } from "api"; +import { AXIOS_CONNECTION_ABORTED_CODE } from "ee/constants/ApiConstants"; import { endSpan, startNestedSpan, @@ -254,7 +254,7 @@ export function* getInitResponses({ if (!isValidResponse) { // its only invalid when there is a axios related error - throw new Error("Error occured " + axiosConnectionAbortedCode); + throw new Error("Error occured " + AXIOS_CONNECTION_ABORTED_CODE); } // TODO: Fix this the next time the file is edited // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/app/client/src/sagas/helper.ts b/app/client/src/sagas/helper.ts index 38ad773c4c..c720022a3f 100644 --- a/app/client/src/sagas/helper.ts +++ b/app/client/src/sagas/helper.ts @@ -20,7 +20,7 @@ import set from "lodash/set"; import log from "loglevel"; import { isPlainObject, isString } from "lodash"; import { DATA_BIND_REGEX_GLOBAL } from "constants/BindingsConstants"; -import { apiFailureResponseInterceptor } from "ee/api/ApiUtils"; +import { apiFailureResponseInterceptor } from "api/interceptors"; import { klonaLiteWithTelemetry } from "utils/helpers"; // function to extract all objects that have dynamic values @@ -237,7 +237,9 @@ export function* getFromServerWhenNoPrefetchedResult( }, status, }, - }); + // TODO: Fix this the next time the file is edited + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); return resp; }