Автогенерация функций выборки данных и всей сопутствующей типизации с помощью Orval

от автора

Требования к быстрому и качественному созданию интерфейсов растут с каждым днем. Поэтому разработчики плавно отходят от написания вручную кода, который может быть сгенерирован автоматически. Мы перешли к автоматизации с таким инструментом, как Orval. Расскажем, как это было, поделимся примером кода и библиотеками (следите за ссылками в тексте).

Почему мы отказались от ручной выборки данных?

Правило нашей команды: если рутинные процессы могут быть успешно автоматизированы, мы обязательно так и сделаем. И потратим свободное время на куда более приоритетные вещи, чем написание повторяющегося кода из проекта в проект. Например, на дополнительную оптимизацию приложений.

Большинство наших проектов состоит из множества CRUD-ов, а количество запросов может превышать сотню. Ранее мы описывали запросы выборки данных и всю относящуюся к ним типизацию вручную. Выглядеть это могло так:

const getVacanciesData = async ({  locale, }: ServiceDefaultParams): Promise> => {  try {    const response: JsonResponse = await get({      url: VACANCIES_ENDPOINT,      headers: { ...getXLangHeader(locale) },    });    return { ok: true, data: response?.data || [] };  } catch (e) {    handleError(e);    return { ok: false, data: undefined };  } };   export default getVacanciesData;

Ранее мы написали оптимизированное API для отправки запросов на сервер на базе axios. Весь код с примерами сервисов на базе данного API вы сможете найти в другой нашей статье. К слову, метод get, используемый на скриншоте выше относится к данному API.

Главный минус, помимо времени: высокая вероятность допустить ошибки при создании подобных запросов. Например, при настройке опциональности внутри типов или неправильной передаче тела запроса. А в случае с автогенерацией, ошибка может быть ТОЛЬКО со стороны сервера – код опирается на yaml-файл, созданный бэкенд-разработчиком, поэтому ответственность лежит исключительно на одной стороне.

Создание тривиальных запросов на фронте буквально занимает 0 секунд.А единственный нюанс, с которым мы столкнулись за все время использования автогенерации, – модифицикация существующих запросов. А именно – создание прослойки в виде адаптора. Но она требуется не всегда.

Так использование Orval для генерации сервисов помогает сэкономить время и исключить вероятность возникновения ошибок на стороне фронтенда.

Почему Orval?

Далее мы рассмотрим самые важные настройки Orval и узнаем, как интегрировать автогенерацию в наше приложение.

Orval – это инструмент для генерации клиентского кода для RESTful API на основе OpenAPI-спецификаций. С его официальной документацией можно ознакомиться по ссылке.

Для базовой настройки поведения Orval достаточно просто создать конфигурационный файл в корне проекта. Выглядит он так – orval.config.js

Один из ключевых параметров конфигурации – это input. В orval.config.js он указывает на источник спецификации OpenAPI и включает различные опции для его настройки.

Давайте рассмотрим его подробнее.

Input

Данная часть конфигурации отвечает за импортирование и преобразование используемого OpenAPI-файла.

target — обязательный параметр, который содержит путь до openapi файла, из которого будут сгенерированы сервисы.

validation — параметр, отвечающий за использование линтера openapi-validator для openapi, разработанного IBM. По-умолчанию имеет значение false. Включает в себя стандартный набор правил, по желанию их можно расширить в .validaterc файле.

override.transformer — путь до файла, импортирующего функцию-трансформер, либо же сама функция-трансформер. Функция принимает первым параметром OpenAPIObject и должна возвращать объект с такой же структурой.

filters — принимает в себя объект с ключом tags, в который необходимо передать массив со строками, либо регулярным выражением. Будет произведена фильтрация по тегам, если они есть в openapi схеме. В случае если теги не найдены — генерация вернет пустой файл с заголовком и версией.

Output

Данная часть конфигурации отвечает за настройку генерируемого кода.

workspace — общий путь, который будет использоваться в последующих заданных путях внутри output.

target — путь к файлу, который будет включать сгенерированный код.

