diff --git a/app/composables/api/appSetting/useAppSettingAdmin.api.ts b/app/composables/api/appSetting/useAppSettingAdmin.api.ts index 5fcb2859c..ce6f37e32 100644 --- a/app/composables/api/appSetting/useAppSettingAdmin.api.ts +++ b/app/composables/api/appSetting/useAppSettingAdmin.api.ts @@ -1,7 +1,6 @@ import type { AdminAppSettingsDTO, ApiResult, - AppError, } from '@/types' type AppSettingsPayload = Record @@ -11,14 +10,7 @@ const loadingUpdateSettings = ref(false) export const useAppSettingAdmin = () => { const { $toast } = useNuxtApp() - - const handleError = (err: unknown) => { - const error = err as AppError - - if (error.response?.status === 400) { - $toast.error(error.response.data?.message || '') - } - } + const { handleApiResponseError, handleApiCatchError, createApiFailure } = useApiErrorHandler() const getSettings = async () => { loadingGetSettings.value = true @@ -28,16 +20,17 @@ export const useAppSettingAdmin = () => { '/api/v2/admin/applicationsettings', ) + if (response.succeeded && response.data) { + return response + } + + handleApiResponseError(response) return response } catch (err: unknown) { - handleError(err) + handleApiCatchError(err) - return { - succeeded: false, - status: 0, - data: null, - } + return createApiFailure(err) } finally { loadingGetSettings.value = false @@ -53,23 +46,19 @@ export const useAppSettingAdmin = () => { payload, ) - if (response.data) { + if (response.succeeded && response.data) { $toast.success('Your settings have been changed successfully.') } else { - $toast.error('The operation failed. Please try again later.') + handleApiResponseError(response) } return response } catch (err: unknown) { - handleError(err) + handleApiCatchError(err) - return { - succeeded: false, - status: 0, - data: false, - } + return createApiFailure(err, false) } finally { loadingUpdateSettings.value = false diff --git a/app/composables/api/tags/useTagAdmin.api.ts b/app/composables/api/tags/useTagAdmin.api.ts index de448ac3d..b8c845fa2 100644 --- a/app/composables/api/tags/useTagAdmin.api.ts +++ b/app/composables/api/tags/useTagAdmin.api.ts @@ -2,7 +2,6 @@ import type { AddAdminTagDTO, AdminTagDTO, ApiResult, - AppError, GetAdminTagParams, ResponseListDTO, } from '@/types' @@ -20,22 +19,7 @@ const NAME = 'Tag' export const useTagAdmin = () => { const { $toast } = useNuxtApp() - - const handleError = (err: unknown) => { - const error = err as AppError - if (error.response?.status === 400) { - $toast.error(error.response.data?.message || '') - } - } - - const showResponseError = (response: ApiResult) => { - if (response.errors && response.errors.length > 0) { - $toast.error(response.errors[0].message || '') - } - else { - $toast.error('The operation failed. Please try again later.') - } - } + const { handleApiResponseError, handleApiCatchError, createApiFailure } = useApiErrorHandler() const getData = async (params: GetAdminTagParams) => { loadingGetData.value = true @@ -52,7 +36,7 @@ export const useTagAdmin = () => { ApiResult> >('/api/v2/admin/tags', query) - if (response.data) { + if (response.succeeded && response.data) { data.value = response.data.list totalCount.value = response.data.totalRecordsCount pageCount.value = Math.ceil(totalCount.value / params.pageSize) @@ -61,10 +45,14 @@ export const useTagAdmin = () => { data.value = [] totalCount.value = 0 pageCount.value = 0 + handleApiResponseError(response) } } catch (err: unknown) { - handleError(err) + data.value = [] + totalCount.value = 0 + pageCount.value = 0 + handleApiCatchError(err) } finally { loadingGetData.value = false @@ -79,16 +67,17 @@ export const useTagAdmin = () => { `/api/v2/admin/tags/${id}`, ) + if (response.succeeded && response.data) { + return response + } + + handleApiResponseError(response) return response } catch (err: unknown) { - handleError(err) + handleApiCatchError(err) - return { - succeeded: false, - status: 0, - data: null, - } + return createApiFailure(err) } finally { loadingGetItemById.value = false @@ -107,19 +96,15 @@ export const useTagAdmin = () => { $toast.success(`${NAME} deleted successfully!`) } else { - showResponseError(response) + handleApiResponseError(response) } return response } catch (err: unknown) { - handleError(err) + handleApiCatchError(err) - return { - succeeded: false, - status: 0, - data: false, - } + return createApiFailure(err, false) } finally { loadingDeleteItem.value = false @@ -139,19 +124,15 @@ export const useTagAdmin = () => { $toast.success(`${NAME} created successfully!`) } else { - showResponseError(response) + handleApiResponseError(response) } return response } catch (err: unknown) { - handleError(err) + handleApiCatchError(err) - return { - succeeded: false, - status: 0, - data: false, - } + return createApiFailure(err, false) } finally { loadingAddItem.value = false @@ -171,19 +152,15 @@ export const useTagAdmin = () => { $toast.success(`${NAME} updated successfully!`) } else { - showResponseError(response) + handleApiResponseError(response) } return response } catch (err: unknown) { - handleError(err) + handleApiCatchError(err) - return { - succeeded: false, - status: 0, - data: false, - } + return createApiFailure(err, false) } finally { loadingEditItem.value = false diff --git a/app/composables/api/tags/useTags.api.ts b/app/composables/api/tags/useTags.api.ts index 02d271b1e..435c74da8 100644 --- a/app/composables/api/tags/useTags.api.ts +++ b/app/composables/api/tags/useTags.api.ts @@ -1,6 +1,5 @@ import type { ApiResult, - AppError, TagTypeDTO, TagDTO, } from '@/types' @@ -9,7 +8,7 @@ const data = ref([]) const loadingGetData = ref(true) export const useTags = () => { - const { $toast } = useNuxtApp() + const { handleApiResponseError, handleApiCatchError } = useApiErrorHandler() const getData = async (type: TagTypeDTO) => { loadingGetData.value = true @@ -17,18 +16,18 @@ export const useTags = () => { const response = await useApiService.get< ApiResult >(`/api/v2/tags/${type}`) - if (response.data) { + + if (response.succeeded && response.data) { data.value = response.data } else { data.value = [] + handleApiResponseError(response) } } catch (err: unknown) { - const error = err as AppError - if (error.response?.status === 400) { - $toast.error(error.response.data?.message || '') - } + data.value = [] + handleApiCatchError(err) } finally { loadingGetData.value = false diff --git a/app/composables/useApiErrorHandler.ts b/app/composables/useApiErrorHandler.ts new file mode 100644 index 000000000..4d7a6d3bb --- /dev/null +++ b/app/composables/useApiErrorHandler.ts @@ -0,0 +1,151 @@ +import type { ApiErrorItem, ApiResult, NormalizedApiError } from '@/types' + +const DEFAULT_API_ERROR_MESSAGE = 'The operation failed. Please try again later.' +const DEFAULT_DISCONNECT_MESSAGE = 'Connection failed. Please check your internet connection.' + +const isRecord = (value: unknown): value is Record => { + return typeof value === 'object' && value !== null +} + +const getString = (value: unknown): string | undefined => { + return typeof value === 'string' && value.trim() ? value : undefined +} + +const getNumber = (value: unknown): number | undefined => { + return typeof value === 'number' ? value : undefined +} + +const getErrorItems = (value: unknown): ApiErrorItem[] => { + if (!Array.isArray(value)) { + return [] + } + + return value + .filter(isRecord) + .map(item => ({ + message: getString(item.message) || DEFAULT_API_ERROR_MESSAGE, + code: getString(item.code), + reference: getString(item.reference), + info: getString(item.info), + value: getString(item.value), + })) +} + +export const useApiErrorHandler = () => { + const { $toast } = useNuxtApp() + + const showErrorToast = (message: string, showToast = true) => { + if (showToast) { + $toast.error(message) + } + } + + const normalizeApiResponseError = ( + response: ApiResult, + fallbackMessage = DEFAULT_API_ERROR_MESSAGE, + ): NormalizedApiError => { + const errors = getErrorItems(response.errors) + const errorMessage = getString(response.error) + const message = errors[0]?.message || errorMessage || fallbackMessage + + return { + message: response.status === 0 && !errorMessage ? DEFAULT_DISCONNECT_MESSAGE : message, + status: response.status === 0 ? 'disconnect' : response.status, + errors, + raw: response, + } + } + + const normalizeApiCatchError = ( + error: unknown, + fallbackMessage = DEFAULT_API_ERROR_MESSAGE, + ): NormalizedApiError => { + if (typeof error === 'string') { + return { + message: error || fallbackMessage, + status: 'disconnect', + errors: [], + raw: error, + } + } + + if (!isRecord(error)) { + return { + message: fallbackMessage, + status: 'disconnect', + errors: [], + raw: error, + } + } + + const response = isRecord(error.response) ? error.response : undefined + const responseData = response && isRecord(response.data) ? response.data : undefined + const directData = isRecord(error.data) ? error.data : undefined + const data = responseData || directData + + const status = getNumber(response?.status) + ?? getNumber(data?.status) + ?? getNumber(error.status) + ?? getNumber(error.statusCode) + ?? 0 + + const errors = getErrorItems(data?.errors) + const message = errors[0]?.message + || getString(data?.message) + || getString(data?.error) + || getString(error.message) + || getString(error.statusMessage) + || fallbackMessage + + return { + message: status === 0 && message === fallbackMessage ? DEFAULT_DISCONNECT_MESSAGE : message, + status: status === 0 ? 'disconnect' : status, + errors, + raw: error, + } + } + + const handleApiResponseError = ( + response: ApiResult, + fallbackMessage = DEFAULT_API_ERROR_MESSAGE, + showToast = true, + ) => { + const normalizedError = normalizeApiResponseError(response, fallbackMessage) + showErrorToast(normalizedError.message, showToast) + return normalizedError + } + + const handleApiCatchError = ( + error: unknown, + fallbackMessage = DEFAULT_API_ERROR_MESSAGE, + showToast = true, + ) => { + const normalizedError = normalizeApiCatchError(error, fallbackMessage) + showErrorToast(normalizedError.message, showToast) + return normalizedError + } + + const createApiFailure = ( + error: unknown, + data: T | null = null, + fallbackMessage = DEFAULT_API_ERROR_MESSAGE, + ): ApiResult => { + const normalizedError = normalizeApiCatchError(error, fallbackMessage) + + return { + data, + status: normalizedError.status === 'disconnect' ? 0 : normalizedError.status, + error: normalizedError.message, + succeeded: false, + errors: normalizedError.errors, + } + } + + return { + normalizeApiResponseError, + normalizeApiCatchError, + handleApiResponseError, + handleApiCatchError, + createApiFailure, + } +} diff --git a/app/types/api/index.ts b/app/types/api/index.ts index 488c490cc..18d34ec8d 100644 --- a/app/types/api/index.ts +++ b/app/types/api/index.ts @@ -1,17 +1,17 @@ +export interface ApiErrorItem { + message: string + code?: string + reference?: string + info?: string + value?: string +} + export interface ApiResult { data: T | null status: number error?: unknown succeeded: boolean - errors: [ - { - message: string - code: string - reference: string - info: string - value: string - }, - ] + errors?: ApiErrorItem[] } export interface ApiErrorResult { status?: number @@ -24,12 +24,22 @@ export interface ApiErrorResult { export interface AppError { response?: ApiErrorResult + data?: unknown message?: string status?: number + statusCode?: number + statusMessage?: string } export type StatusErrorCodeApp = 500 | 404 | 403 | 401 | 'disconnect' +export interface NormalizedApiError { + message: string + status: number | 'disconnect' + errors: ApiErrorItem[] + raw: unknown +} + export interface ResponseListDTO { list: T[] totalRecordsCount: number