From ed688c12d7235ad9baa16361c14960d73bd6efef Mon Sep 17 00:00:00 2001 From: mufazalov Date: Thu, 6 Jun 2024 11:14:46 +0300 Subject: [PATCH] fix(Query): parse 200 error response --- .../Errors/ResponseError/ResponseError.tsx | 4 + .../QueryExecutionStatus.tsx | 7 +- .../TenantOverview/TenantCpu/TopQueries.tsx | 3 +- .../TenantOverview/TenantCpu/TopShards.tsx | 3 +- .../TenantStorage/TopTables.tsx | 3 +- .../Diagnostics/TopQueries/TopQueries.tsx | 6 +- .../Diagnostics/TopShards/TopShards.tsx | 6 +- .../Query/ExecuteResult/ExecuteResult.tsx | 16 +- .../Query/ExplainResult/ExplainResult.js | 19 +-- .../Tenant/Query/Preview/Preview.tsx | 6 +- .../Tenant/Query/QueryEditor/QueryEditor.tsx | 95 ++++++----- src/services/api.ts | 5 +- src/store/reducers/cluster/cluster.ts | 5 + src/store/reducers/executeQuery.ts | 104 ++++++------ .../executeTopQueries/executeTopQueries.ts | 7 +- src/store/reducers/explainQuery.ts | 158 ------------------ .../reducers/explainQuery/explainQuery.ts | 48 ++++++ src/store/reducers/explainQuery/types.ts | 14 ++ src/store/reducers/explainQuery/utils.ts | 56 +++++++ src/store/reducers/index.ts | 2 - src/store/reducers/olapStats.ts | 7 +- src/store/reducers/preview.ts | 7 +- .../reducers/shardsWorkload/shardsWorkload.ts | 7 +- .../executeTopTables/executeTopTables.ts | 11 +- .../topQueries/tenantOverviewTopQueries.ts | 11 +- .../topShards/tenantOverviewTopShards.ts | 11 +- src/store/utils.ts | 11 +- src/types/store/executeQuery.ts | 10 -- src/types/store/explainQuery.ts | 40 ----- src/utils/error.ts | 24 --- src/utils/query.ts | 41 ++++- src/utils/response.ts | 16 ++ 32 files changed, 365 insertions(+), 398 deletions(-) delete mode 100644 src/store/reducers/explainQuery.ts create mode 100644 src/store/reducers/explainQuery/explainQuery.ts create mode 100644 src/store/reducers/explainQuery/types.ts create mode 100644 src/store/reducers/explainQuery/utils.ts delete mode 100644 src/types/store/explainQuery.ts delete mode 100644 src/utils/error.ts create mode 100644 src/utils/response.ts diff --git a/src/components/Errors/ResponseError/ResponseError.tsx b/src/components/Errors/ResponseError/ResponseError.tsx index ce9be261f9..56994d8fd8 100644 --- a/src/components/Errors/ResponseError/ResponseError.tsx +++ b/src/components/Errors/ResponseError/ResponseError.tsx @@ -12,6 +12,10 @@ export const ResponseError = ({ defaultMessage = i18n('responseError.defaultMessage'), }: ResponseErrorProps) => { let statusText = ''; + + if (error && typeof error === 'string') { + statusText = error; + } if (error && typeof error === 'object') { if ('statusText' in error && typeof error.statusText === 'string') { statusText = error.statusText; diff --git a/src/components/QueryExecutionStatus/QueryExecutionStatus.tsx b/src/components/QueryExecutionStatus/QueryExecutionStatus.tsx index 3507a90a83..3595611594 100644 --- a/src/components/QueryExecutionStatus/QueryExecutionStatus.tsx +++ b/src/components/QueryExecutionStatus/QueryExecutionStatus.tsx @@ -1,6 +1,6 @@ import {CircleCheck, CircleQuestionFill, CircleXmark} from '@gravity-ui/icons'; import {Icon} from '@gravity-ui/uikit'; -import type {AxiosError} from 'axios'; +import {isAxiosError} from 'axios'; import {cn} from '../../utils/cn'; @@ -10,15 +10,14 @@ const b = cn('kv-query-execution-status'); interface QueryExecutionStatusProps { className?: string; - // TODO: Remove Record when ECONNABORTED error case is fully typed - error?: AxiosError | Record | string; + error?: unknown; } export const QueryExecutionStatus = ({className, error}: QueryExecutionStatusProps) => { let icon: React.ReactNode; let label: string; - if (typeof error === 'object' && error?.code === 'ECONNABORTED') { + if (isAxiosError(error) && error.code === 'ECONNABORTED') { icon = ; label = 'Connection aborted'; } else { diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopQueries.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopQueries.tsx index a4caa6e3c2..5f71a9f39c 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopQueries.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopQueries.tsx @@ -12,6 +12,7 @@ import { } from '../../../../../store/reducers/tenant/constants'; import {topQueriesApi} from '../../../../../store/reducers/tenantOverview/topQueries/tenantOverviewTopQueries'; import {useTypedDispatch, useTypedSelector} from '../../../../../utils/hooks'; +import {parseQueryErrorToString} from '../../../../../utils/query'; import {TenantTabsGroups, getTenantPath} from '../../../TenantPages'; import { TOP_QUERIES_COLUMNS_WIDTH_LS_KEY, @@ -80,7 +81,7 @@ export function TopQueries({path}: TopQueriesProps) { onRowClick={handleRowClick} title={title} loading={loading} - error={error} + error={parseQueryErrorToString(error)} rowClassName={() => b('top-queries-row')} /> ); diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopShards.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopShards.tsx index 544f2fdb25..6dd2424939 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopShards.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopShards.tsx @@ -4,6 +4,7 @@ import {parseQuery} from '../../../../../routes'; import {TENANT_DIAGNOSTICS_TABS_IDS} from '../../../../../store/reducers/tenant/constants'; import {topShardsApi} from '../../../../../store/reducers/tenantOverview/topShards/tenantOverviewTopShards'; import {useTypedSelector} from '../../../../../utils/hooks'; +import {parseQueryErrorToString} from '../../../../../utils/query'; import {TenantTabsGroups, getTenantPath} from '../../../TenantPages'; import { TOP_SHARDS_COLUMNS_WIDTH_LS_KEY, @@ -50,7 +51,7 @@ export const TopShards = ({path}: TopShardsProps) => { columns={columns} title={title} loading={loading} - error={error} + error={parseQueryErrorToString(error)} /> ); }; diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TopTables.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TopTables.tsx index 44012e3184..06e8f935e2 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TopTables.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TopTables.tsx @@ -8,6 +8,7 @@ import {topTablesApi} from '../../../../../store/reducers/tenantOverview/execute import type {KeyValueRow} from '../../../../../types/api/query'; import {formatBytes, getSizeWithSignificantDigits} from '../../../../../utils/bytesParsers'; import {useTypedSelector} from '../../../../../utils/hooks'; +import {parseQueryErrorToString} from '../../../../../utils/query'; import {TenantOverviewTableLayout} from '../TenantOverviewTableLayout'; import {getSectionTitle} from '../getSectionTitle'; import i18n from '../i18n'; @@ -73,7 +74,7 @@ export function TopTables({path}: TopTablesProps) { columns={columns} title={title} loading={loading} - error={error} + error={parseQueryErrorToString(error)} /> ); } diff --git a/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx b/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx index 18593303c2..1df62ea08f 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx @@ -22,7 +22,7 @@ import type {EPathType} from '../../../../types/api/schema'; import {cn} from '../../../../utils/cn'; import {isSortableTopQueriesProperty} from '../../../../utils/diagnostics'; import {useTypedDispatch, useTypedSelector} from '../../../../utils/hooks'; -import {prepareQueryError} from '../../../../utils/query'; +import {parseQueryErrorToString} from '../../../../utils/query'; import {TenantTabsGroups, getTenantPath} from '../../TenantPages'; import {QUERY_TABLE_SETTINGS} from '../../utils/constants'; import {isColumnEntityType} from '../../utils/schema'; @@ -91,8 +91,8 @@ export const TopQueries = ({path, type}: TopQueriesProps) => { }; const renderContent = () => { - if (error && typeof error === 'object' && !(error as any).isCancelled) { - return
{prepareQueryError(error)}
; + if (error) { + return
{parseQueryErrorToString(error)}
; } if (!data || isColumnEntityType(type)) { diff --git a/src/containers/Tenant/Diagnostics/TopShards/TopShards.tsx b/src/containers/Tenant/Diagnostics/TopShards/TopShards.tsx index 60a21c7b81..e78b846f21 100644 --- a/src/containers/Tenant/Diagnostics/TopShards/TopShards.tsx +++ b/src/containers/Tenant/Diagnostics/TopShards/TopShards.tsx @@ -19,7 +19,7 @@ import {DEFAULT_TABLE_SETTINGS, HOUR_IN_SECONDS} from '../../../../utils/constan import {formatDateTime} from '../../../../utils/dataFormatters/dataFormatters'; import {isSortableTopShardsProperty} from '../../../../utils/diagnostics'; import {useTypedDispatch, useTypedSelector} from '../../../../utils/hooks'; -import {prepareQueryError} from '../../../../utils/query'; +import {parseQueryErrorToString} from '../../../../utils/query'; import {isColumnEntityType} from '../../utils/schema'; import {Filters} from './Filters'; @@ -192,8 +192,8 @@ export const TopShards = ({tenantPath, type}: TopShardsProps) => { }; const renderContent = () => { - if (error && typeof error === 'object' && !(error as any).isCancelled) { - return
{prepareQueryError(error)}
; + if (error) { + return
{parseQueryErrorToString(error)}
; } if (!data || isColumnEntityType(type)) { diff --git a/src/containers/Tenant/Query/ExecuteResult/ExecuteResult.tsx b/src/containers/Tenant/Query/ExecuteResult/ExecuteResult.tsx index a521f936d4..aafb6cc8fa 100644 --- a/src/containers/Tenant/Query/ExecuteResult/ExecuteResult.tsx +++ b/src/containers/Tenant/Query/ExecuteResult/ExecuteResult.tsx @@ -12,11 +12,11 @@ import {QueryResultTable} from '../../../../components/QueryResultTable/QueryRes import {disableFullscreen} from '../../../../store/reducers/fullscreen'; import type {ColumnType, KeyValueRow} from '../../../../types/api/query'; import type {ValueOf} from '../../../../types/common'; -import type {IQueryResult, QueryErrorResponse} from '../../../../types/store/query'; +import type {IQueryResult} from '../../../../types/store/query'; import {getArray} from '../../../../utils'; import {cn} from '../../../../utils/cn'; import {useTypedDispatch, useTypedSelector} from '../../../../utils/hooks'; -import {prepareQueryError} from '../../../../utils/query'; +import {parseQueryError} from '../../../../utils/query'; import {PaneVisibilityToggleButtons} from '../../utils/paneVisibilityToggleHelpers'; import {ResultIssues} from '../Issues/Issues'; import {QueryDuration} from '../QueryDuration/QueryDuration'; @@ -41,7 +41,7 @@ const resultOptions = [ interface ExecuteResultProps { data: IQueryResult | undefined; stats: IQueryResult['stats'] | undefined; - error: string | QueryErrorResponse | undefined; + error: unknown; isResultsCollapsed?: boolean; onCollapseResults: VoidFunction; onExpandResults: VoidFunction; @@ -68,6 +68,8 @@ export function ExecuteResult({ const textResults = getPreparedResult(currentResult); const copyDisabled = !textResults.length; + const parsedError = parseQueryError(error); + React.useEffect(() => { return () => { dispatch(disableFullscreen()); @@ -159,12 +161,12 @@ export function ExecuteResult({ }; const renderIssues = () => { - if (!error) { + if (!parsedError) { return null; } - if (typeof error === 'object' && error.data?.issues && Array.isArray(error.data.issues)) { - const content = ; + if (typeof parsedError === 'object') { + const content = ; return ( @@ -180,8 +182,6 @@ export function ExecuteResult({ ); } - const parsedError = typeof error === 'string' ? error : prepareQueryError(error); - return
{parsedError}
; }; diff --git a/src/containers/Tenant/Query/ExplainResult/ExplainResult.js b/src/containers/Tenant/Query/ExplainResult/ExplainResult.js index 2282b385f1..8ddad6f04c 100644 --- a/src/containers/Tenant/Query/ExplainResult/ExplainResult.js +++ b/src/containers/Tenant/Query/ExplainResult/ExplainResult.js @@ -9,11 +9,12 @@ import EnableFullscreenButton from '../../../../components/EnableFullscreenButto import Fullscreen from '../../../../components/Fullscreen/Fullscreen'; import {MonacoEditor} from '../../../../components/MonacoEditor/MonacoEditor'; import {QueryExecutionStatus} from '../../../../components/QueryExecutionStatus'; -import {explainVersions} from '../../../../store/reducers/explainQuery'; +import {explainVersions} from '../../../../store/reducers/explainQuery/utils'; import {disableFullscreen} from '../../../../store/reducers/fullscreen'; import {cn} from '../../../../utils/cn'; import {useTypedDispatch, useTypedSelector} from '../../../../utils/hooks'; import {LANGUAGE_S_EXPRESSION_ID} from '../../../../utils/monaco/s-expression/constants'; +import {parseQueryErrorToString} from '../../../../utils/query'; import {PaneVisibilityToggleButtons} from '../../utils/paneVisibilityToggleHelpers'; import {renderExplainNode} from './utils'; @@ -193,22 +194,12 @@ export function ExplainResult(props) { }; const renderError = () => { - const {error} = props; - - let message; - - if (error.data) { - message = typeof error.data === 'string' ? error.data : error.data.error?.message; - } else { - message = error; - } - - return
{message}
; + return
{parseQueryErrorToString(props.error)}
; }; const renderContent = () => { - const {error, loading, loadingAst} = props; - if (loading || loadingAst) { + const {error, loading} = props; + if (loading) { return renderLoader(); } diff --git a/src/containers/Tenant/Query/Preview/Preview.tsx b/src/containers/Tenant/Query/Preview/Preview.tsx index e66a29ef8a..0b0f518595 100644 --- a/src/containers/Tenant/Query/Preview/Preview.tsx +++ b/src/containers/Tenant/Query/Preview/Preview.tsx @@ -9,7 +9,7 @@ import {setShowPreview} from '../../../../store/reducers/schema/schema'; import type {EPathType} from '../../../../types/api/schema'; import {cn} from '../../../../utils/cn'; import {useTypedDispatch, useTypedSelector} from '../../../../utils/hooks'; -import {prepareQueryError} from '../../../../utils/query'; +import {parseQueryErrorToString} from '../../../../utils/query'; import {isExternalTableType, isTableType} from '../../utils/schema'; import i18n from '../i18n'; @@ -76,7 +76,9 @@ export const Preview = ({database, type}: PreviewProps) => { if (!isPreviewAvailable) { message =
{i18n('preview.not-available')}
; } else if (error) { - message =
{prepareQueryError(error)}
; + message = ( +
{parseQueryErrorToString(error)}
+ ); } const content = message ?? ( diff --git a/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx b/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx index ec2ff76da3..f67087435f 100644 --- a/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx +++ b/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx @@ -8,19 +8,19 @@ import {MonacoEditor} from '../../../../components/MonacoEditor/MonacoEditor'; import SplitPane from '../../../../components/SplitPane'; import type {RootState} from '../../../../store'; import { + executeQueryApi, goToNextQuery, goToPreviousQuery, saveQueryToHistory, - sendExecuteQuery, setTenantPath, } from '../../../../store/reducers/executeQuery'; -import {getExplainQuery, getExplainQueryAst} from '../../../../store/reducers/explainQuery'; +import {explainQueryApi} from '../../../../store/reducers/explainQuery/explainQuery'; +import type {PreparedExplainResponse} from '../../../../store/reducers/explainQuery/types'; import {setShowPreview} from '../../../../store/reducers/schema/schema'; import type {EPathType} from '../../../../types/api/schema'; import type {ValueOf} from '../../../../types/common'; import type {ExecuteQueryState} from '../../../../types/store/executeQuery'; -import type {ExplainQueryState} from '../../../../types/store/explainQuery'; -import type {QueryAction, QueryMode, SavedQuery} from '../../../../types/store/query'; +import type {IQueryResult, QueryAction, QueryMode, SavedQuery} from '../../../../types/store/query'; import {cn} from '../../../../utils/cn'; import { DEFAULT_IS_QUERY_RESULT_COLLAPSED, @@ -68,15 +68,11 @@ const initialTenantCommonInfoState = { interface QueryEditorProps { path: string; - sendExecuteQuery: (...args: Parameters) => void; - getExplainQuery: (...args: Parameters) => void; - getExplainQueryAst: (...args: Parameters) => void; changeUserInput: (arg: {input: string}) => void; goToNextQuery: (...args: Parameters) => void; goToPreviousQuery: (...args: Parameters) => void; setTenantPath: (...args: Parameters) => void; executeQuery: ExecuteQueryState; - explainQuery: ExplainQueryState; theme: string; type?: EPathType; showPreview: boolean; @@ -90,7 +86,6 @@ function QueryEditor(props: QueryEditorProps) { path, setTenantPath: setPath, executeQuery, - explainQuery, type, theme, changeUserInput, @@ -111,6 +106,9 @@ function QueryEditor(props: QueryEditorProps) { typeof MONACO_HOT_KEY_ACTIONS > | null>(null); + const [sendExecuteQuery, executeQueryResult] = executeQueryApi.useExecuteQueryMutation(); + const [sendExplainQuery, explainQueryResult] = explainQueryApi.useExplainQueryMutation(); + React.useEffect(() => { if (savedPath !== path) { if (savedPath) { @@ -205,7 +203,7 @@ function QueryEditor(props: QueryEditorProps) { setLastUsedQueryAction(QUERY_ACTIONS.execute); setResultType(RESULT_TYPES.EXECUTE); - props.sendExecuteQuery({ + sendExecuteQuery({ query, database: path, mode, @@ -229,7 +227,7 @@ function QueryEditor(props: QueryEditorProps) { setLastUsedQueryAction(QUERY_ACTIONS.explain); setResultType(RESULT_TYPES.EXPLAIN); - props.getExplainQuery({ + sendExplainQuery({ query: input, database: path, mode: mode, @@ -345,10 +343,6 @@ function QueryEditor(props: QueryEditorProps) { props.changeUserInput({input: newValue}); }; - const handleAstQuery = () => { - props.getExplainQueryAst({query: executeQuery.input, database: path}); - }; - const onCollapseResultHandler = () => { dispatchResultVisibilityState(PaneVisibilityActionTypes.triggerCollapse); }; @@ -381,9 +375,9 @@ function QueryEditor(props: QueryEditorProps) { return (
{ return { executeQuery: state.executeQuery, - explainQuery: state.explainQuery, showPreview: state.schema.showPreview, }; }; const mapDispatchToProps = { - sendExecuteQuery, saveQueryToHistory, goToPreviousQuery, goToNextQuery, - getExplainQuery, - getExplainQueryAst, setShowPreview, setTenantPath, }; @@ -466,26 +458,30 @@ const mapDispatchToProps = { export default connect(mapStateToProps, mapDispatchToProps)(QueryEditor); interface ResultProps { - executeQuery: ExecuteQueryState; - explainQuery: ExplainQueryState; + executeQueryData?: IQueryResult; + executeQueryError?: unknown; + explainQueryData?: PreparedExplainResponse; + explainQueryError?: unknown; + explainQueryLoading?: boolean; resultVisibilityState: InitialPaneState; onExpandResultHandler: () => void; onCollapseResultHandler: () => void; type?: EPathType; - handleAstQuery: () => void; theme: string; resultType: ValueOf | undefined; path: string; showPreview?: boolean; } function Result({ - executeQuery, - explainQuery, + executeQueryData, + executeQueryError, + explainQueryData, + explainQueryError, + explainQueryLoading, resultVisibilityState, onExpandResultHandler, onCollapseResultHandler, type, - handleAstQuery, theme, resultType, path, @@ -496,30 +492,33 @@ function Result({ } if (resultType === RESULT_TYPES.EXECUTE) { - const {data, error, stats} = executeQuery; - return data || error ? ( - - ) : null; + if (executeQueryData || executeQueryError) { + const {stats, ...data} = executeQueryData || {}; + + return ( + + ); + } + + return null; } if (resultType === RESULT_TYPES.EXPLAIN) { - const {data, dataAst, error, loading, loadingAst} = explainQuery; + const {plan, ast} = explainQueryData || {}; return ( >( + return this.post | ErrorResponse>( this.getPath( `/viewer/json/query?timeout=${backendTimeout}&base64=${base64}${ schema ? `&schema=${schema}` : '' @@ -423,7 +424,7 @@ export class YdbEmbeddedAPI extends AxiosWrapper { action: Action, syntax?: QuerySyntax, ) { - return this.post>( + return this.post | ErrorResponse>( this.getPath('/viewer/json/query'), { query, diff --git a/src/store/reducers/cluster/cluster.ts b/src/store/reducers/cluster/cluster.ts index 0ab88dd5d3..0f1b84c885 100644 --- a/src/store/reducers/cluster/cluster.ts +++ b/src/store/reducers/cluster/cluster.ts @@ -5,6 +5,7 @@ import type {ClusterTab} from '../../../containers/Cluster/utils'; import {clusterTabsIds, isClusterTab} from '../../../containers/Cluster/utils'; import type {TClusterInfo} from '../../../types/api/cluster'; import {DEFAULT_CLUSTER_TAB_KEY} from '../../../utils/constants'; +import {isQueryErrorResponse} from '../../../utils/query'; import {api} from '../api'; import type {ClusterGroupsStats, ClusterState} from './types'; @@ -75,6 +76,10 @@ export const clusterApi = api.injectEndpoints({ action: 'execute-scan', }); + if (isQueryErrorResponse(groupsStatsResponse)) { + return {data: {clusterData}}; + } + return { data: { clusterData, diff --git a/src/store/reducers/executeQuery.ts b/src/store/reducers/executeQuery.ts index e42960d877..bb19f32ed6 100644 --- a/src/store/reducers/executeQuery.ts +++ b/src/store/reducers/executeQuery.ts @@ -8,11 +8,22 @@ import type { ExecuteQueryStateSlice, QueryInHistory, } from '../../types/store/executeQuery'; -import type {QueryMode, QueryRequestParams, QuerySyntax} from '../../types/store/query'; +import type { + IQueryResult, + QueryMode, + QueryRequestParams, + QuerySyntax, +} from '../../types/store/query'; import {QUERIES_HISTORY_KEY} from '../../utils/constants'; -import {parseQueryError} from '../../utils/error'; -import {QUERY_MODES, QUERY_SYNTAX, parseQueryAPIExecuteResponse} from '../../utils/query'; -import {createApiRequest, createRequestActionTypes} from '../utils'; +import { + QUERY_MODES, + QUERY_SYNTAX, + isQueryErrorResponse, + parseQueryAPIExecuteResponse, +} from '../../utils/query'; +import {createRequestActionTypes} from '../utils'; + +import {api} from './api'; const MAXIMUM_QUERIES_IN_HISTORY = 20; @@ -50,31 +61,6 @@ const executeQuery: Reducer = ( action, ) => { switch (action.type) { - case SEND_QUERY.REQUEST: { - return { - ...state, - loading: true, - data: undefined, - error: undefined, - }; - } - case SEND_QUERY.SUCCESS: { - return { - ...state, - data: action.data, - stats: action.data.stats, - loading: false, - error: undefined, - }; - } - case SEND_QUERY.FAILURE: { - return { - ...state, - error: parseQueryError(action.error), - loading: false, - }; - } - case CHANGE_USER_INPUT: { return { ...state, @@ -157,30 +143,44 @@ interface SendQueryParams extends QueryRequestParams { schema?: Schemas; } -export const sendExecuteQuery = ({query, database, mode, schema = 'modern'}: SendQueryParams) => { - let action: ExecuteActions = 'execute'; - let syntax: QuerySyntax = QUERY_SYNTAX.yql; - - if (mode === 'pg') { - action = 'execute-query'; - syntax = QUERY_SYNTAX.pg; - } else if (mode) { - action = `execute-${mode}`; - } - - return createApiRequest({ - request: window.api.sendQuery({ - schema, - query, - database, - action, - syntax, - stats: 'full', +export const executeQueryApi = api.injectEndpoints({ + endpoints: (build) => ({ + executeQuery: build.mutation({ + queryFn: async ({query, database, mode, schema = 'modern'}) => { + let action: ExecuteActions = 'execute'; + let syntax: QuerySyntax = QUERY_SYNTAX.yql; + + if (mode === 'pg') { + action = 'execute-query'; + syntax = QUERY_SYNTAX.pg; + } else if (mode) { + action = `execute-${mode}`; + } + + try { + const response = await window.api.sendQuery({ + schema, + query, + database, + action, + syntax, + stats: 'full', + }); + + if (isQueryErrorResponse(response)) { + return {error: response}; + } + + const data = parseQueryAPIExecuteResponse(response); + return {data}; + } catch (error) { + return {error}; + } + }, }), - actions: SEND_QUERY, - dataHandler: parseQueryAPIExecuteResponse, - }); -}; + }), + overrideExisting: 'throw', +}); export const saveQueryToHistory = (queryText: string, mode: QueryMode) => { return { diff --git a/src/store/reducers/executeTopQueries/executeTopQueries.ts b/src/store/reducers/executeTopQueries/executeTopQueries.ts index 4351d33fbf..80569a8012 100644 --- a/src/store/reducers/executeTopQueries/executeTopQueries.ts +++ b/src/store/reducers/executeTopQueries/executeTopQueries.ts @@ -2,7 +2,7 @@ import {createSlice} from '@reduxjs/toolkit'; import type {PayloadAction} from '@reduxjs/toolkit'; import {HOUR_IN_SECONDS} from '../../../utils/constants'; -import {parseQueryAPIExecuteResponse} from '../../../utils/query'; +import {isQueryErrorResponse, parseQueryAPIExecuteResponse} from '../../../utils/query'; import {api} from '../api'; import type {TopQueriesFilters} from './types'; @@ -57,6 +57,11 @@ export const topQueriesApi = api.injectEndpoints({ }, {signal}, ); + + if (isQueryErrorResponse(response)) { + return {error: response}; + } + const data = parseQueryAPIExecuteResponse(response); // FIXME: do we really need this? if (!filters?.from && !filters?.to) { diff --git a/src/store/reducers/explainQuery.ts b/src/store/reducers/explainQuery.ts deleted file mode 100644 index 5b761ae588..0000000000 --- a/src/store/reducers/explainQuery.ts +++ /dev/null @@ -1,158 +0,0 @@ -import type {ExplainPlanNodeData, GraphNode, Link} from '@gravity-ui/paranoid'; -import type {Reducer} from '@reduxjs/toolkit'; - -import type {ExplainActions} from '../../types/api/query'; -import type { - ExplainQueryAction, - ExplainQueryState, - PreparedExplainResponse, -} from '../../types/store/explainQuery'; -import type {QueryMode, QueryRequestParams, QuerySyntax} from '../../types/store/query'; -import {parseQueryError} from '../../utils/error'; -import {preparePlan} from '../../utils/prepareQueryExplain'; -import {QUERY_SYNTAX, parseQueryAPIExplainResponse, parseQueryExplainPlan} from '../../utils/query'; -import {createApiRequest, createRequestActionTypes} from '../utils'; - -export const GET_EXPLAIN_QUERY = createRequestActionTypes('query', 'GET_EXPLAIN_QUERY'); -export const GET_EXPLAIN_QUERY_AST = createRequestActionTypes('query', 'GET_EXPLAIN_QUERY_AST'); - -const initialState = { - loading: false, -}; - -const explainQuery: Reducer = ( - state = initialState, - action, -) => { - switch (action.type) { - case GET_EXPLAIN_QUERY.REQUEST: { - return { - ...state, - loading: true, - data: undefined, - error: undefined, - dataAst: undefined, - errorAst: undefined, - }; - } - case GET_EXPLAIN_QUERY.SUCCESS: { - return { - ...state, - data: action.data.plan, - dataAst: action.data.ast, - loading: false, - error: undefined, - }; - } - case GET_EXPLAIN_QUERY.FAILURE: { - return { - ...state, - error: parseQueryError(action.error), - loading: false, - }; - } - case GET_EXPLAIN_QUERY_AST.REQUEST: { - return { - ...state, - loadingAst: true, - dataAst: undefined, - errorAst: undefined, - }; - } - case GET_EXPLAIN_QUERY_AST.SUCCESS: { - return { - ...state, - dataAst: action.data.ast, - loadingAst: false, - error: undefined, - }; - } - case GET_EXPLAIN_QUERY_AST.FAILURE: { - return { - ...state, - errorAst: parseQueryError(action.error), - loadingAst: false, - }; - } - - default: - return state; - } -}; - -export const getExplainQueryAst = ({query, database}: QueryRequestParams) => { - return createApiRequest({ - request: window.api.getExplainQueryAst(query, database), - actions: GET_EXPLAIN_QUERY_AST, - dataHandler: parseQueryAPIExplainResponse, - }); -}; - -export const explainVersions = { - v2: '0.2', -}; - -const supportedExplainQueryVersions = Object.values(explainVersions); - -interface ExplainQueryParams extends QueryRequestParams { - mode?: QueryMode; -} - -export const getExplainQuery = ({query, database, mode}: ExplainQueryParams) => { - let action: ExplainActions = 'explain'; - let syntax: QuerySyntax = QUERY_SYNTAX.yql; - - if (mode === 'pg') { - action = 'explain-query'; - syntax = QUERY_SYNTAX.pg; - } else if (mode) { - action = `explain-${mode}`; - } - - return createApiRequest({ - request: window.api.getExplainQuery(query, database, action, syntax), - actions: GET_EXPLAIN_QUERY, - dataHandler: (response): PreparedExplainResponse => { - const {plan: rawPlan, ast} = parseQueryAPIExplainResponse(response); - - if (!rawPlan) { - return {ast}; - } - - const {tables, meta, Plan} = parseQueryExplainPlan(rawPlan); - - if (supportedExplainQueryVersions.indexOf(meta.version) === -1) { - // Do not prepare plan for not supported versions - return { - plan: { - pristine: rawPlan, - version: meta.version, - }, - ast, - }; - } - - let links: Link[] = []; - let nodes: GraphNode[] = []; - - if (Plan) { - const preparedPlan = preparePlan(Plan); - links = preparedPlan.links; - nodes = preparedPlan.nodes; - } - - return { - plan: { - links, - nodes, - tables, - version: meta.version, - pristine: rawPlan, - }, - ast, - }; - }, - }); -}; - -export default explainQuery; diff --git a/src/store/reducers/explainQuery/explainQuery.ts b/src/store/reducers/explainQuery/explainQuery.ts new file mode 100644 index 0000000000..0c3dce80ce --- /dev/null +++ b/src/store/reducers/explainQuery/explainQuery.ts @@ -0,0 +1,48 @@ +import type {ExplainActions} from '../../../types/api/query'; +import type {QueryMode, QueryRequestParams, QuerySyntax} from '../../../types/store/query'; +import {QUERY_SYNTAX, isQueryErrorResponse} from '../../../utils/query'; +import {api} from '../api'; + +import type {PreparedExplainResponse} from './types'; +import {prepareExplainResponse} from './utils'; + +interface ExplainQueryParams extends QueryRequestParams { + mode?: QueryMode; +} + +export const explainQueryApi = api.injectEndpoints({ + endpoints: (build) => ({ + explainQuery: build.mutation({ + queryFn: async ({query, database, mode}) => { + let action: ExplainActions = 'explain'; + let syntax: QuerySyntax = QUERY_SYNTAX.yql; + + if (mode === 'pg') { + action = 'explain-query'; + syntax = QUERY_SYNTAX.pg; + } else if (mode) { + action = `explain-${mode}`; + } + + try { + const response = await window.api.getExplainQuery( + query, + database, + action, + syntax, + ); + + if (isQueryErrorResponse(response)) { + return {error: response}; + } + + const data = prepareExplainResponse(response); + return {data}; + } catch (error) { + return {error}; + } + }, + }), + }), + overrideExisting: 'throw', +}); diff --git a/src/store/reducers/explainQuery/types.ts b/src/store/reducers/explainQuery/types.ts new file mode 100644 index 0000000000..a17a7fcbc0 --- /dev/null +++ b/src/store/reducers/explainQuery/types.ts @@ -0,0 +1,14 @@ +import type {ExplainPlanNodeData, GraphNode, Link} from '@gravity-ui/paranoid'; + +import type {PlanTable, QueryPlan, ScriptPlan} from '../../../types/api/query'; + +export interface PreparedExplainResponse { + plan?: { + links?: Link[]; + nodes?: GraphNode[]; + tables?: PlanTable[]; + version?: string; + pristine?: QueryPlan | ScriptPlan; + }; + ast?: string; +} diff --git a/src/store/reducers/explainQuery/utils.ts b/src/store/reducers/explainQuery/utils.ts new file mode 100644 index 0000000000..5ea5d8a760 --- /dev/null +++ b/src/store/reducers/explainQuery/utils.ts @@ -0,0 +1,56 @@ +import type {ExplainPlanNodeData, GraphNode, Link} from '@gravity-ui/paranoid'; + +import type {ExplainQueryResponse, ExplainScriptResponse} from '../../../types/api/query'; +import {preparePlan} from '../../../utils/prepareQueryExplain'; +import {parseQueryAPIExplainResponse, parseQueryExplainPlan} from '../../../utils/query'; + +import type {PreparedExplainResponse} from './types'; + +export const explainVersions = { + v2: '0.2', +}; + +const supportedExplainQueryVersions = Object.values(explainVersions); + +export const prepareExplainResponse = ( + response: ExplainScriptResponse | ExplainQueryResponse, +): PreparedExplainResponse => { + const {plan: rawPlan, ast} = parseQueryAPIExplainResponse(response); + + if (!rawPlan) { + return {ast}; + } + + const {tables, meta, Plan} = parseQueryExplainPlan(rawPlan); + + if (supportedExplainQueryVersions.indexOf(meta.version) === -1) { + // Do not prepare plan for not supported versions + return { + plan: { + pristine: rawPlan, + version: meta.version, + }, + ast, + }; + } + + let links: Link[] = []; + let nodes: GraphNode[] = []; + + if (Plan) { + const preparedPlan = preparePlan(Plan); + links = preparedPlan.links; + nodes = preparedPlan.nodes; + } + + return { + plan: { + links, + nodes, + tables, + version: meta.version, + pristine: rawPlan, + }, + ast, + }; +}; diff --git a/src/store/reducers/index.ts b/src/store/reducers/index.ts index 0917e39b54..86cd2aaf20 100644 --- a/src/store/reducers/index.ts +++ b/src/store/reducers/index.ts @@ -6,7 +6,6 @@ import cluster from './cluster/cluster'; import clusters from './clusters/clusters'; import executeQuery from './executeQuery'; import executeTopQueries from './executeTopQueries/executeTopQueries'; -import explainQuery from './explainQuery'; import fullscreen from './fullscreen'; import header from './header/header'; import heatmap from './heatmap'; @@ -37,7 +36,6 @@ export const rootReducer = { tenants, partitions, executeQuery, - explainQuery, tabletsFilters, heatmap, settings, diff --git a/src/store/reducers/olapStats.ts b/src/store/reducers/olapStats.ts index 600b5b4bbe..1233828761 100644 --- a/src/store/reducers/olapStats.ts +++ b/src/store/reducers/olapStats.ts @@ -1,4 +1,4 @@ -import {parseQueryAPIExecuteResponse} from '../../utils/query'; +import {isQueryErrorResponse, parseQueryAPIExecuteResponse} from '../../utils/query'; import {api} from './api'; @@ -22,6 +22,11 @@ export const olapApi = api.injectEndpoints({ }, {signal}, ); + + if (isQueryErrorResponse(response)) { + return {error: response}; + } + return {data: parseQueryAPIExecuteResponse(response)}; } catch (error) { return {error: error || new Error('Unauthorized')}; diff --git a/src/store/reducers/preview.ts b/src/store/reducers/preview.ts index 554718c765..b5993a2561 100644 --- a/src/store/reducers/preview.ts +++ b/src/store/reducers/preview.ts @@ -1,5 +1,5 @@ import type {ExecuteActions} from '../../types/api/query'; -import {parseQueryAPIExecuteResponse} from '../../utils/query'; +import {isQueryErrorResponse, parseQueryAPIExecuteResponse} from '../../utils/query'; import {api} from './api'; @@ -18,6 +18,11 @@ export const previewApi = api.injectEndpoints({ {schema: 'modern', query, database, action}, {signal}, ); + + if (isQueryErrorResponse(response)) { + return {error: response}; + } + return {data: parseQueryAPIExecuteResponse(response)}; } catch (error) { return {error: error || new Error('Unauthorized')}; diff --git a/src/store/reducers/shardsWorkload/shardsWorkload.ts b/src/store/reducers/shardsWorkload/shardsWorkload.ts index 02a498a2bf..579e751147 100644 --- a/src/store/reducers/shardsWorkload/shardsWorkload.ts +++ b/src/store/reducers/shardsWorkload/shardsWorkload.ts @@ -1,7 +1,7 @@ import {createSlice} from '@reduxjs/toolkit'; import type {PayloadAction} from '@reduxjs/toolkit'; -import {parseQueryAPIExecuteResponse} from '../../../utils/query'; +import {isQueryErrorResponse, parseQueryAPIExecuteResponse} from '../../../utils/query'; import {api} from '../api'; import type {ShardsWorkloadFilters} from './types'; @@ -147,6 +147,11 @@ export const shardApi = api.injectEndpoints({ signal, }, ); + + if (isQueryErrorResponse(response)) { + return {error: response}; + } + const data = parseQueryAPIExecuteResponse(response); return {data}; } catch (error) { diff --git a/src/store/reducers/tenantOverview/executeTopTables/executeTopTables.ts b/src/store/reducers/tenantOverview/executeTopTables/executeTopTables.ts index efad5982ac..3fe1e02a8b 100644 --- a/src/store/reducers/tenantOverview/executeTopTables/executeTopTables.ts +++ b/src/store/reducers/tenantOverview/executeTopTables/executeTopTables.ts @@ -1,5 +1,5 @@ import {TENANT_OVERVIEW_TABLES_LIMIT} from '../../../../utils/constants'; -import {parseQueryAPIExecuteResponse} from '../../../../utils/query'; +import {isQueryErrorResponse, parseQueryAPIExecuteResponse} from '../../../../utils/query'; import {api} from '../../api'; const getQueryText = (path: string) => { @@ -18,7 +18,7 @@ export const topTablesApi = api.injectEndpoints({ getTopTables: builder.query({ queryFn: async ({path}: {path: string}, {signal}) => { try { - const data = await window.api.sendQuery( + const response = await window.api.sendQuery( { schema: 'modern', query: getQueryText(path), @@ -27,7 +27,12 @@ export const topTablesApi = api.injectEndpoints({ }, {signal}, ); - return {data: parseQueryAPIExecuteResponse(data)}; + + if (isQueryErrorResponse(response)) { + return {error: response}; + } + + return {data: parseQueryAPIExecuteResponse(response)}; } catch (error) { return {error: error || 'Unauthorized'}; } diff --git a/src/store/reducers/tenantOverview/topQueries/tenantOverviewTopQueries.ts b/src/store/reducers/tenantOverview/topQueries/tenantOverviewTopQueries.ts index 6dade227cb..613fa1d7b1 100644 --- a/src/store/reducers/tenantOverview/topQueries/tenantOverviewTopQueries.ts +++ b/src/store/reducers/tenantOverview/topQueries/tenantOverviewTopQueries.ts @@ -1,5 +1,5 @@ import {TENANT_OVERVIEW_TABLES_LIMIT} from '../../../../utils/constants'; -import {parseQueryAPIExecuteResponse} from '../../../../utils/query'; +import {isQueryErrorResponse, parseQueryAPIExecuteResponse} from '../../../../utils/query'; import {api} from '../../api'; const getQueryText = (path: string) => { @@ -18,7 +18,7 @@ export const topQueriesApi = api.injectEndpoints({ getOverviewTopQueries: builder.query({ queryFn: async ({database}: {database: string}, {signal}) => { try { - const data = await window.api.sendQuery( + const response = await window.api.sendQuery( { schema: 'modern', query: getQueryText(database), @@ -27,7 +27,12 @@ export const topQueriesApi = api.injectEndpoints({ }, {signal}, ); - return {data: parseQueryAPIExecuteResponse(data)}; + + if (isQueryErrorResponse(response)) { + return {error: response}; + } + + return {data: parseQueryAPIExecuteResponse(response)}; } catch (error) { return {error: error || new Error('Unauthorized')}; } diff --git a/src/store/reducers/tenantOverview/topShards/tenantOverviewTopShards.ts b/src/store/reducers/tenantOverview/topShards/tenantOverviewTopShards.ts index 5569c6f168..8bf1b220a7 100644 --- a/src/store/reducers/tenantOverview/topShards/tenantOverviewTopShards.ts +++ b/src/store/reducers/tenantOverview/topShards/tenantOverviewTopShards.ts @@ -1,5 +1,5 @@ import {TENANT_OVERVIEW_TABLES_LIMIT} from '../../../../utils/constants'; -import {parseQueryAPIExecuteResponse} from '../../../../utils/query'; +import {isQueryErrorResponse, parseQueryAPIExecuteResponse} from '../../../../utils/query'; import {api} from '../../api'; function createShardQuery(path: string, tenantName?: string) { @@ -26,7 +26,7 @@ export const topShardsApi = api.injectEndpoints({ getTopShards: builder.query({ queryFn: async ({database, path = ''}: {database: string; path?: string}, {signal}) => { try { - const data = await window.api.sendQuery( + const response = await window.api.sendQuery( { schema: 'modern', query: createShardQuery(path, database), @@ -35,7 +35,12 @@ export const topShardsApi = api.injectEndpoints({ }, {signal}, ); - return {data: parseQueryAPIExecuteResponse(data)}; + + if (isQueryErrorResponse(response)) { + return {error: response}; + } + + return {data: parseQueryAPIExecuteResponse(response)}; } catch (error) { return {error: error || new Error('Unauthorized')}; } diff --git a/src/store/utils.ts b/src/store/utils.ts index 999de0ec82..798eef5770 100644 --- a/src/store/utils.ts +++ b/src/store/utils.ts @@ -1,11 +1,9 @@ -import type {Dispatch} from '@reduxjs/toolkit'; -import type {AxiosResponse} from 'axios'; - import createToast from '../utils/createToast'; +import {isAxiosResponse} from '../utils/response'; import {SET_UNAUTHENTICATED} from './reducers/authentication/authentication'; -import type {GetState} from '.'; +import type {AppDispatch, GetState} from '.'; export const nop = (result: any) => result; @@ -20,9 +18,6 @@ export function createRequestActionTypes - response && 'status' in response; - type CreateApiRequestParams = { actions: Actions; request: Promise; @@ -38,7 +33,7 @@ export function createApiRequest< request, dataHandler = nop, }: CreateApiRequestParams) { - const doRequest = async function (dispatch: Dispatch, getState: GetState) { + const doRequest = async function (dispatch: AppDispatch, getState: GetState) { dispatch({ type: actions.REQUEST, }); diff --git a/src/types/store/executeQuery.ts b/src/types/store/executeQuery.ts index ee1d71da09..4d8370740d 100644 --- a/src/types/store/executeQuery.ts +++ b/src/types/store/executeQuery.ts @@ -1,14 +1,10 @@ import type { - SEND_QUERY, changeUserInput, goToNextQuery, goToPreviousQuery, saveQueryToHistory, setTenantPath, } from '../../store/reducers/executeQuery'; -import type {ApiRequestAction} from '../../store/utils'; - -import type {IQueryResult, QueryError, QueryErrorResponse} from './query'; export interface QueryInHistory { queryText: string; @@ -24,15 +20,9 @@ export interface ExecuteQueryState { currentIndex: number; }; tenantPath?: string; - data?: IQueryResult; - stats?: IQueryResult['stats']; - error?: string | QueryErrorResponse; } -type SendQueryAction = ApiRequestAction; - export type ExecuteQueryAction = - | SendQueryAction | ReturnType | ReturnType | ReturnType diff --git a/src/types/store/explainQuery.ts b/src/types/store/explainQuery.ts deleted file mode 100644 index 2de5a94787..0000000000 --- a/src/types/store/explainQuery.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type {ExplainPlanNodeData, GraphNode, Link} from '@gravity-ui/paranoid'; - -import type {GET_EXPLAIN_QUERY, GET_EXPLAIN_QUERY_AST} from '../../store/reducers/explainQuery'; -import type {ApiRequestAction} from '../../store/utils'; -import type {PlanTable, QueryPlan, ScriptPlan} from '../api/query'; - -import type {IQueryResult, QueryError, QueryErrorResponse} from './query'; - -export interface PreparedExplainResponse { - plan?: { - links?: Link[]; - nodes?: GraphNode[]; - tables?: PlanTable[]; - version?: string; - pristine?: QueryPlan | ScriptPlan; - }; - ast?: string; -} - -export interface ExplainQueryState { - loading: boolean; - loadingAst?: boolean; - data?: PreparedExplainResponse['plan']; - dataAst?: PreparedExplainResponse['ast']; - error?: string | QueryErrorResponse; - errorAst?: string | QueryErrorResponse; -} - -type GetExplainQueryAstAction = ApiRequestAction< - typeof GET_EXPLAIN_QUERY_AST, - IQueryResult, - QueryError ->; -type GetExplainQueryAction = ApiRequestAction< - typeof GET_EXPLAIN_QUERY, - PreparedExplainResponse, - QueryError ->; - -export type ExplainQueryAction = GetExplainQueryAstAction | GetExplainQueryAction; diff --git a/src/utils/error.ts b/src/utils/error.ts deleted file mode 100644 index dcefdffe12..0000000000 --- a/src/utils/error.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type {AxiosError} from 'axios'; - -import type {IResponseError, NetworkError} from '../types/api/error'; -import type {QueryError, QueryErrorResponse} from '../types/store/query'; - -type RequestError = NetworkError | IResponseError | AxiosError | unknown; - -const isNetworkError = (error: RequestError): error is NetworkError => { - return Boolean( - error && - typeof error === 'object' && - 'message' in error && - (error as {message: unknown}).message === 'Network Error', - ); -}; - -export const parseQueryError = (error: QueryError): QueryErrorResponse | string | undefined => { - if (isNetworkError(error)) { - return error.message; - } - - // 401 Unauthorized error is handled by GenericAPI - return error ?? 'Unauthorized'; -}; diff --git a/src/utils/query.ts b/src/utils/query.ts index b5092661b8..3549bae5a1 100644 --- a/src/utils/query.ts +++ b/src/utils/query.ts @@ -4,13 +4,16 @@ import type { AnyExplainResponse, ArrayRow, ColumnType, + ErrorResponse, ExecuteModernResponse, ExecuteMultiResponse, KeyValueRow, QueryPlan, ScriptPlan, } from '../types/api/query'; -import type {IQueryResult, QueryErrorResponse, QueryMode} from '../types/store/query'; +import type {IQueryResult, QueryMode} from '../types/store/query'; + +import {isAxiosResponse, isNetworkError} from './response'; export const QUERY_ACTIONS = { execute: 'execute', @@ -161,6 +164,10 @@ const isUnsupportedType = ( ); }; +export function isQueryErrorResponse(data: unknown): data is ErrorResponse { + return Boolean(data && typeof data === 'object' && 'error' in data && 'issues' in data); +} + // Although schema is set in request, if schema is not supported default schema for the version will be used // So we should additionally parse response export const parseQueryAPIExecuteResponse = ( @@ -238,6 +245,32 @@ export const prepareQueryResponse = (data?: KeyValueRow[]) => { }); }; -export function prepareQueryError(error: QueryErrorResponse) { - return error.data?.error?.message || error.statusText; -} +export const parseQueryError = (error: unknown): ErrorResponse | string | undefined => { + if (typeof error === 'string' || isQueryErrorResponse(error)) { + return error; + } + + if (isNetworkError(error)) { + return error.message; + } + + if (isAxiosResponse(error)) { + if ('data' in error && isQueryErrorResponse(error.data)) { + return error.data; + } + + return error.statusText; + } + + return undefined; +}; + +export const parseQueryErrorToString = (error: unknown) => { + const parsedError = parseQueryError(error); + + if (typeof parsedError === 'string') { + return parsedError; + } + + return parsedError?.error?.message; +}; diff --git a/src/utils/response.ts b/src/utils/response.ts new file mode 100644 index 0000000000..2599c3569b --- /dev/null +++ b/src/utils/response.ts @@ -0,0 +1,16 @@ +import type {AxiosResponse} from 'axios'; + +import type {NetworkError} from '../types/api/error'; + +export const isNetworkError = (error: unknown): error is NetworkError => { + return Boolean( + error && + typeof error === 'object' && + 'message' in error && + error.message === 'Network Error', + ); +}; + +export const isAxiosResponse = (response: unknown): response is AxiosResponse => { + return Boolean(response && typeof response === 'object' && 'status' in response); +};