client — название клиента выборки данных, либо ваша собственная функция с реализацией.(angular, axios, axios-functions, react-query, svelte-query, vue-query, swr, zod, fetch.)

schemas — путь, по которому будут сгенерированы типы TS. (по-умолчанию типы генерируются в файле, указанном в target)

mode — способ генерации конечных файлов.

  • single — один общий файл, который включает в себя весь сгенерированный код.

  • split — разные файлы для запросов и типизации

  • tags — генерация собственного файла для каждого тега из openapi.

  • tags-split — генерация директории для каждого тега в целевой папке и разделение ее на несколько файлов.

Теперь рассмотрим полный флоу интеграции и пример сгенерированного кода.

Устанавливаем orval в проект.

  • yarn add -D orval или npm i –save-dev orval в зависимости от используемого менеджера пакетов.

Создаём конфигурационный файл orval.config.js в корне проекта.

import { defineConfig } from 'orval'   export default defineConfig({  base: {    input: {      target: 'https://your-domen/api.openapi',      validation: true,    },    output: {      target: './path-to-generated-file/schema.ts',      headers: true,      prettier: true,      mode: 'split',      override: {        mutator: {          path: './path-to-your-mutator/fetch.ts',          name: 'customInstance',        },      },    },  }, })

Добавляем в проект мутатор, если он вам необходим. Вы можете ограничиться стандартными клиентом выборки данных из числа предлагаемых самим Orval: Angular, Axios, Axios-functions, React-query, Svelte-query, Vue-query, Swr, Zod, Fetch.

Мы же написали свой собственный, который подходит для использования в последних версиях Next.js. Вот его код:

import { getCookie } from 'cookies-next' import qs from 'qs'   import { AUTH_TOKEN } from '../constants' import { deleteEmptyKeys } from '../helpers' import type { BaseRequestParams, ExternalRequestParams } from './typescript'   const API_URL = process.env.NEXT_PUBLIC_API_URL   const validateStatus = (status: number) => status >= 200 && status <= 399   const validateRequest = async (response: Response) => {  try {    const data = await response.json()    if (validateStatus(response.status)) {      return data    } else {      throw { ...data, status: response.status }    }  } catch (error) {    throw error  } }   export async function customInstance(  { url, method, data: body, headers, params = {} }: BaseRequestParams,  externalParams?: ExternalRequestParams ): Promise {  const baseUrl = `${API_URL}${url}`  const queryString = qs.stringify(deleteEmptyKeys(params))  const fullUrl = queryString ? `${baseUrl}?${queryString}` : baseUrl    const requestBody = body instanceof FormData ? body : JSON.stringify(body)  const authToken = typeof window !== 'undefined' ? getCookie(AUTH_TOKEN) : null    const requestConfig: RequestInit = {    method,    headers: {      'Content-Type': 'application/json',      Accept: 'application/json',      ...(authToken && { Authorization: `Bearer ${authToken}` }),      ...headers,      ...externalParams?.headers,    },    next: {      revalidate: externalParams?.revalidate,      tags: externalParams?.tag ? [externalParams?.tag] : undefined,    },    body: ['POST', 'PUT', 'PATCH'].includes(method) ? requestBody : undefined,  }    try {    const response = await fetch(fullUrl, requestConfig)    return await validateRequest(response)  } catch (error) {    console.error(`Request failed with ${error.status}: ${error.message}`)    throw error  } }

Сгенерированные сервисы выглядят так:

/** * @summary Get config for payout */ export const getConfigForPayout = (options?: SecondParameter) => {  return customInstance({ url: `/api/payout/config`, method: 'GET' }, options) }   /** * Method blocks specified user's balance for payout * @summary Request payout action */ export const requestPayoutAction = (  requestPayoutActionBody: RequestPayoutActionBody,  options?: SecondParameter ) => {  return customInstance(    {      url: `/api/payout/request`,      method: 'POST',      headers: { 'Content-Type': 'application/json' },      data: requestPayoutActionBody,    },    options  ) }

