Автогенерация функций выборки данных и всей сопутствующей типизации с помощью Orval
Почему мы отказались от ручной выборки данных?
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;
А единственный нюанс, с которым мы столкнулись за все время использования автогенерации, – модифицикация существующих запросов. А именно – создание прослойки в виде адаптора. Но она требуется не всегда.
Почему Orval?
Input
validation - параметр, отвечающий за использование линтера openapi-validator для openapi, разработанного IBM. По-умолчанию имеет значение false. Включает в себя стандартный набор правил, по желанию их можно расширить в .validaterc файле.
override.transformer - путь до файла, импортирующего функцию-трансформер, либо же сама функция-трансформер. Функция принимает первым параметром OpenAPIObject и должна возвращать объект с такой же структурой.
filters - принимает в себя объект с ключом tags, в который необходимо передать массив со строками, либо регулярным выражением. Будет произведена фильтрация по тегам, если они есть в openapi схеме. В случае если теги не найдены - генерация вернет пустой файл с заголовком и версией.
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 - генерация директории для каждого тега в целевой папке и разделение ее на несколько файлов.
- yarn add -D orval или npm i –save-dev orval в зависимости от используемого менеджера пакетов.
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',
},
},
},
},
})
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
)
}
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
}
/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 генерирует стандартизированный код, что помогает поддерживать единообразие и упрощает работу с кодовой базой;
- Кастомизации под другие проекты – инструмент можно адаптировать под конкретные нужды проекта, включая трансформацию данных, фильтрацию эндпоинтов и другие настройки.