diff --git a/CHANGELOG.md b/CHANGELOG.md index c49f87c..cb30d89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ Формат основан на [Keep a Changelog](https://keepachangelog.com/ru/1.0.0/), и этот проект придерживается [Semantic Versioning](https://semver.org/lang/ru/). +## [3.1.0] - 2025-11-06 + +### Добавлено + +- Добавлена утилита `parseApiRequest` для упрощения создания запросов к API на основе спецификации OpenAPI и эндпоинтов + +### Исправлено + +- Исправлена генерация типов в корневой папке проекта при использовании команды генерации типов + ## [3.0.0] - 2025-10-30 ### Изменено diff --git a/README.md b/README.md index 60ff341..ca2c480 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,72 @@ async getEncryptedData(): Promise { } ``` +## Утилита: parseApiRequest + +`parseApiRequest` помогает собрать корректный `Request` для `fetch` из структурированного описания запроса: базовый URL, путь с параметрами, метод, заголовки, тело и query-параметры. + +Импорт: + +```ts +import { parseApiRequest } from '@gratio/api'; +``` + +### Пример: GET с path и query параметрами + +```ts +const request = parseApiRequest({ + baseUrl: 'https://api.example.com', + path: '/users/{id}', + method: 'get', + status: '200', + pathParams: { id: '123' }, + queryParams: { search: 'john', limit: '10' }, + headers: { Authorization: `Bearer ${token}` }, +}); + +const response = await fetch(request); +const data = await response.json(); +``` + +### Пример: POST с JSON-телом + +```ts +type CreateUserDto = { name: string; email: string }; + +const request = parseApiRequest({ + baseUrl: 'https://api.example.com', + path: '/users', + method: 'post', + status: '201', + body: { name: 'John', email: 'john@example.com' } satisfies CreateUserDto, +}); + +const response = await fetch(request); +``` + +### Пример: POST с FormData + +```ts +const form = new FormData(); +form.append('avatar', file); + +const request = parseApiRequest({ + baseUrl: 'https://api.example.com', + path: '/users/{id}/avatar', + method: 'post', + status: '200', + pathParams: { id: '123' }, + body: form, // Content-Type проставлять не нужно — браузер выставит boundary автоматически +}); + +await fetch(request); +``` + +Примечания: + +- Если указан `FormData`, заголовок `Content-Type` не добавляется вручную. +- Путь может включать параметры вида `{id}` — они будут подставлены из `pathParams` с URL-экранированием. Query-параметры добавляются из `queryParams`. + ## `@gratio/api gen types --help` ``` diff --git a/package.json b/package.json index a864bb5..78c1cb6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@gratio/api", - "version": "3.0.0", + "version": "3.1.0", "description": "TypeScript common types for Gratio projects API responses and requests", "homepage": "https://github.com/Gratio-tech/Api#readme", "packageManager": "bun@1.2.17", diff --git a/src/commands/gen/types.ts b/src/commands/gen/types.ts index fa3ea68..944faa4 100644 --- a/src/commands/gen/types.ts +++ b/src/commands/gen/types.ts @@ -68,7 +68,9 @@ export default defineCommand({ } if (typeof outputPath === 'string') { - mkdirSync(dirname(outputPath), { recursive: true }); + const dirName = dirname(outputPath); + if (dirName !== '.') mkdirSync(dirName, { recursive: true }); + await writeFile(outputPath, contents); console.info(`✓ ${outputPath}`); } diff --git a/src/index.ts b/src/index.ts index a1b7d34..9223848 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,2 @@ +export * from '~/utils/parseApiRequest'; export * from '~/types/api'; diff --git a/src/utils/parseApiRequest.ts b/src/utils/parseApiRequest.ts new file mode 100644 index 0000000..d9d59a4 --- /dev/null +++ b/src/utils/parseApiRequest.ts @@ -0,0 +1,41 @@ +interface ApiResponse { + path: string; + method: string; + status: string; +} + +interface ApiRequest extends ApiResponse { + baseUrl: string; + headers?: Record; + body?: Record | FormData; + queryParams?: Record; + pathParams?: Record; +} + +/** Parse ApiRequest to a Request object. */ +export function parseApiRequest(request: ApiRequest): Request { + // Parse query parameters + const url = new URL(request.path, request.baseUrl); + if (request.queryParams) { + for (const [key, value] of Object.entries(request.queryParams)) { + url.searchParams.append(key, String(value)); + } + } + + // Parse path parameters + for (const [key, value] of Object.entries(request.pathParams || {})) { + url.pathname = url.pathname.replace(`{${key}}`, encodeURIComponent(String(value))); + } + + // Parse headers + const headers = new Headers(request.headers || {}); + if (request.body && !(request.body instanceof FormData)) { + headers.append('Content-Type', 'application/json'); + } + + return new Request(url.toString(), { + method: request.method.toUpperCase(), + headers, + body: request.body ? (request.body instanceof FormData ? request.body : JSON.stringify(request.body)) : undefined, + }); +} diff --git a/src/utils/template.ts b/src/utils/template.ts index 598d3aa..078a441 100644 --- a/src/utils/template.ts +++ b/src/utils/template.ts @@ -12,7 +12,9 @@ export function createTemplate(path: string, isRaw = false) { const write = (outputPath: string): void => { try { - mkdirSync(dirname(outputPath), { recursive: true }); + const dirName = dirname(outputPath); + if (dirName !== '.') mkdirSync(dirName, { recursive: true }); + writeFileSync(outputPath, template); console.info(`✓ ${outputPath}`); } catch {