Сквозная обработка ошибок GraphQL?

Я хочу поделиться своим подходом к обработке ошибок в резолверах GraphQL (я использую GraphQL Yoga на стороне сервера) и передаче их на сторону фронтенда (где я использую Svelte + @urql/svelte).

Бэкенд

Информация об ошибке упаковки

Как я выяснил, единственный способ вернуть ошибку из конечной точки GraphQL — это бросить ошибку в резольвер. Такая конструкция накладывает некоторые ограничения на передачу данных об ошибках между бэкендом и фронтендом. Представим, что пришел какой-то плохой запрос, который сервер не может обработать, и вы хотите вернуть ошибку и, возможно, некоторые подробности о том, что именно пошло не так. Предположим, что мы хотим упаковать всю информацию в следующий интерфейс:

type ApiError = {
   code: 400,
   message: "The request was bad :("
}
Вход в полноэкранный режим Выход из полноэкранного режима

Вернемся к ограничениям — нет способа поместить что-то объектоподобное в качестве аргумента в вызов new Error(arg), но мы можем преобразовать объект ApiError в строку JSON. Это и есть тот странный маленький трюк, который позволяет упаковать любую информацию в бросаемый объект. Это все о передаче информации, но есть еще пара вещей об организации кода.
Возврат ошибки из конечной точки GraphQL путем бросания ошибки может быть удобным и безопасным с точки зрения типов с помощью функций assert в Typescript. Нет необходимости заворачивать операторы возврата в операторы if; достаточно просто выбросить ошибку и сохранить плоскую структуру кода. Давайте объединим это с упаковкой информации в объект ошибки:

export const newApiError = (errorObject: ApiError) => {
   return new Error(JSON.stringify(errorObject))
}
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь можно сделать throw newApiError(apiError).

Но все еще необходимо проверять, нужно ли нам бросать, что подразумевает операторы if. Можем ли мы сделать лучше? Да, с помощью функций assert в typescript.


Избавление от утверждений if

Представьте, что бэкенд, получающий запрос на конечной точке, требует авторизации, но в запросе нет кредов. Похоже, что мы хотим сказать: «401: Не авторизован, кредов нет, вы не пройдете». Итак, какой самый простой способ сделать это в resolver? Я полагаю, что следующее:

// get an auth token somehow
const authToken string | null = req.headers.get("Authorization") 
assert(
  authToken,
  "401: There are no creds, you shall not pass"
)
Войти в полноэкранный режим Выйти из полноэкранного режима

Пользовательский assert

Но это единственная строка, так как мы не можем передать в вызов assert ничего похожего на объект, поэтому давайте напишем свою реализацию assert:

export function assertWithApiError<T>(
    statement: T | null | undefined,
    errorObject: ApiError
): asserts statement is T {
    if (!statement) {
        throw newApiError(errorObject)
    }
}

Войти в полноэкранный режим Выйти из полноэкранного режима

Функция assert в Typescript позволяет нам иметь типобезопасные утверждения, в которых мы можем бросать любой бросаемый объект, даже созданный нами самими, с любыми данными, упакованными в ошибку.

// get an auth token somehow
const authToken: string | null = req.headers.get("Authorization")
assertWithApiError(
  authToken, 
  { message: 'Auth token is not presented', code: 401 }
)
// there is typescript knows that token is a string
Вход в полноэкранный режим Выход из полноэкранного режима

Фронтенд

Что насчет фронтенда? Как я уже сказал выше, я использую пакет @svelte/urql для работы с GraphQL в моем приложении svelte. Давайте посмотрим на код!
Начнем с обычного случая использования @svelte/urql:

// mutation function is from @svelte/urql package
const editUserMutation = mutation<{ me: User }>({
   query: EDIT_USER,
})

const { data, error } = await editMeMutation({ intro }) 
Войти в полноэкранный режим Выход из полноэкранного режима

Итак, ошибка здесь имеет тип CombinedError из @svelte/urql, который включает массив исходных ошибок GraphQL:


type OriginalError = {
   message: string
   stack: string
}

export const parseGraphqlApiErrors = (error: CombinedError): ApiError[] => error.graphQLErrors.map((e) => {
   const rawOriginalError = (e.extensions?.originalError as OriginalError).message
   return parseApiError(rawOriginalError)
})


export const parseApiError = (jsonString: string): ApiError => {
   try {
      const parsed: unknown = JSON.parse(jsonString)
      if (apiErrorTypeGuard(parsed)) {
         return parsed
      } else {
         throw new Error('got invalid api error')
      }
   } catch (e) {
      console.error(e)
      throw Error("Can't parse api error from json string")
   }
}

const apiErrorTypeGuard = (possiblyError: any): possiblyError is ApiError =>
   typeof possiblyError === 'object' && 'code' in possiblyError && 'message' in possiblyError

Войти в полноэкранный режим Выход из полноэкранного режима

В конце истории у нас есть пользовательские утверждения, которые выбрасывают ошибки, наполненные любыми данными, которые мы хотим, и код фронтенда, который может извлечь эти данные; вот и все.


Я не очень опытный пользователь GraphQL, Svelte или Urql, и я буду рад, если вы укажете мне на какое-либо существующее решение, которое лучше, чем описанное выше, и надеюсь, что мои идеи кому-нибудь пригодятся 🙂

Фото Mario Mendez

Оцените статью
Procodings.ru
Добавить комментарий