chore: Refactor API (#36412)

Fixes #36481 

/ok-to-test tags="@tag.All"

<!-- This is an auto-generated comment: Cypress test results  -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/11028542060>
> Commit: ed537d3958a3eba4502cbc32daf60c4cd814002d
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=11028542060&attempt=1"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.All`
> Spec:
> <hr>Wed, 25 Sep 2024 08:56:31 UTC
<!-- end of auto-generated comment: Cypress test results  -->


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## 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.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Pawan Kumar <pawankumar@Pawans-MacBook-Pro-2.local>
This commit is contained in:
Pawan Kumar 2024-09-25 16:29:21 +05:30 committed by GitHub
parent 1210104575
commit 2f2f5a6bf4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
54 changed files with 1079 additions and 482 deletions

View File

@ -0,0 +1,4 @@
export const compose =
<T>(...fns: Array<(arg: T) => T>) =>
(x: T) =>
fns.reduce((acc, fn) => fn(acc), x);

View File

@ -0,0 +1 @@
export { compose } from "./compose";

View File

@ -1 +1,2 @@
export * from "./object";
export * from "./compose";

View File

@ -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,

View File

@ -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<AxiosPromise<ApiResponse<InitConsolidatedApi>>> {
return Api.get(ConsolidatedPageLoadApi.consolidatedApiViewUrl, params);
}
static async getConsolidatedPageLoadDataEdit(params: {
applicationId?: string;
defaultPageId?: string;
}): Promise<AxiosPromise<ApiResponse<InitConsolidatedApi>>> {
return Api.get(ConsolidatedPageLoadApi.consolidatedApiEditUrl, params);
}
}
export default ConsolidatedPageLoadApi;

View File

@ -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<ApiResponse>).response?.status).toBe(413);
expect((error as AxiosError<ApiResponse>).message).toBe(
createMessage(ERROR_413, 100),
);
expect(
(error as AxiosError<ApiResponse> & { 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<ApiResponse>).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<ApiResponse>).message).toBe(
createMessage(SERVER_API_TIMEOUT_ERROR),
);
expect((error as AxiosError<ApiResponse>).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<ApiResponse>).message).toBe(
createMessage(ERROR_500),
);
expect((error as AxiosError<ApiResponse>).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<ApiResponse>).message).toBe(
"Unauthorized. Redirecting to login page...",
);
expect((error as AxiosError<ApiResponse>).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<ApiResponse>).message).toBe(
"Resource Not Found",
);
expect((error as AxiosError<ApiResponse>).code).toBe(
ERROR_CODES.PAGE_NOT_FOUND,
);
}
});
});

View File

@ -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);
});
});

View File

@ -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");
});
});

View File

@ -0,0 +1,31 @@
import type { AxiosResponseData } from "api/types";
import { apiFactory } from "./factory";
const apiInstance = apiFactory();
export async function get<T>(...args: Parameters<typeof apiInstance.get>) {
// 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<T, AxiosResponseData<T>>(...args);
}
export async function post<T>(...args: Parameters<typeof apiInstance.post>) {
return apiInstance.post<T, AxiosResponseData<T>>(...args);
}
export async function put<T>(...args: Parameters<typeof apiInstance.put>) {
return apiInstance.put<T, AxiosResponseData<T>>(...args);
}
// Note: _delete is used instead of delete because delete is a reserved keyword in JavaScript
async function _delete<T>(...args: Parameters<typeof apiInstance.delete>) {
return apiInstance.delete<T, AxiosResponseData<T>>(...args);
}
export { _delete as delete };
export async function patch<T>(...args: Parameters<typeof apiInstance.patch>) {
return apiInstance.patch<T, AxiosResponseData<T>>(...args);
}

View File

@ -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;
}

View File

@ -0,0 +1,2 @@
export * as api from "./api";
export { apiFactory } from "./factory";

View File

@ -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 };
};

View File

@ -0,0 +1,3 @@
export { is404orAuthPath } from "./is404orAuthPath";
export { validateJsonResponseMeta } from "./validateJsonResponseMeta";
export { addExecutionMetaProperties } from "./addExecutionMetaProperties";

View File

@ -0,0 +1,5 @@
export const is404orAuthPath = () => {
const pathName = window.location.pathname;
return /^\/404/.test(pathName) || /^\/user\/\w+/.test(pathName);
};

View File

@ -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 },
});
}
};

View File

@ -0,0 +1 @@
export * from "./services";

View File

@ -0,0 +1,2 @@
export * from "./request";
export * from "./response";

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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<InternalAxiosRequestConfig>(
blockAirgappedRoutes,
addRequestedByHeader,
addGitBranchHeader,
increaseGitApiTimeout,
addEnvironmentHeader,
addAnonymousUserIdHeader,
addPerformanceMonitoringHeaders,
);
return interceptorPipeline(config);
};

View File

@ -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 };

View File

@ -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;
};

View File

@ -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";

View File

@ -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<ApiResponse>,
) => {
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);
};

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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<ApiResponse>,
) => {
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;
};

View File

@ -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<ApiResponse>) {
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;
}

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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";

View File

@ -0,0 +1,2 @@
export { apiFailureResponseInterceptor } from "./apiFailureResponseInterceptor";
export { apiSuccessResponseInterceptor } from "./apiSuccessResponseInterceptor";

View File

@ -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<AppTheme[]>(url);
}
export async function fetchSelected(applicationId: string, mode = "EDIT") {
const url = `${baseURL}/themes/applications/${applicationId}/current`;
return api.get<AppTheme[]>(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<AppTheme[]>(url, payload);
}
export async function changeTheme(applicationId: string, theme: AppTheme) {
const url = `${baseURL}/applications/${applicationId}/themes/${theme.id}`;
return api.patch<AppTheme[]>(url, theme);
}
export async function deleteTheme(themeId: string) {
const url = `${baseURL}/themes/${themeId}`;
return api.delete<AppTheme[]>(url);
}

View File

@ -0,0 +1 @@
export * from "./api";

View File

@ -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<InitConsolidatedApi>(VIEW_URL, { params });
};
export const getConsolidatedPageLoadDataEdit = async (params: {
applicationId?: string;
defaultPageId?: string;
}) => {
return api.get<InitConsolidatedApi>(EDIT_URL, { params });
};

View File

@ -0,0 +1 @@
export * from "./api";

View File

@ -0,0 +1,2 @@
export * as AppThemingApi from "./AppThemingApi";
export * as ConsolidatedPageLoadApi from "./ConsolidatedPageLoadApi";

View File

@ -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<T = unknown> {
responseMeta: ApiResponseMeta;
data: T;
code?: string;
}
export type AxiosResponseData<T> = AxiosResponse<ApiResponse<T>>["data"];
export type ErrorHandler = (
error: AxiosError<ApiResponse>,
) => Promise<unknown | null>;

View File

@ -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();
});
});
});

View File

@ -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;

View File

@ -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,

View File

@ -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,

View File

@ -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<FetchAppThemesAction>) {
try {
const { applicationId, themes } = action.payload;
const response: ApiResponse<AppTheme> = yield call(
getFromServerWhenNoPrefetchedResult,
themes,
async () => ThemingApi.fetchThemes(applicationId),
);
const response: SagaReturnType<typeof AppThemingApi.fetchThemes> =
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<AppTheme[]> = yield call(
getFromServerWhenNoPrefetchedResult,
currentTheme,
async () => ThemingApi.fetchSelected(applicationId, mode),
);
const response: SagaReturnType<typeof AppThemingApi.fetchSelected> =
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<DeleteAppThemeAction>) {
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<AppTheme[]> =
yield ThemingApi.fetchThemes(applicationId);
const response: SagaReturnType<typeof AppThemingApi.fetchThemes> =
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,

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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

View File

@ -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;
}