Обратите внимание на функцию customInstance – это мутатор, в который Orval передаёт все необходимые данные. Вы можете реализовать эту функцию, как вам нужно. Главное правильно принять входные параметры.

Сгенерированная типизация выглядит так:

export type GetConfigForPayoutResult = NonNullable>>  export type GetConfigForPayout200DataRestrictions = {  max_amount: number  min_amount: number }  export type GetConfigForPayout200DataAccount = {  created_at: string  id: number  type: string }  export type GetConfigForPayout200Data = {  account?: GetConfigForPayout200DataAccount  balance: number  restrictions: GetConfigForPayout200DataRestrictions }  export type GetConfigForPayout200 = {  data?: GetConfigForPayout200Data }

OpenAPI спецификация для данных сервисов выглядит так:

/api/payout/config:     get:       summary: 'Get config for payout'       operationId: getConfigForPayout       description: ''       parameters: []       responses:         200:           description: ''           content:             application/json:               schema:                 type: object                 example:                   data:                     balance: 180068.71618                     restrictions:                       max_amount: 63012600.110975                       min_amount: 22.2679516                     account:                       id: 20                       type: eum                       created_at: '1970-01-02T03:46:40.000000Z'                 properties:                   data:                     type: object                     properties:                       balance:                         type: number                         example: 180068.71618                       restrictions:                         type: object                         properties:                           max_amount:                             type: number                             example: 63012600.110975                           min_amount:                             type: number                             example: 22.2679516                         required:                           - max_amount                           - min_amount                       account:                         type: object                         properties:                           id:                             type: integer                             example: 20                           type:                             type: string                             example: eum                           created_at:                             type: string                             example: '1970-01-02T03:46:40.000000Z'                         required:                           - id                           - type                           - created_at                     required:                       - balance                       - restrictions       tags:         - Payout   /api/payout/request:     post:       summary: 'Request payout action'       operationId: requestPayoutAction       description: "Method blocks specified user's balance for payout"       parameters: []       responses:         200:           description: ''           content:             application/json:               schema:                 type: object                 example:                   data: null                 properties:                   data:                     type: string                     example: null       tags:         - Payout       requestBody:         required: true         content:           application/json:             schema:               type: object               properties:                 type:                   type: string                   description: ''                   example: withdrawal                   enum:                     - withdrawal                 method_id:                   type: integer                   description: 'Must be at least 1.'                   example: 12                 amount:                   type: number                   description: 'Must be at least 0.01. Must not be greater than 99999999.99.'                   example: 17               required:                 - type                 - method_id                 - amount

После настройки автогенерации всё, что нам необходимо для удобного использования, – документация, где мы смотрим наименование необходимого сервиса, а затем используем автоимпорт.

Сперва мы внедрили данную конфигурацию в один из наших проектов и вносили корректировки, опираясь на полученные проблемы. Когда мы убедились, что все работает, как и планировалось, то начали использовать Orval для автоматической генерации функций выборки данных на всех новых проектах. Познакомиться с ними можно здесь.

Почему вам нужна автогенерация?

Один раз настроив Orval под реалии своего проекта, вы сэкономите кучу времени, которое лучше потратить на оптимизацию или рефакторинг.

Обязательно используйте его для:

  • Крупных проектов с большим количеством эндпоинтов – ваши фронтендеры избавятся от необходимости вручную писать повторяющийся код и станут не только свободнее для более приоритетных задач, но и счастливее;

  • Команд, в которых работает сразу несколько разработчиков – Orval генерирует стандартизированный код, что помогает поддерживать единообразие и упрощает работу с кодовой базой;

  • Кастомизации под другие проекты – инструмент можно адаптировать под конкретные нужды проекта, включая трансформацию данных, фильтрацию эндпоинтов и другие настройки.

Обновлять API также станет проще и быстрее: когда спецификация меняется, Orval позволяет быстро сгенерировать обновленные функции и типы, сокращая риск появления устаревшего или некорректного кода в проекте.


ссылка на оригинал статьи https://habr.com/ru/articles/848182/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *