mirror of
https://github.com/gromlab-ru/slm-design.git
synced 2026-08-22 15:30:16 +03:00
chore: demo app
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
/** Базовый URL локального Simple API. */
|
||||
export const SIMPLE_REST_API_BASE_URL =
|
||||
import.meta.env.VITE_SIMPLE_API_URL ?? 'http://localhost:3001'
|
||||
|
||||
/** Максимальное время выполнения одного REST-запроса. */
|
||||
export const SIMPLE_REST_API_TIMEOUT_MS = 12_000
|
||||
@@ -0,0 +1,2 @@
|
||||
export { SimpleRestApiError } from './simple-rest-api.error'
|
||||
export { toSimpleRestApiError } from './to-simple-rest-api-error'
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Нормализованная ошибка Simple API для source adapters.
|
||||
*/
|
||||
export class SimpleRestApiError extends Error {
|
||||
/** HTTP-статус ответа, если сервер успел его вернуть. */
|
||||
readonly status: number | null
|
||||
/** Стабильный код ошибки внешнего API. */
|
||||
readonly code: string
|
||||
/** Идентификатор запроса для диагностики backend-логов. */
|
||||
readonly requestId: string | null
|
||||
|
||||
constructor(params: {
|
||||
/** HTTP-статус ответа, если сервер успел его вернуть. */
|
||||
status: number | null
|
||||
/** Стабильный код ошибки внешнего API. */
|
||||
code: string
|
||||
/** Безопасное публичное сообщение backend. */
|
||||
message: string
|
||||
/** Идентификатор запроса для диагностики backend-логов. */
|
||||
requestId: string | null
|
||||
}) {
|
||||
super(params.message)
|
||||
this.name = 'SimpleRestApiError'
|
||||
this.status = params.status
|
||||
this.code = params.code
|
||||
this.requestId = params.requestId
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
import { ApiError } from '../generated'
|
||||
import { SimpleRestApiError } from './simple-rest-api.error'
|
||||
|
||||
const errorResponseSchema = z.object({
|
||||
code: z.string(),
|
||||
message: z.string(),
|
||||
requestId: z.string().optional()
|
||||
})
|
||||
|
||||
/**
|
||||
* Преобразует transport failure в стабильную ошибку REST-модуля.
|
||||
*/
|
||||
export const toSimpleRestApiError = (error: unknown): SimpleRestApiError => {
|
||||
if (error instanceof SimpleRestApiError) {
|
||||
return error
|
||||
}
|
||||
|
||||
if (error instanceof ApiError) {
|
||||
const parsedError = errorResponseSchema.safeParse(error.error)
|
||||
|
||||
if (parsedError.success) {
|
||||
return new SimpleRestApiError({
|
||||
status: error.status,
|
||||
code: parsedError.data.code,
|
||||
message: parsedError.data.message,
|
||||
requestId: parsedError.data.requestId ?? null
|
||||
})
|
||||
}
|
||||
|
||||
return new SimpleRestApiError({
|
||||
status: error.status,
|
||||
code: `HTTP_${error.status}`,
|
||||
message: error.message,
|
||||
requestId: null
|
||||
})
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return new SimpleRestApiError({
|
||||
status: null,
|
||||
code: 'NETWORK_ERROR',
|
||||
message: error.message,
|
||||
requestId: null
|
||||
})
|
||||
}
|
||||
|
||||
return new SimpleRestApiError({
|
||||
status: null,
|
||||
code: 'UNKNOWN_ERROR',
|
||||
message: 'Не удалось выполнить запрос к Simple API.',
|
||||
requestId: null
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
|
||||
/*
|
||||
* ----------------------------------------------------------------------
|
||||
* ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
|
||||
* ## ##
|
||||
* ## Не редактируйте вручную: изменения будут перезаписаны. ##
|
||||
* ## Для изменений перегенерируйте клиент. ##
|
||||
* ## ##
|
||||
* ## Генератор: @gromlab/api-codegen ##
|
||||
* ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
|
||||
* ----------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import type { ApiRequestClient } from "./http-client";
|
||||
|
||||
export type ApiOperation<TClient extends ApiRequestClient = ApiRequestClient> =
|
||||
(client: TClient, ...args: any[]) => any;
|
||||
|
||||
export type ApiTree<TClient extends ApiRequestClient = ApiRequestClient> = {
|
||||
readonly [key: string]: ApiOperation<TClient> | ApiTree<TClient>;
|
||||
};
|
||||
|
||||
export type BoundApi<TTree, TClient extends ApiRequestClient> = {
|
||||
readonly [K in keyof TTree]: TTree[K] extends (
|
||||
client: TClient,
|
||||
...args: infer Args
|
||||
) => infer Result
|
||||
? (...args: Args) => Result
|
||||
: TTree[K] extends ApiTree<TClient>
|
||||
? BoundApi<TTree[K], TClient>
|
||||
: never;
|
||||
};
|
||||
|
||||
export const createApiClient = <
|
||||
TClient extends ApiRequestClient,
|
||||
const TTree extends ApiTree<TClient>,
|
||||
>(
|
||||
client: TClient,
|
||||
tree: TTree,
|
||||
): BoundApi<TTree, TClient> => {
|
||||
const bindNode = (
|
||||
node: ApiOperation<TClient> | ApiTree<TClient>,
|
||||
): unknown => {
|
||||
if (typeof node === "function") {
|
||||
return (...args: unknown[]) => node(client, ...args);
|
||||
}
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(node).map(([key, value]) => [
|
||||
key,
|
||||
bindNode(value as ApiOperation<TClient> | ApiTree<TClient>),
|
||||
]),
|
||||
);
|
||||
};
|
||||
|
||||
return bindNode(tree) as BoundApi<TTree, TClient>;
|
||||
};
|
||||
@@ -0,0 +1,743 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
|
||||
/*
|
||||
* ----------------------------------------------------------------------
|
||||
* ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
|
||||
* ## ##
|
||||
* ## Не редактируйте вручную: изменения будут перезаписаны. ##
|
||||
* ## Для изменений перегенерируйте клиент. ##
|
||||
* ## ##
|
||||
* ## Генератор: @gromlab/api-codegen ##
|
||||
* ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
|
||||
* ----------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
export interface HealthDataDto {
|
||||
/** @example "simple" */
|
||||
application: HealthDataDtoApplicationEnum;
|
||||
/** @example "ok" */
|
||||
status: HealthDataDtoStatusEnum;
|
||||
/** @format date-time */
|
||||
timestamp: string;
|
||||
/** @example "1.0.0" */
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface HealthResponseDto {
|
||||
data: HealthDataDto;
|
||||
}
|
||||
|
||||
export interface LoginDto {
|
||||
/**
|
||||
* @format email
|
||||
* @example "admin@demo.local"
|
||||
*/
|
||||
email: string;
|
||||
/**
|
||||
* @format password
|
||||
* @minLength 8
|
||||
* @example "demo1234"
|
||||
*/
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface JwtTokensDto {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
/**
|
||||
* Access-token lifetime in seconds.
|
||||
* @example 60
|
||||
*/
|
||||
expiresIn: number;
|
||||
/** @example "Bearer" */
|
||||
tokenType: JwtTokensDtoTokenTypeEnum;
|
||||
}
|
||||
|
||||
export interface SimpleUserDto {
|
||||
/** @example "user-admin" */
|
||||
id: string;
|
||||
/**
|
||||
* @format email
|
||||
* @example "admin@demo.local"
|
||||
*/
|
||||
email: string;
|
||||
/** @example "Demo Admin" */
|
||||
name: string;
|
||||
role: SimpleUserDtoRoleEnum;
|
||||
/** @example "https://i.pravatar.cc/160?img=12" */
|
||||
avatarUrl: object | null;
|
||||
}
|
||||
|
||||
export interface JwtAuthDataDto {
|
||||
tokens: JwtTokensDto;
|
||||
user: SimpleUserDto;
|
||||
}
|
||||
|
||||
export interface JwtAuthResponseDto {
|
||||
data: JwtAuthDataDto;
|
||||
}
|
||||
|
||||
export interface ErrorDetailDto {
|
||||
/** @example "email" */
|
||||
field?: string;
|
||||
/** @example "must be an email" */
|
||||
message: string;
|
||||
/** @example "isEmail" */
|
||||
code?: string;
|
||||
}
|
||||
|
||||
export interface ErrorResponseDto {
|
||||
/** @example 404 */
|
||||
statusCode: number;
|
||||
/** @example "PRODUCT_NOT_FOUND" */
|
||||
code: string;
|
||||
/** @example "Product not found" */
|
||||
message: string;
|
||||
details: ErrorDetailDto[];
|
||||
/**
|
||||
* @format date-time
|
||||
* @example "2026-07-30T12:00:00.000Z"
|
||||
*/
|
||||
timestamp: string;
|
||||
/** @example "/api/v1/products/product-404" */
|
||||
path: string;
|
||||
/** @example "req-5c9f7a3d" */
|
||||
requestId: string;
|
||||
}
|
||||
|
||||
export interface RefreshTokenDto {
|
||||
/** Refresh token returned by login or the previous refresh call. */
|
||||
refreshToken: string;
|
||||
}
|
||||
|
||||
export interface SimpleUserResponseDto {
|
||||
data: SimpleUserDto;
|
||||
}
|
||||
|
||||
export type Object = object;
|
||||
|
||||
export interface SimpleProductDto {
|
||||
/** @example "product-keyboard" */
|
||||
id: string;
|
||||
/** @example "Mechanical Keyboard" */
|
||||
name: string;
|
||||
/** @example "mechanical-keyboard" */
|
||||
slug: string;
|
||||
/** @example "Hot-swappable compact keyboard." */
|
||||
description: string;
|
||||
/**
|
||||
* Price in the smallest currency unit.
|
||||
* @example 12990
|
||||
*/
|
||||
priceCents: number;
|
||||
/** @example "USD" */
|
||||
currency: SimpleProductDtoCurrencyEnum;
|
||||
/** @example "category-electronics" */
|
||||
categoryId: string;
|
||||
/**
|
||||
* @min 0
|
||||
* @example 24
|
||||
*/
|
||||
stock: number;
|
||||
/**
|
||||
* @min 0
|
||||
* @max 5
|
||||
* @example 4.8
|
||||
*/
|
||||
rating: number;
|
||||
/**
|
||||
* @format uri
|
||||
* @example "https://picsum.photos/seed/keyboard/640/480"
|
||||
*/
|
||||
imageUrl: string;
|
||||
/** @format date-time */
|
||||
createdAt: string;
|
||||
/**
|
||||
* Optimistic-lock version.
|
||||
* @example 1
|
||||
*/
|
||||
version: number;
|
||||
}
|
||||
|
||||
export interface PageMetaDto {
|
||||
/**
|
||||
* @min 1
|
||||
* @example 1
|
||||
*/
|
||||
page: number;
|
||||
/**
|
||||
* @min 1
|
||||
* @example 20
|
||||
*/
|
||||
limit: number;
|
||||
/**
|
||||
* @min 0
|
||||
* @example 48
|
||||
*/
|
||||
total: number;
|
||||
/**
|
||||
* @min 0
|
||||
* @example 3
|
||||
*/
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export interface ProductsResponseDto {
|
||||
data: SimpleProductDto[];
|
||||
meta: PageMetaDto;
|
||||
}
|
||||
|
||||
export interface ProductResponseDto {
|
||||
data: SimpleProductDto;
|
||||
}
|
||||
|
||||
export interface CreateProductDto {
|
||||
/** @example "USB-C Dock" */
|
||||
name: string;
|
||||
/** @example "Dock with HDMI, Ethernet and power delivery." */
|
||||
description: string;
|
||||
/**
|
||||
* @min 0
|
||||
* @example 8990
|
||||
*/
|
||||
priceCents: number;
|
||||
/** @example "USD" */
|
||||
currency: CreateProductDtoCurrencyEnum;
|
||||
/** @example "category-electronics" */
|
||||
categoryId: string;
|
||||
/**
|
||||
* @min 0
|
||||
* @example 15
|
||||
*/
|
||||
stock: number;
|
||||
/**
|
||||
* @format uri
|
||||
* @example "https://picsum.photos/seed/dock/640/480"
|
||||
*/
|
||||
imageUrl: string;
|
||||
}
|
||||
|
||||
export interface UpdateProductDto {
|
||||
/** @example "USB-C Dock" */
|
||||
name?: string;
|
||||
/** @example "Dock with HDMI, Ethernet and power delivery." */
|
||||
description?: string;
|
||||
/**
|
||||
* @min 0
|
||||
* @example 8990
|
||||
*/
|
||||
priceCents?: number;
|
||||
/** @example "USD" */
|
||||
currency?: UpdateProductDtoCurrencyEnum;
|
||||
/** @example "category-electronics" */
|
||||
categoryId?: string;
|
||||
/**
|
||||
* @min 0
|
||||
* @example 15
|
||||
*/
|
||||
stock?: number;
|
||||
/**
|
||||
* @format uri
|
||||
* @example "https://picsum.photos/seed/dock/640/480"
|
||||
*/
|
||||
imageUrl?: string;
|
||||
/**
|
||||
* Version last read by the frontend.
|
||||
* @min 1
|
||||
* @example 1
|
||||
*/
|
||||
version: number;
|
||||
}
|
||||
|
||||
export interface MutationResultDto {
|
||||
/** @example "product-001" */
|
||||
id: string;
|
||||
/** @example true */
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export interface MutationResponseDto {
|
||||
data: MutationResultDto;
|
||||
}
|
||||
|
||||
export interface SimpleCategoryDto {
|
||||
/** @example "category-electronics" */
|
||||
id: string;
|
||||
/** @example "Electronics" */
|
||||
name: string;
|
||||
/** @example "electronics" */
|
||||
slug: string;
|
||||
/** @example 4 */
|
||||
productCount: number;
|
||||
}
|
||||
|
||||
export interface CategoriesResponseDto {
|
||||
data: SimpleCategoryDto[];
|
||||
}
|
||||
|
||||
export interface CategoryResponseDto {
|
||||
data: SimpleCategoryDto;
|
||||
}
|
||||
|
||||
export interface SimpleOrderItemDto {
|
||||
/** @example "product-keyboard" */
|
||||
productId: string;
|
||||
/** @example "Mechanical Keyboard" */
|
||||
productName: string;
|
||||
/** @example 1 */
|
||||
quantity: number;
|
||||
/** @example 12990 */
|
||||
unitPriceCents: number;
|
||||
}
|
||||
|
||||
export interface SimpleOrderDto {
|
||||
/** @example "order-001" */
|
||||
id: string;
|
||||
/** @example "user-customer" */
|
||||
userId: string;
|
||||
status: SimpleOrderDtoStatusEnum;
|
||||
items: SimpleOrderItemDto[];
|
||||
/** @example 17980 */
|
||||
totalCents: number;
|
||||
/** @example "USD" */
|
||||
currency: SimpleOrderDtoCurrencyEnum;
|
||||
/** @format date-time */
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface OrdersResponseDto {
|
||||
data: SimpleOrderDto[];
|
||||
meta: PageMetaDto;
|
||||
}
|
||||
|
||||
export interface OrderResponseDto {
|
||||
data: SimpleOrderDto;
|
||||
}
|
||||
|
||||
export interface CreateOrderItemDto {
|
||||
/** @example "product-keyboard" */
|
||||
productId: string;
|
||||
/**
|
||||
* @min 1
|
||||
* @max 20
|
||||
* @example 1
|
||||
*/
|
||||
quantity: number;
|
||||
/**
|
||||
* @min 1
|
||||
* @example 1
|
||||
*/
|
||||
expectedVersion: number;
|
||||
/**
|
||||
* @min 0
|
||||
* @example 12990
|
||||
*/
|
||||
expectedUnitPriceCents: number;
|
||||
}
|
||||
|
||||
export interface CreateOrderDto {
|
||||
items: CreateOrderItemDto[];
|
||||
}
|
||||
|
||||
export interface ScenarioDto {
|
||||
/** @example "slow" */
|
||||
name: string;
|
||||
/** @example "Delays the response to exercise loading and cancellation states." */
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface ScenariosResponseDto {
|
||||
data: ScenarioDto[];
|
||||
}
|
||||
|
||||
export interface TestingActionDataDto {
|
||||
/** @example true */
|
||||
success: boolean;
|
||||
/** @example "State reset to the default deterministic seed." */
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface TestingActionResponseDto {
|
||||
data: TestingActionDataDto;
|
||||
}
|
||||
|
||||
export interface ChangeSimpleRoleDto {
|
||||
/** @example "customer" */
|
||||
role: ChangeSimpleRoleDtoRoleEnum;
|
||||
}
|
||||
|
||||
/** @example "simple" */
|
||||
export type HealthDataDtoApplicationEnum = "simple" | "complex";
|
||||
|
||||
/** @example "ok" */
|
||||
export type HealthDataDtoStatusEnum = "ok";
|
||||
|
||||
/** @example "Bearer" */
|
||||
export type JwtTokensDtoTokenTypeEnum = "Bearer";
|
||||
|
||||
export type SimpleUserDtoRoleEnum = "admin" | "customer";
|
||||
|
||||
/** @example "USD" */
|
||||
export type SimpleProductDtoCurrencyEnum = "USD" | "EUR";
|
||||
|
||||
/** @example "USD" */
|
||||
export type CreateProductDtoCurrencyEnum = "USD" | "EUR";
|
||||
|
||||
/** @example "USD" */
|
||||
export type UpdateProductDtoCurrencyEnum = "USD" | "EUR";
|
||||
|
||||
export type SimpleOrderDtoStatusEnum =
|
||||
| "pending"
|
||||
| "paid"
|
||||
| "shipped"
|
||||
| "cancelled";
|
||||
|
||||
/** @example "USD" */
|
||||
export type SimpleOrderDtoCurrencyEnum = "USD" | "EUR";
|
||||
|
||||
/** @example "customer" */
|
||||
export type ChangeSimpleRoleDtoRoleEnum = "admin" | "customer";
|
||||
|
||||
/** Forces a deterministic frontend-testing scenario for this request. */
|
||||
export type SimpleHealthHealthParamsXDemoScenarioEnum =
|
||||
| "normal"
|
||||
| "slow"
|
||||
| "timeout"
|
||||
| "server-error"
|
||||
| "rate-limited"
|
||||
| "empty"
|
||||
| "expired-auth"
|
||||
| "forbidden"
|
||||
| "conflict"
|
||||
| "large-dataset";
|
||||
|
||||
/** Forces a deterministic frontend-testing scenario for this request. */
|
||||
export type SimpleAuthLoginParamsXDemoScenarioEnum =
|
||||
| "normal"
|
||||
| "slow"
|
||||
| "timeout"
|
||||
| "server-error"
|
||||
| "rate-limited"
|
||||
| "empty"
|
||||
| "expired-auth"
|
||||
| "forbidden"
|
||||
| "conflict"
|
||||
| "large-dataset";
|
||||
|
||||
/** Forces a deterministic frontend-testing scenario for this request. */
|
||||
export type SimpleAuthRefreshParamsXDemoScenarioEnum =
|
||||
| "normal"
|
||||
| "slow"
|
||||
| "timeout"
|
||||
| "server-error"
|
||||
| "rate-limited"
|
||||
| "empty"
|
||||
| "expired-auth"
|
||||
| "forbidden"
|
||||
| "conflict"
|
||||
| "large-dataset";
|
||||
|
||||
/** Forces a deterministic frontend-testing scenario for this request. */
|
||||
export type SimpleAuthLogoutParamsXDemoScenarioEnum =
|
||||
| "normal"
|
||||
| "slow"
|
||||
| "timeout"
|
||||
| "server-error"
|
||||
| "rate-limited"
|
||||
| "empty"
|
||||
| "expired-auth"
|
||||
| "forbidden"
|
||||
| "conflict"
|
||||
| "large-dataset";
|
||||
|
||||
/** Forces a deterministic frontend-testing scenario for this request. */
|
||||
export type SimpleUsersMeParamsXDemoScenarioEnum =
|
||||
| "normal"
|
||||
| "slow"
|
||||
| "timeout"
|
||||
| "server-error"
|
||||
| "rate-limited"
|
||||
| "empty"
|
||||
| "expired-auth"
|
||||
| "forbidden"
|
||||
| "conflict"
|
||||
| "large-dataset";
|
||||
|
||||
export interface SimpleProductsListParams {
|
||||
/**
|
||||
* @min 1
|
||||
* @default 1
|
||||
*/
|
||||
page?: Object;
|
||||
/**
|
||||
* @min 1
|
||||
* @max 100
|
||||
* @default 20
|
||||
*/
|
||||
limit?: Object;
|
||||
/** @example "keyboard" */
|
||||
search?: string;
|
||||
/** @example "category-electronics" */
|
||||
categoryId?: string;
|
||||
/** @default "newest" */
|
||||
sort?: SortEnum;
|
||||
}
|
||||
|
||||
/** @default "newest" */
|
||||
export type SortEnum = "newest" | "price-asc" | "price-desc" | "name";
|
||||
|
||||
/** @default "newest" */
|
||||
export type SimpleProductsListParams1SortEnum =
|
||||
| "newest"
|
||||
| "price-asc"
|
||||
| "price-desc"
|
||||
| "name";
|
||||
|
||||
/** Forces a deterministic frontend-testing scenario for this request. */
|
||||
export type SimpleProductsListParams1XDemoScenarioEnum =
|
||||
| "normal"
|
||||
| "slow"
|
||||
| "timeout"
|
||||
| "server-error"
|
||||
| "rate-limited"
|
||||
| "empty"
|
||||
| "expired-auth"
|
||||
| "forbidden"
|
||||
| "conflict"
|
||||
| "large-dataset";
|
||||
|
||||
/** Forces a deterministic frontend-testing scenario for this request. */
|
||||
export type SimpleProductsCreateParamsXDemoScenarioEnum =
|
||||
| "normal"
|
||||
| "slow"
|
||||
| "timeout"
|
||||
| "server-error"
|
||||
| "rate-limited"
|
||||
| "empty"
|
||||
| "expired-auth"
|
||||
| "forbidden"
|
||||
| "conflict"
|
||||
| "large-dataset";
|
||||
|
||||
export interface SimpleProductsGetParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
/** Forces a deterministic frontend-testing scenario for this request. */
|
||||
export type SimpleProductsGetParams1XDemoScenarioEnum =
|
||||
| "normal"
|
||||
| "slow"
|
||||
| "timeout"
|
||||
| "server-error"
|
||||
| "rate-limited"
|
||||
| "empty"
|
||||
| "expired-auth"
|
||||
| "forbidden"
|
||||
| "conflict"
|
||||
| "large-dataset";
|
||||
|
||||
export interface SimpleProductsUpdateParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
/** Forces a deterministic frontend-testing scenario for this request. */
|
||||
export type SimpleProductsUpdateParams1XDemoScenarioEnum =
|
||||
| "normal"
|
||||
| "slow"
|
||||
| "timeout"
|
||||
| "server-error"
|
||||
| "rate-limited"
|
||||
| "empty"
|
||||
| "expired-auth"
|
||||
| "forbidden"
|
||||
| "conflict"
|
||||
| "large-dataset";
|
||||
|
||||
export interface SimpleProductsRemoveParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
/** Forces a deterministic frontend-testing scenario for this request. */
|
||||
export type SimpleProductsRemoveParams1XDemoScenarioEnum =
|
||||
| "normal"
|
||||
| "slow"
|
||||
| "timeout"
|
||||
| "server-error"
|
||||
| "rate-limited"
|
||||
| "empty"
|
||||
| "expired-auth"
|
||||
| "forbidden"
|
||||
| "conflict"
|
||||
| "large-dataset";
|
||||
|
||||
/** Forces a deterministic frontend-testing scenario for this request. */
|
||||
export type SimpleCategoriesListParamsXDemoScenarioEnum =
|
||||
| "normal"
|
||||
| "slow"
|
||||
| "timeout"
|
||||
| "server-error"
|
||||
| "rate-limited"
|
||||
| "empty"
|
||||
| "expired-auth"
|
||||
| "forbidden"
|
||||
| "conflict"
|
||||
| "large-dataset";
|
||||
|
||||
export interface SimpleCategoriesGetParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
/** Forces a deterministic frontend-testing scenario for this request. */
|
||||
export type SimpleCategoriesGetParams1XDemoScenarioEnum =
|
||||
| "normal"
|
||||
| "slow"
|
||||
| "timeout"
|
||||
| "server-error"
|
||||
| "rate-limited"
|
||||
| "empty"
|
||||
| "expired-auth"
|
||||
| "forbidden"
|
||||
| "conflict"
|
||||
| "large-dataset";
|
||||
|
||||
export interface SimpleOrdersListParams {
|
||||
/**
|
||||
* @min 1
|
||||
* @default 1
|
||||
*/
|
||||
page?: Object;
|
||||
/**
|
||||
* @min 1
|
||||
* @max 100
|
||||
* @default 20
|
||||
*/
|
||||
limit?: Object;
|
||||
}
|
||||
|
||||
/** Forces a deterministic frontend-testing scenario for this request. */
|
||||
export type SimpleOrdersListParams1XDemoScenarioEnum =
|
||||
| "normal"
|
||||
| "slow"
|
||||
| "timeout"
|
||||
| "server-error"
|
||||
| "rate-limited"
|
||||
| "empty"
|
||||
| "expired-auth"
|
||||
| "forbidden"
|
||||
| "conflict"
|
||||
| "large-dataset";
|
||||
|
||||
/** Forces a deterministic frontend-testing scenario for this request. */
|
||||
export type SimpleOrdersCreateParamsXDemoScenarioEnum =
|
||||
| "normal"
|
||||
| "slow"
|
||||
| "timeout"
|
||||
| "server-error"
|
||||
| "rate-limited"
|
||||
| "empty"
|
||||
| "expired-auth"
|
||||
| "forbidden"
|
||||
| "conflict"
|
||||
| "large-dataset";
|
||||
|
||||
export interface SimpleOrdersGetParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
/** Forces a deterministic frontend-testing scenario for this request. */
|
||||
export type SimpleOrdersGetParams1XDemoScenarioEnum =
|
||||
| "normal"
|
||||
| "slow"
|
||||
| "timeout"
|
||||
| "server-error"
|
||||
| "rate-limited"
|
||||
| "empty"
|
||||
| "expired-auth"
|
||||
| "forbidden"
|
||||
| "conflict"
|
||||
| "large-dataset";
|
||||
|
||||
export interface SimpleOrdersCancelParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
/** Forces a deterministic frontend-testing scenario for this request. */
|
||||
export type SimpleOrdersCancelParams1XDemoScenarioEnum =
|
||||
| "normal"
|
||||
| "slow"
|
||||
| "timeout"
|
||||
| "server-error"
|
||||
| "rate-limited"
|
||||
| "empty"
|
||||
| "expired-auth"
|
||||
| "forbidden"
|
||||
| "conflict"
|
||||
| "large-dataset";
|
||||
|
||||
/** Forces a deterministic frontend-testing scenario for this request. */
|
||||
export type SimpleTestingScenariosParamsXDemoScenarioEnum =
|
||||
| "normal"
|
||||
| "slow"
|
||||
| "timeout"
|
||||
| "server-error"
|
||||
| "rate-limited"
|
||||
| "empty"
|
||||
| "expired-auth"
|
||||
| "forbidden"
|
||||
| "conflict"
|
||||
| "large-dataset";
|
||||
|
||||
/** Forces a deterministic frontend-testing scenario for this request. */
|
||||
export type SimpleTestingResetParamsXDemoScenarioEnum =
|
||||
| "normal"
|
||||
| "slow"
|
||||
| "timeout"
|
||||
| "server-error"
|
||||
| "rate-limited"
|
||||
| "empty"
|
||||
| "expired-auth"
|
||||
| "forbidden"
|
||||
| "conflict"
|
||||
| "large-dataset";
|
||||
|
||||
export interface SimpleTestingSeedParams {
|
||||
preset: PresetEnum;
|
||||
}
|
||||
|
||||
export type PresetEnum = "small" | "large";
|
||||
|
||||
export type SimpleTestingSeedParams1PresetEnum = "small" | "large";
|
||||
|
||||
/** Forces a deterministic frontend-testing scenario for this request. */
|
||||
export type SimpleTestingSeedParams1XDemoScenarioEnum =
|
||||
| "normal"
|
||||
| "slow"
|
||||
| "timeout"
|
||||
| "server-error"
|
||||
| "rate-limited"
|
||||
| "empty"
|
||||
| "expired-auth"
|
||||
| "forbidden"
|
||||
| "conflict"
|
||||
| "large-dataset";
|
||||
|
||||
export type SimpleTestingSeedParams1Enum = "small" | "large";
|
||||
|
||||
export interface SimpleTestingChangeRoleParams {
|
||||
userId: string;
|
||||
}
|
||||
|
||||
/** Forces a deterministic frontend-testing scenario for this request. */
|
||||
export type SimpleTestingChangeRoleParams1XDemoScenarioEnum =
|
||||
| "normal"
|
||||
| "slow"
|
||||
| "timeout"
|
||||
| "server-error"
|
||||
| "rate-limited"
|
||||
| "empty"
|
||||
| "expired-auth"
|
||||
| "forbidden"
|
||||
| "conflict"
|
||||
| "large-dataset";
|
||||
@@ -0,0 +1,543 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
|
||||
/*
|
||||
* ----------------------------------------------------------------------
|
||||
* ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
|
||||
* ## ##
|
||||
* ## Не редактируйте вручную: изменения будут перезаписаны. ##
|
||||
* ## Для изменений перегенерируйте клиент. ##
|
||||
* ## ##
|
||||
* ## Генератор: @gromlab/api-codegen ##
|
||||
* ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
|
||||
* ----------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
export type QueryParamsType = Record<string | number, any>;
|
||||
export type ResponseFormat = keyof Omit<Body, "body" | "bodyUsed">;
|
||||
|
||||
export interface FullRequestParams extends Omit<RequestInit, "body"> {
|
||||
/** set parameter to `true` to mark this request as protected */
|
||||
secure?: boolean;
|
||||
/** request path */
|
||||
path: string;
|
||||
/** content type of request body */
|
||||
type?: ContentType;
|
||||
/** query params */
|
||||
query?: QueryParamsType;
|
||||
/** format of response (i.e. response.json() -> format: "json") */
|
||||
format?: ResponseFormat;
|
||||
/** request body */
|
||||
body?: unknown;
|
||||
/** base url */
|
||||
baseUrl?: string;
|
||||
/** request cancellation token */
|
||||
cancelToken?: CancelToken;
|
||||
/** request timeout in milliseconds */
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
export type RequestParams = Omit<
|
||||
FullRequestParams,
|
||||
"body" | "method" | "query" | "path"
|
||||
>;
|
||||
|
||||
export interface RequestContext<TResult = unknown> {
|
||||
url: string;
|
||||
request: FullRequestParams;
|
||||
retryCount: number;
|
||||
retry: () => Promise<TResult>;
|
||||
}
|
||||
|
||||
export type RequestInterceptor = (
|
||||
params: FullRequestParams,
|
||||
context: RequestContext,
|
||||
) => FullRequestParams | Promise<FullRequestParams>;
|
||||
|
||||
export type ResponseInterceptor = <D = unknown, E = unknown>(
|
||||
response: HttpResponse<D, E>,
|
||||
context: RequestContext,
|
||||
) => HttpResponse<D, E> | Promise<HttpResponse<D, E>>;
|
||||
|
||||
export type ErrorInterceptor = <TResult = unknown>(
|
||||
error: unknown,
|
||||
context: RequestContext<TResult>,
|
||||
) => TResult | Promise<TResult>;
|
||||
|
||||
export type ParamsSerializer = (query: QueryParamsType) => string;
|
||||
export type ResponseParser = (
|
||||
response: Response,
|
||||
format?: ResponseFormat,
|
||||
) => unknown | Promise<unknown>;
|
||||
|
||||
export interface ApiRequestClient {
|
||||
request<T = any, E = any>(params: FullRequestParams): Promise<T>;
|
||||
}
|
||||
|
||||
export interface ApiConfig<SecurityDataType = unknown>
|
||||
extends Omit<RequestParams, "baseUrl" | "cancelToken" | "signal"> {
|
||||
baseUrl?: string;
|
||||
customFetch?: typeof fetch;
|
||||
paramsSerializer?: ParamsSerializer;
|
||||
responseParser?: ResponseParser;
|
||||
onRequest?: RequestInterceptor;
|
||||
onResponse?: ResponseInterceptor;
|
||||
onError?: ErrorInterceptor;
|
||||
}
|
||||
|
||||
export interface HttpResponse<D extends unknown, E extends unknown = unknown>
|
||||
extends Response {
|
||||
data: D;
|
||||
error: E;
|
||||
}
|
||||
|
||||
export class ApiError<E = unknown> extends Error {
|
||||
public readonly status: number;
|
||||
public readonly statusText: string;
|
||||
public readonly response: Response;
|
||||
public readonly data: unknown;
|
||||
public readonly error: E;
|
||||
public readonly request: FullRequestParams;
|
||||
|
||||
constructor(
|
||||
response: Response,
|
||||
request: FullRequestParams,
|
||||
data: unknown,
|
||||
error: E,
|
||||
) {
|
||||
super(
|
||||
`Request failed with status ${response.status} ${response.statusText}`.trim(),
|
||||
);
|
||||
this.name = "ApiError";
|
||||
this.status = response.status;
|
||||
this.statusText = response.statusText;
|
||||
this.response = response;
|
||||
this.data = data;
|
||||
this.error = error;
|
||||
this.request = request;
|
||||
}
|
||||
}
|
||||
|
||||
export type CancelToken = Symbol | string | number;
|
||||
|
||||
export enum ContentType {
|
||||
Json = "application/json",
|
||||
JsonApi = "application/vnd.api+json",
|
||||
FormData = "multipart/form-data",
|
||||
UrlEncoded = "application/x-www-form-urlencoded",
|
||||
Text = "text/plain",
|
||||
}
|
||||
|
||||
export class HttpClient<SecurityDataType = unknown>
|
||||
implements ApiRequestClient
|
||||
{
|
||||
public baseUrl: string = "http://localhost:3001";
|
||||
private abortControllers = new Map<CancelToken, AbortController>();
|
||||
private customFetch: typeof fetch = (...fetchParams) => fetch(...fetchParams);
|
||||
private paramsSerializer?: ParamsSerializer;
|
||||
private responseParser?: ResponseParser;
|
||||
private onRequest?: RequestInterceptor;
|
||||
private onResponse?: ResponseInterceptor;
|
||||
private onError?: ErrorInterceptor;
|
||||
|
||||
private baseRequestParams: RequestParams = {
|
||||
credentials: "same-origin",
|
||||
headers: {},
|
||||
redirect: "follow",
|
||||
referrerPolicy: "no-referrer",
|
||||
};
|
||||
|
||||
constructor({
|
||||
baseUrl,
|
||||
customFetch,
|
||||
paramsSerializer,
|
||||
responseParser,
|
||||
onRequest,
|
||||
onResponse,
|
||||
onError,
|
||||
...baseRequestParams
|
||||
}: ApiConfig<SecurityDataType> = {}) {
|
||||
if (typeof baseUrl === "string") {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
this.customFetch = customFetch || this.customFetch;
|
||||
this.paramsSerializer = paramsSerializer;
|
||||
this.responseParser = responseParser;
|
||||
this.onRequest = onRequest;
|
||||
this.onResponse = onResponse;
|
||||
this.onError = onError;
|
||||
this.baseRequestParams = this.mergeRequestParams(
|
||||
this.baseRequestParams,
|
||||
baseRequestParams,
|
||||
);
|
||||
}
|
||||
|
||||
protected encodeQueryParam(key: string, value: any) {
|
||||
const encodedKey = encodeURIComponent(key);
|
||||
return `${encodedKey}=${encodeURIComponent(typeof value === "number" ? value : `${value}`)}`;
|
||||
}
|
||||
|
||||
protected addQueryParam(query: QueryParamsType, key: string) {
|
||||
return this.encodeQueryParam(key, query[key]);
|
||||
}
|
||||
|
||||
protected addArrayQueryParam(query: QueryParamsType, key: string) {
|
||||
const value = query[key];
|
||||
return value.map((v: any) => this.encodeQueryParam(key, v)).join("&");
|
||||
}
|
||||
|
||||
protected toQueryString(rawQuery?: QueryParamsType): string {
|
||||
const query = rawQuery || {};
|
||||
|
||||
if (this.paramsSerializer) {
|
||||
return this.paramsSerializer(query);
|
||||
}
|
||||
|
||||
const keys = Object.keys(query).filter(
|
||||
(key) => "undefined" !== typeof query[key],
|
||||
);
|
||||
return keys
|
||||
.map((key) =>
|
||||
Array.isArray(query[key])
|
||||
? this.addArrayQueryParam(query, key)
|
||||
: this.addQueryParam(query, key),
|
||||
)
|
||||
.join("&");
|
||||
}
|
||||
|
||||
protected addQueryParams(rawQuery?: QueryParamsType): string {
|
||||
const queryString = this.toQueryString(rawQuery);
|
||||
return queryString ? `?${queryString}` : "";
|
||||
}
|
||||
|
||||
protected buildRequestUrl(
|
||||
baseUrl: string | undefined,
|
||||
path: string,
|
||||
query?: QueryParamsType,
|
||||
): string {
|
||||
return `${baseUrl || this.baseUrl || ""}${path}${this.addQueryParams(query)}`;
|
||||
}
|
||||
|
||||
protected createRequestContext<TResult>(
|
||||
request: FullRequestParams,
|
||||
retryCount: number,
|
||||
retry: () => Promise<TResult>,
|
||||
): RequestContext<TResult> {
|
||||
return {
|
||||
url: this.buildRequestUrl(request.baseUrl, request.path, request.query),
|
||||
request,
|
||||
retryCount,
|
||||
retry,
|
||||
};
|
||||
}
|
||||
|
||||
protected updateRequestContext<TResult>(
|
||||
context: RequestContext<TResult>,
|
||||
request: FullRequestParams,
|
||||
) {
|
||||
context.request = request;
|
||||
context.url = this.buildRequestUrl(
|
||||
request.baseUrl,
|
||||
request.path,
|
||||
request.query,
|
||||
);
|
||||
}
|
||||
|
||||
protected mergeHeaders(
|
||||
...headers: Array<HeadersInit | undefined>
|
||||
): HeadersInit {
|
||||
const mergedHeaders = new Headers();
|
||||
|
||||
headers.forEach((headers) => {
|
||||
if (!headers) {
|
||||
return;
|
||||
}
|
||||
|
||||
new Headers(headers).forEach((value, key) =>
|
||||
mergedHeaders.set(key, value),
|
||||
);
|
||||
});
|
||||
|
||||
return Object.fromEntries(mergedHeaders.entries());
|
||||
}
|
||||
|
||||
protected mergeRequestParams<T extends Partial<FullRequestParams>>(
|
||||
params1: T,
|
||||
params2?: Partial<FullRequestParams>,
|
||||
): T {
|
||||
return {
|
||||
...params1,
|
||||
...(params2 || {}),
|
||||
headers: this.mergeHeaders(params1.headers, params2?.headers),
|
||||
} as T;
|
||||
}
|
||||
|
||||
protected createAbortSignal = (
|
||||
cancelToken: CancelToken,
|
||||
): AbortSignal | undefined => {
|
||||
if (this.abortControllers.has(cancelToken)) {
|
||||
const abortController = this.abortControllers.get(cancelToken);
|
||||
if (abortController) {
|
||||
return abortController.signal;
|
||||
}
|
||||
return void 0;
|
||||
}
|
||||
|
||||
const abortController = new AbortController();
|
||||
this.abortControllers.set(cancelToken, abortController);
|
||||
return abortController.signal;
|
||||
};
|
||||
|
||||
protected createRequestSignal = (
|
||||
signal?: AbortSignal | null,
|
||||
cancelToken?: CancelToken,
|
||||
timeout?: number,
|
||||
): { signal: AbortSignal | null; cleanup: () => void } => {
|
||||
const signals: AbortSignal[] = [];
|
||||
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
if (signal) {
|
||||
signals.push(signal);
|
||||
}
|
||||
|
||||
if (cancelToken) {
|
||||
const cancelSignal = this.createAbortSignal(cancelToken);
|
||||
if (cancelSignal) {
|
||||
signals.push(cancelSignal);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof timeout === "number" && timeout > 0) {
|
||||
const timeoutController = new AbortController();
|
||||
timeoutId = setTimeout(() => timeoutController.abort(), timeout);
|
||||
signals.push(timeoutController.signal);
|
||||
}
|
||||
|
||||
const cleanupTimeout = () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
};
|
||||
|
||||
if (signals.length === 0) {
|
||||
return { signal: null, cleanup: cleanupTimeout };
|
||||
}
|
||||
|
||||
if (signals.length === 1) {
|
||||
return { signal: signals[0] || null, cleanup: cleanupTimeout };
|
||||
}
|
||||
|
||||
const abortController = new AbortController();
|
||||
const abortRequest = () => abortController.abort();
|
||||
|
||||
signals.forEach((signal) => {
|
||||
if (signal.aborted) {
|
||||
abortController.abort();
|
||||
} else {
|
||||
signal.addEventListener("abort", abortRequest, { once: true });
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
signal: abortController.signal,
|
||||
cleanup: () => {
|
||||
cleanupTimeout();
|
||||
signals.forEach((signal) =>
|
||||
signal.removeEventListener("abort", abortRequest),
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
public abortRequest = (cancelToken: CancelToken) => {
|
||||
const abortController = this.abortControllers.get(cancelToken);
|
||||
|
||||
if (abortController) {
|
||||
abortController.abort();
|
||||
this.abortControllers.delete(cancelToken);
|
||||
}
|
||||
};
|
||||
|
||||
private contentFormatters: Record<ContentType, (input: any) => any> = {
|
||||
[ContentType.Json]: (input: any) =>
|
||||
input !== null && (typeof input === "object" || typeof input === "string")
|
||||
? JSON.stringify(input)
|
||||
: input,
|
||||
[ContentType.JsonApi]: (input: any) =>
|
||||
input !== null && (typeof input === "object" || typeof input === "string")
|
||||
? JSON.stringify(input)
|
||||
: input,
|
||||
[ContentType.Text]: (input: any) =>
|
||||
input !== null && typeof input !== "string"
|
||||
? JSON.stringify(input)
|
||||
: input,
|
||||
[ContentType.FormData]: (input: any) => {
|
||||
if (input instanceof FormData) {
|
||||
return input;
|
||||
}
|
||||
|
||||
return Object.keys(input || {}).reduce((formData, key) => {
|
||||
const property = input[key];
|
||||
formData.append(
|
||||
key,
|
||||
property instanceof Blob
|
||||
? property
|
||||
: typeof property === "object" && property !== null
|
||||
? JSON.stringify(property)
|
||||
: `${property}`,
|
||||
);
|
||||
return formData;
|
||||
}, new FormData());
|
||||
},
|
||||
[ContentType.UrlEncoded]: (input: any) => this.toQueryString(input),
|
||||
};
|
||||
|
||||
protected parseResponse = async <T = any, E = any>(
|
||||
response: Response,
|
||||
responseFormat?: ResponseFormat,
|
||||
): Promise<HttpResponse<T, E>> => {
|
||||
const parsedResponse = response as HttpResponse<T, E>;
|
||||
parsedResponse.data = null as unknown as T;
|
||||
parsedResponse.error = null as unknown as E;
|
||||
|
||||
if (!responseFormat && !this.responseParser) {
|
||||
return parsedResponse;
|
||||
}
|
||||
|
||||
const responseToParse = response.clone();
|
||||
|
||||
await Promise.resolve(
|
||||
this.responseParser
|
||||
? this.responseParser(responseToParse, responseFormat)
|
||||
: responseToParse[responseFormat as ResponseFormat](),
|
||||
)
|
||||
.then((data) => {
|
||||
if (parsedResponse.ok) {
|
||||
parsedResponse.data = data as T;
|
||||
} else {
|
||||
parsedResponse.error = data as E;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if (parsedResponse.ok) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
parsedResponse.error = error as E;
|
||||
});
|
||||
|
||||
return parsedResponse;
|
||||
};
|
||||
|
||||
public request = async <T = any, E = any>(
|
||||
requestParams: FullRequestParams,
|
||||
) => {
|
||||
return this.requestWithRetry<T, E>(requestParams, 0);
|
||||
};
|
||||
|
||||
private requestWithRetry = async <T = any, E = any>(
|
||||
requestParams: FullRequestParams,
|
||||
retryCount: number,
|
||||
): Promise<T> => {
|
||||
let request = this.mergeRequestParams(
|
||||
this.baseRequestParams,
|
||||
requestParams,
|
||||
) as FullRequestParams;
|
||||
request.baseUrl = request.baseUrl || this.baseUrl;
|
||||
request.secure =
|
||||
typeof request.secure === "boolean"
|
||||
? request.secure
|
||||
: this.baseRequestParams.secure;
|
||||
|
||||
const context = this.createRequestContext<T>(request, retryCount, () =>
|
||||
this.requestWithRetry<T, E>(requestParams, retryCount + 1),
|
||||
);
|
||||
|
||||
let cleanupSignal = () => {};
|
||||
let cancelToken: CancelToken | undefined;
|
||||
|
||||
const cleanupRequest = () => {
|
||||
cleanupSignal();
|
||||
if (cancelToken) {
|
||||
this.abortControllers.delete(cancelToken);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
if (this.onRequest) {
|
||||
request = await this.onRequest(request, context);
|
||||
this.updateRequestContext(context, request);
|
||||
}
|
||||
|
||||
const {
|
||||
body,
|
||||
secure,
|
||||
path,
|
||||
type,
|
||||
query,
|
||||
format,
|
||||
baseUrl,
|
||||
cancelToken: requestCancelToken,
|
||||
timeout,
|
||||
...params
|
||||
} = request;
|
||||
|
||||
cancelToken = requestCancelToken;
|
||||
const { signal, cleanup } = this.createRequestSignal(
|
||||
params.signal,
|
||||
cancelToken,
|
||||
timeout,
|
||||
);
|
||||
cleanupSignal = cleanup;
|
||||
|
||||
const payloadFormatter = this.contentFormatters[type || ContentType.Json];
|
||||
const responseFormat = format;
|
||||
const response = await this.customFetch(context.url, {
|
||||
...params,
|
||||
headers: this.mergeHeaders(
|
||||
params.headers,
|
||||
type && type !== ContentType.FormData
|
||||
? { "Content-Type": type }
|
||||
: undefined,
|
||||
),
|
||||
signal,
|
||||
body:
|
||||
typeof body === "undefined" || body === null
|
||||
? null
|
||||
: payloadFormatter(body),
|
||||
});
|
||||
|
||||
const parsedResponse = await this.parseResponse<T, E>(
|
||||
response,
|
||||
responseFormat,
|
||||
);
|
||||
|
||||
if (!parsedResponse.ok) {
|
||||
throw new ApiError<E>(
|
||||
parsedResponse,
|
||||
request,
|
||||
parsedResponse.error || parsedResponse.data,
|
||||
parsedResponse.error,
|
||||
);
|
||||
}
|
||||
|
||||
const finalResponse = this.onResponse
|
||||
? await this.onResponse<T, E>(parsedResponse, context)
|
||||
: parsedResponse;
|
||||
|
||||
return finalResponse.data;
|
||||
} catch (error) {
|
||||
cleanupRequest();
|
||||
|
||||
if (this.onError) {
|
||||
return this.onError(error, context);
|
||||
}
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
cleanupRequest();
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
|
||||
/*
|
||||
* ----------------------------------------------------------------------
|
||||
* ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
|
||||
* ## ##
|
||||
* ## Не редактируйте вручную: изменения будут перезаписаны. ##
|
||||
* ## Для изменений перегенерируйте клиент. ##
|
||||
* ## ##
|
||||
* ## Генератор: @gromlab/api-codegen ##
|
||||
* ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
|
||||
* ----------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
export { createApiClient } from "./create-api-client";
|
||||
export type { ApiOperation, ApiTree, BoundApi } from "./create-api-client";
|
||||
export type * from "./data-contracts";
|
||||
export { ApiError, ContentType, HttpClient } from "./http-client";
|
||||
export type {
|
||||
ApiConfig,
|
||||
ApiRequestClient,
|
||||
ErrorInterceptor,
|
||||
FullRequestParams,
|
||||
HttpResponse,
|
||||
ParamsSerializer,
|
||||
QueryParamsType,
|
||||
RequestContext,
|
||||
RequestInterceptor,
|
||||
RequestParams,
|
||||
ResponseFormat,
|
||||
ResponseInterceptor,
|
||||
ResponseParser,
|
||||
} from "./http-client";
|
||||
export * from "./operations";
|
||||
export * as operations from "./operations";
|
||||
export { operationsTree } from "./operations-tree";
|
||||
export type { OperationsTree } from "./operations-tree";
|
||||
@@ -0,0 +1,75 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
|
||||
/*
|
||||
* ----------------------------------------------------------------------
|
||||
* ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
|
||||
* ## ##
|
||||
* ## Не редактируйте вручную: изменения будут перезаписаны. ##
|
||||
* ## Для изменений перегенерируйте клиент. ##
|
||||
* ## ##
|
||||
* ## Генератор: @gromlab/api-codegen ##
|
||||
* ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
|
||||
* ----------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import { simpleAuthLogin } from "./operations/simple-auth-login";
|
||||
import { simpleAuthLogout } from "./operations/simple-auth-logout";
|
||||
import { simpleAuthRefresh } from "./operations/simple-auth-refresh";
|
||||
import { simpleCategoriesGet } from "./operations/simple-categories-get";
|
||||
import { simpleCategoriesList } from "./operations/simple-categories-list";
|
||||
import { simpleHealthHealth } from "./operations/simple-health-health";
|
||||
import { simpleOrdersCancel } from "./operations/simple-orders-cancel";
|
||||
import { simpleOrdersCreate } from "./operations/simple-orders-create";
|
||||
import { simpleOrdersGet } from "./operations/simple-orders-get";
|
||||
import { simpleOrdersList } from "./operations/simple-orders-list";
|
||||
import { simpleProductsCreate } from "./operations/simple-products-create";
|
||||
import { simpleProductsGet } from "./operations/simple-products-get";
|
||||
import { simpleProductsList } from "./operations/simple-products-list";
|
||||
import { simpleProductsRemove } from "./operations/simple-products-remove";
|
||||
import { simpleProductsUpdate } from "./operations/simple-products-update";
|
||||
import { simpleTestingChangeRole } from "./operations/simple-testing-change-role";
|
||||
import { simpleTestingReset } from "./operations/simple-testing-reset";
|
||||
import { simpleTestingScenarios } from "./operations/simple-testing-scenarios";
|
||||
import { simpleTestingSeed } from "./operations/simple-testing-seed";
|
||||
import { simpleUsersMe } from "./operations/simple-users-me";
|
||||
|
||||
export const operationsTree = {
|
||||
health: {
|
||||
simpleHealthHealth: simpleHealthHealth,
|
||||
},
|
||||
auth: {
|
||||
simpleAuthLogin: simpleAuthLogin,
|
||||
simpleAuthRefresh: simpleAuthRefresh,
|
||||
simpleAuthLogout: simpleAuthLogout,
|
||||
},
|
||||
users: {
|
||||
simpleUsersMe: simpleUsersMe,
|
||||
},
|
||||
products: {
|
||||
simpleProductsList: simpleProductsList,
|
||||
simpleProductsCreate: simpleProductsCreate,
|
||||
simpleProductsGet: simpleProductsGet,
|
||||
simpleProductsUpdate: simpleProductsUpdate,
|
||||
simpleProductsRemove: simpleProductsRemove,
|
||||
},
|
||||
categories: {
|
||||
simpleCategoriesList: simpleCategoriesList,
|
||||
simpleCategoriesGet: simpleCategoriesGet,
|
||||
},
|
||||
orders: {
|
||||
simpleOrdersList: simpleOrdersList,
|
||||
simpleOrdersCreate: simpleOrdersCreate,
|
||||
simpleOrdersGet: simpleOrdersGet,
|
||||
simpleOrdersCancel: simpleOrdersCancel,
|
||||
},
|
||||
testing: {
|
||||
simpleTestingScenarios: simpleTestingScenarios,
|
||||
simpleTestingReset: simpleTestingReset,
|
||||
simpleTestingSeed: simpleTestingSeed,
|
||||
simpleTestingChangeRole: simpleTestingChangeRole,
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type OperationsTree = typeof operationsTree;
|
||||
@@ -0,0 +1,36 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
|
||||
/*
|
||||
* ----------------------------------------------------------------------
|
||||
* ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
|
||||
* ## ##
|
||||
* ## Не редактируйте вручную: изменения будут перезаписаны. ##
|
||||
* ## Для изменений перегенерируйте клиент. ##
|
||||
* ## ##
|
||||
* ## Генератор: @gromlab/api-codegen ##
|
||||
* ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
|
||||
* ----------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
export { simpleAuthLogin } from "./simple-auth-login";
|
||||
export { simpleAuthLogout } from "./simple-auth-logout";
|
||||
export { simpleAuthRefresh } from "./simple-auth-refresh";
|
||||
export { simpleCategoriesGet } from "./simple-categories-get";
|
||||
export { simpleCategoriesList } from "./simple-categories-list";
|
||||
export { simpleHealthHealth } from "./simple-health-health";
|
||||
export { simpleOrdersCancel } from "./simple-orders-cancel";
|
||||
export { simpleOrdersCreate } from "./simple-orders-create";
|
||||
export { simpleOrdersGet } from "./simple-orders-get";
|
||||
export { simpleOrdersList } from "./simple-orders-list";
|
||||
export { simpleProductsCreate } from "./simple-products-create";
|
||||
export { simpleProductsGet } from "./simple-products-get";
|
||||
export { simpleProductsList } from "./simple-products-list";
|
||||
export { simpleProductsRemove } from "./simple-products-remove";
|
||||
export { simpleProductsUpdate } from "./simple-products-update";
|
||||
export { simpleTestingChangeRole } from "./simple-testing-change-role";
|
||||
export { simpleTestingReset } from "./simple-testing-reset";
|
||||
export { simpleTestingScenarios } from "./simple-testing-scenarios";
|
||||
export { simpleTestingSeed } from "./simple-testing-seed";
|
||||
export { simpleUsersMe } from "./simple-users-me";
|
||||
@@ -0,0 +1,45 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
|
||||
/*
|
||||
* ----------------------------------------------------------------------
|
||||
* ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
|
||||
* ## ##
|
||||
* ## Не редактируйте вручную: изменения будут перезаписаны. ##
|
||||
* ## Для изменений перегенерируйте клиент. ##
|
||||
* ## ##
|
||||
* ## Генератор: @gromlab/api-codegen ##
|
||||
* ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
|
||||
* ----------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import type {
|
||||
ErrorResponseDto,
|
||||
JwtAuthResponseDto,
|
||||
LoginDto,
|
||||
} from "../data-contracts";
|
||||
import type { ApiRequestClient, RequestParams } from "../http-client";
|
||||
import { ContentType } from "../http-client";
|
||||
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags Auth
|
||||
* @name SimpleAuthLogin
|
||||
* @summary Login and receive JWT access/refresh tokens
|
||||
* @request POST:/api/v1/auth/login
|
||||
*/
|
||||
export const simpleAuthLogin = (
|
||||
http: ApiRequestClient,
|
||||
data: LoginDto,
|
||||
requestParams: RequestParams = {},
|
||||
) =>
|
||||
http.request<JwtAuthResponseDto, ErrorResponseDto>({
|
||||
path: `/api/v1/auth/login`,
|
||||
method: "POST",
|
||||
body: data,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...requestParams,
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
|
||||
/*
|
||||
* ----------------------------------------------------------------------
|
||||
* ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
|
||||
* ## ##
|
||||
* ## Не редактируйте вручную: изменения будут перезаписаны. ##
|
||||
* ## Для изменений перегенерируйте клиент. ##
|
||||
* ## ##
|
||||
* ## Генератор: @gromlab/api-codegen ##
|
||||
* ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
|
||||
* ----------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import type { ErrorResponseDto, RefreshTokenDto } from "../data-contracts";
|
||||
import type { ApiRequestClient, RequestParams } from "../http-client";
|
||||
import { ContentType } from "../http-client";
|
||||
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags Auth
|
||||
* @name SimpleAuthLogout
|
||||
* @summary Revoke a refresh token; the operation is idempotent
|
||||
* @request POST:/api/v1/auth/logout
|
||||
*/
|
||||
export const simpleAuthLogout = (
|
||||
http: ApiRequestClient,
|
||||
data: RefreshTokenDto,
|
||||
requestParams: RequestParams = {},
|
||||
) =>
|
||||
http.request<void, ErrorResponseDto>({
|
||||
path: `/api/v1/auth/logout`,
|
||||
method: "POST",
|
||||
body: data,
|
||||
type: ContentType.Json,
|
||||
...requestParams,
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
|
||||
/*
|
||||
* ----------------------------------------------------------------------
|
||||
* ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
|
||||
* ## ##
|
||||
* ## Не редактируйте вручную: изменения будут перезаписаны. ##
|
||||
* ## Для изменений перегенерируйте клиент. ##
|
||||
* ## ##
|
||||
* ## Генератор: @gromlab/api-codegen ##
|
||||
* ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
|
||||
* ----------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import type {
|
||||
ErrorResponseDto,
|
||||
JwtAuthResponseDto,
|
||||
RefreshTokenDto,
|
||||
} from "../data-contracts";
|
||||
import type { ApiRequestClient, RequestParams } from "../http-client";
|
||||
import { ContentType } from "../http-client";
|
||||
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags Auth
|
||||
* @name SimpleAuthRefresh
|
||||
* @summary Rotate a refresh token and issue a new token pair
|
||||
* @request POST:/api/v1/auth/refresh
|
||||
*/
|
||||
export const simpleAuthRefresh = (
|
||||
http: ApiRequestClient,
|
||||
data: RefreshTokenDto,
|
||||
requestParams: RequestParams = {},
|
||||
) =>
|
||||
http.request<JwtAuthResponseDto, ErrorResponseDto>({
|
||||
path: `/api/v1/auth/refresh`,
|
||||
method: "POST",
|
||||
body: data,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...requestParams,
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
|
||||
/*
|
||||
* ----------------------------------------------------------------------
|
||||
* ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
|
||||
* ## ##
|
||||
* ## Не редактируйте вручную: изменения будут перезаписаны. ##
|
||||
* ## Для изменений перегенерируйте клиент. ##
|
||||
* ## ##
|
||||
* ## Генератор: @gromlab/api-codegen ##
|
||||
* ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
|
||||
* ----------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import type {
|
||||
CategoryResponseDto,
|
||||
ErrorResponseDto,
|
||||
SimpleCategoriesGetParams,
|
||||
} from "../data-contracts";
|
||||
import type { ApiRequestClient, RequestParams } from "../http-client";
|
||||
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags Categories
|
||||
* @name SimpleCategoriesGet
|
||||
* @summary Get one category
|
||||
* @request GET:/api/v1/categories/{id}
|
||||
*/
|
||||
export const simpleCategoriesGet = (
|
||||
http: ApiRequestClient,
|
||||
{ id, ...query }: SimpleCategoriesGetParams,
|
||||
requestParams: RequestParams = {},
|
||||
) =>
|
||||
http.request<CategoryResponseDto, ErrorResponseDto>({
|
||||
path: `/api/v1/categories/${id}`,
|
||||
method: "GET",
|
||||
format: "json",
|
||||
...requestParams,
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
|
||||
/*
|
||||
* ----------------------------------------------------------------------
|
||||
* ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
|
||||
* ## ##
|
||||
* ## Не редактируйте вручную: изменения будут перезаписаны. ##
|
||||
* ## Для изменений перегенерируйте клиент. ##
|
||||
* ## ##
|
||||
* ## Генератор: @gromlab/api-codegen ##
|
||||
* ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
|
||||
* ----------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import type { CategoriesResponseDto } from "../data-contracts";
|
||||
import type { ApiRequestClient, RequestParams } from "../http-client";
|
||||
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags Categories
|
||||
* @name SimpleCategoriesList
|
||||
* @summary List product categories
|
||||
* @request GET:/api/v1/categories
|
||||
*/
|
||||
export const simpleCategoriesList = (
|
||||
http: ApiRequestClient,
|
||||
requestParams: RequestParams = {},
|
||||
) =>
|
||||
http.request<CategoriesResponseDto, any>({
|
||||
path: `/api/v1/categories`,
|
||||
method: "GET",
|
||||
format: "json",
|
||||
...requestParams,
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
|
||||
/*
|
||||
* ----------------------------------------------------------------------
|
||||
* ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
|
||||
* ## ##
|
||||
* ## Не редактируйте вручную: изменения будут перезаписаны. ##
|
||||
* ## Для изменений перегенерируйте клиент. ##
|
||||
* ## ##
|
||||
* ## Генератор: @gromlab/api-codegen ##
|
||||
* ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
|
||||
* ----------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import type { HealthResponseDto } from "../data-contracts";
|
||||
import type { ApiRequestClient, RequestParams } from "../http-client";
|
||||
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags Health
|
||||
* @name SimpleHealthHealth
|
||||
* @summary Check Simple API availability
|
||||
* @request GET:/api/v1/health
|
||||
*/
|
||||
export const simpleHealthHealth = (
|
||||
http: ApiRequestClient,
|
||||
requestParams: RequestParams = {},
|
||||
) =>
|
||||
http.request<HealthResponseDto, any>({
|
||||
path: `/api/v1/health`,
|
||||
method: "GET",
|
||||
format: "json",
|
||||
...requestParams,
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
|
||||
/*
|
||||
* ----------------------------------------------------------------------
|
||||
* ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
|
||||
* ## ##
|
||||
* ## Не редактируйте вручную: изменения будут перезаписаны. ##
|
||||
* ## Для изменений перегенерируйте клиент. ##
|
||||
* ## ##
|
||||
* ## Генератор: @gromlab/api-codegen ##
|
||||
* ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
|
||||
* ----------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import type {
|
||||
ErrorResponseDto,
|
||||
OrderResponseDto,
|
||||
SimpleOrdersCancelParams,
|
||||
} from "../data-contracts";
|
||||
import type { ApiRequestClient, RequestParams } from "../http-client";
|
||||
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags Orders
|
||||
* @name SimpleOrdersCancel
|
||||
* @summary Cancel an order if its state permits the transition
|
||||
* @request POST:/api/v1/orders/{id}/cancel
|
||||
* @secure
|
||||
*/
|
||||
export const simpleOrdersCancel = (
|
||||
http: ApiRequestClient,
|
||||
{ id, ...query }: SimpleOrdersCancelParams,
|
||||
requestParams: RequestParams = {},
|
||||
) =>
|
||||
http.request<OrderResponseDto, ErrorResponseDto>({
|
||||
path: `/api/v1/orders/${id}/cancel`,
|
||||
method: "POST",
|
||||
secure: true,
|
||||
format: "json",
|
||||
...requestParams,
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
|
||||
/*
|
||||
* ----------------------------------------------------------------------
|
||||
* ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
|
||||
* ## ##
|
||||
* ## Не редактируйте вручную: изменения будут перезаписаны. ##
|
||||
* ## Для изменений перегенерируйте клиент. ##
|
||||
* ## ##
|
||||
* ## Генератор: @gromlab/api-codegen ##
|
||||
* ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
|
||||
* ----------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import type {
|
||||
CreateOrderDto,
|
||||
ErrorResponseDto,
|
||||
OrderResponseDto,
|
||||
} from "../data-contracts";
|
||||
import type { ApiRequestClient, RequestParams } from "../http-client";
|
||||
import { ContentType } from "../http-client";
|
||||
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags Orders
|
||||
* @name SimpleOrdersCreate
|
||||
* @summary Create an order and validate product stock
|
||||
* @request POST:/api/v1/orders
|
||||
* @secure
|
||||
*/
|
||||
export const simpleOrdersCreate = (
|
||||
http: ApiRequestClient,
|
||||
data: CreateOrderDto,
|
||||
requestParams: RequestParams = {},
|
||||
) =>
|
||||
http.request<OrderResponseDto, ErrorResponseDto>({
|
||||
path: `/api/v1/orders`,
|
||||
method: "POST",
|
||||
body: data,
|
||||
secure: true,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...requestParams,
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
|
||||
/*
|
||||
* ----------------------------------------------------------------------
|
||||
* ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
|
||||
* ## ##
|
||||
* ## Не редактируйте вручную: изменения будут перезаписаны. ##
|
||||
* ## Для изменений перегенерируйте клиент. ##
|
||||
* ## ##
|
||||
* ## Генератор: @gromlab/api-codegen ##
|
||||
* ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
|
||||
* ----------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import type {
|
||||
ErrorResponseDto,
|
||||
OrderResponseDto,
|
||||
SimpleOrdersGetParams,
|
||||
} from "../data-contracts";
|
||||
import type { ApiRequestClient, RequestParams } from "../http-client";
|
||||
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags Orders
|
||||
* @name SimpleOrdersGet
|
||||
* @summary Get one visible order
|
||||
* @request GET:/api/v1/orders/{id}
|
||||
* @secure
|
||||
*/
|
||||
export const simpleOrdersGet = (
|
||||
http: ApiRequestClient,
|
||||
{ id, ...query }: SimpleOrdersGetParams,
|
||||
requestParams: RequestParams = {},
|
||||
) =>
|
||||
http.request<OrderResponseDto, ErrorResponseDto>({
|
||||
path: `/api/v1/orders/${id}`,
|
||||
method: "GET",
|
||||
secure: true,
|
||||
format: "json",
|
||||
...requestParams,
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
|
||||
/*
|
||||
* ----------------------------------------------------------------------
|
||||
* ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
|
||||
* ## ##
|
||||
* ## Не редактируйте вручную: изменения будут перезаписаны. ##
|
||||
* ## Для изменений перегенерируйте клиент. ##
|
||||
* ## ##
|
||||
* ## Генератор: @gromlab/api-codegen ##
|
||||
* ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
|
||||
* ----------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import type {
|
||||
ErrorResponseDto,
|
||||
OrdersResponseDto,
|
||||
SimpleOrdersListParams,
|
||||
} from "../data-contracts";
|
||||
import type { ApiRequestClient, RequestParams } from "../http-client";
|
||||
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags Orders
|
||||
* @name SimpleOrdersList
|
||||
* @summary List orders visible to the current user
|
||||
* @request GET:/api/v1/orders
|
||||
* @secure
|
||||
*/
|
||||
export const simpleOrdersList = (
|
||||
http: ApiRequestClient,
|
||||
query: SimpleOrdersListParams,
|
||||
requestParams: RequestParams = {},
|
||||
) =>
|
||||
http.request<OrdersResponseDto, ErrorResponseDto>({
|
||||
path: `/api/v1/orders`,
|
||||
method: "GET",
|
||||
query: query,
|
||||
secure: true,
|
||||
format: "json",
|
||||
...requestParams,
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
|
||||
/*
|
||||
* ----------------------------------------------------------------------
|
||||
* ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
|
||||
* ## ##
|
||||
* ## Не редактируйте вручную: изменения будут перезаписаны. ##
|
||||
* ## Для изменений перегенерируйте клиент. ##
|
||||
* ## ##
|
||||
* ## Генератор: @gromlab/api-codegen ##
|
||||
* ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
|
||||
* ----------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import type {
|
||||
CreateProductDto,
|
||||
ErrorResponseDto,
|
||||
ProductResponseDto,
|
||||
} from "../data-contracts";
|
||||
import type { ApiRequestClient, RequestParams } from "../http-client";
|
||||
import { ContentType } from "../http-client";
|
||||
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags Products
|
||||
* @name SimpleProductsCreate
|
||||
* @summary Create a product as an administrator
|
||||
* @request POST:/api/v1/products
|
||||
* @secure
|
||||
*/
|
||||
export const simpleProductsCreate = (
|
||||
http: ApiRequestClient,
|
||||
data: CreateProductDto,
|
||||
requestParams: RequestParams = {},
|
||||
) =>
|
||||
http.request<ProductResponseDto, ErrorResponseDto>({
|
||||
path: `/api/v1/products`,
|
||||
method: "POST",
|
||||
body: data,
|
||||
secure: true,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...requestParams,
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
|
||||
/*
|
||||
* ----------------------------------------------------------------------
|
||||
* ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
|
||||
* ## ##
|
||||
* ## Не редактируйте вручную: изменения будут перезаписаны. ##
|
||||
* ## Для изменений перегенерируйте клиент. ##
|
||||
* ## ##
|
||||
* ## Генератор: @gromlab/api-codegen ##
|
||||
* ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
|
||||
* ----------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import type {
|
||||
ErrorResponseDto,
|
||||
ProductResponseDto,
|
||||
SimpleProductsGetParams,
|
||||
} from "../data-contracts";
|
||||
import type { ApiRequestClient, RequestParams } from "../http-client";
|
||||
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags Products
|
||||
* @name SimpleProductsGet
|
||||
* @summary Get one product with ETag support
|
||||
* @request GET:/api/v1/products/{id}
|
||||
*/
|
||||
export const simpleProductsGet = (
|
||||
http: ApiRequestClient,
|
||||
{ id, ...query }: SimpleProductsGetParams,
|
||||
requestParams: RequestParams = {},
|
||||
) =>
|
||||
http.request<ProductResponseDto, void | ErrorResponseDto>({
|
||||
path: `/api/v1/products/${id}`,
|
||||
method: "GET",
|
||||
format: "json",
|
||||
...requestParams,
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
|
||||
/*
|
||||
* ----------------------------------------------------------------------
|
||||
* ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
|
||||
* ## ##
|
||||
* ## Не редактируйте вручную: изменения будут перезаписаны. ##
|
||||
* ## Для изменений перегенерируйте клиент. ##
|
||||
* ## ##
|
||||
* ## Генератор: @gromlab/api-codegen ##
|
||||
* ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
|
||||
* ----------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import type {
|
||||
ErrorResponseDto,
|
||||
ProductsResponseDto,
|
||||
SimpleProductsListParams,
|
||||
} from "../data-contracts";
|
||||
import type { ApiRequestClient, RequestParams } from "../http-client";
|
||||
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags Products
|
||||
* @name SimpleProductsList
|
||||
* @summary List products using offset pagination, filters and sorting
|
||||
* @request GET:/api/v1/products
|
||||
*/
|
||||
export const simpleProductsList = (
|
||||
http: ApiRequestClient,
|
||||
query: SimpleProductsListParams,
|
||||
requestParams: RequestParams = {},
|
||||
) =>
|
||||
http.request<ProductsResponseDto, ErrorResponseDto>({
|
||||
path: `/api/v1/products`,
|
||||
method: "GET",
|
||||
query: query,
|
||||
format: "json",
|
||||
...requestParams,
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
|
||||
/*
|
||||
* ----------------------------------------------------------------------
|
||||
* ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
|
||||
* ## ##
|
||||
* ## Не редактируйте вручную: изменения будут перезаписаны. ##
|
||||
* ## Для изменений перегенерируйте клиент. ##
|
||||
* ## ##
|
||||
* ## Генератор: @gromlab/api-codegen ##
|
||||
* ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
|
||||
* ----------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import type {
|
||||
ErrorResponseDto,
|
||||
MutationResponseDto,
|
||||
SimpleProductsRemoveParams,
|
||||
} from "../data-contracts";
|
||||
import type { ApiRequestClient, RequestParams } from "../http-client";
|
||||
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags Products
|
||||
* @name SimpleProductsRemove
|
||||
* @summary Delete a product as an administrator
|
||||
* @request DELETE:/api/v1/products/{id}
|
||||
* @secure
|
||||
*/
|
||||
export const simpleProductsRemove = (
|
||||
http: ApiRequestClient,
|
||||
{ id, ...query }: SimpleProductsRemoveParams,
|
||||
requestParams: RequestParams = {},
|
||||
) =>
|
||||
http.request<MutationResponseDto, ErrorResponseDto>({
|
||||
path: `/api/v1/products/${id}`,
|
||||
method: "DELETE",
|
||||
secure: true,
|
||||
format: "json",
|
||||
...requestParams,
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
|
||||
/*
|
||||
* ----------------------------------------------------------------------
|
||||
* ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
|
||||
* ## ##
|
||||
* ## Не редактируйте вручную: изменения будут перезаписаны. ##
|
||||
* ## Для изменений перегенерируйте клиент. ##
|
||||
* ## ##
|
||||
* ## Генератор: @gromlab/api-codegen ##
|
||||
* ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
|
||||
* ----------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import type {
|
||||
ErrorResponseDto,
|
||||
ProductResponseDto,
|
||||
SimpleProductsUpdateParams,
|
||||
UpdateProductDto,
|
||||
} from "../data-contracts";
|
||||
import type { ApiRequestClient, RequestParams } from "../http-client";
|
||||
import { ContentType } from "../http-client";
|
||||
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags Products
|
||||
* @name SimpleProductsUpdate
|
||||
* @summary Update a product using optimistic locking
|
||||
* @request PATCH:/api/v1/products/{id}
|
||||
* @secure
|
||||
*/
|
||||
export const simpleProductsUpdate = (
|
||||
http: ApiRequestClient,
|
||||
{ id, ...query }: SimpleProductsUpdateParams,
|
||||
data: UpdateProductDto,
|
||||
requestParams: RequestParams = {},
|
||||
) =>
|
||||
http.request<ProductResponseDto, ErrorResponseDto>({
|
||||
path: `/api/v1/products/${id}`,
|
||||
method: "PATCH",
|
||||
body: data,
|
||||
secure: true,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...requestParams,
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
|
||||
/*
|
||||
* ----------------------------------------------------------------------
|
||||
* ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
|
||||
* ## ##
|
||||
* ## Не редактируйте вручную: изменения будут перезаписаны. ##
|
||||
* ## Для изменений перегенерируйте клиент. ##
|
||||
* ## ##
|
||||
* ## Генератор: @gromlab/api-codegen ##
|
||||
* ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
|
||||
* ----------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import type {
|
||||
ChangeSimpleRoleDto,
|
||||
ErrorResponseDto,
|
||||
SimpleTestingChangeRoleParams,
|
||||
SimpleUserResponseDto,
|
||||
} from "../data-contracts";
|
||||
import type { ApiRequestClient, RequestParams } from "../http-client";
|
||||
import { ContentType } from "../http-client";
|
||||
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags Testing
|
||||
* @name SimpleTestingChangeRole
|
||||
* @summary Change a user role to exercise dynamic access control
|
||||
* @request POST:/api/v1/testing/users/{userId}/role
|
||||
*/
|
||||
export const simpleTestingChangeRole = (
|
||||
http: ApiRequestClient,
|
||||
{ userId, ...query }: SimpleTestingChangeRoleParams,
|
||||
data: ChangeSimpleRoleDto,
|
||||
requestParams: RequestParams = {},
|
||||
) =>
|
||||
http.request<SimpleUserResponseDto, ErrorResponseDto>({
|
||||
path: `/api/v1/testing/users/${userId}/role`,
|
||||
method: "POST",
|
||||
body: data,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...requestParams,
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
|
||||
/*
|
||||
* ----------------------------------------------------------------------
|
||||
* ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
|
||||
* ## ##
|
||||
* ## Не редактируйте вручную: изменения будут перезаписаны. ##
|
||||
* ## Для изменений перегенерируйте клиент. ##
|
||||
* ## ##
|
||||
* ## Генератор: @gromlab/api-codegen ##
|
||||
* ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
|
||||
* ----------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import type { TestingActionResponseDto } from "../data-contracts";
|
||||
import type { ApiRequestClient, RequestParams } from "../http-client";
|
||||
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags Testing
|
||||
* @name SimpleTestingReset
|
||||
* @summary Reset all Simple API state and token revocations
|
||||
* @request POST:/api/v1/testing/reset
|
||||
*/
|
||||
export const simpleTestingReset = (
|
||||
http: ApiRequestClient,
|
||||
requestParams: RequestParams = {},
|
||||
) =>
|
||||
http.request<TestingActionResponseDto, any>({
|
||||
path: `/api/v1/testing/reset`,
|
||||
method: "POST",
|
||||
format: "json",
|
||||
...requestParams,
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
|
||||
/*
|
||||
* ----------------------------------------------------------------------
|
||||
* ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
|
||||
* ## ##
|
||||
* ## Не редактируйте вручную: изменения будут перезаписаны. ##
|
||||
* ## Для изменений перегенерируйте клиент. ##
|
||||
* ## ##
|
||||
* ## Генератор: @gromlab/api-codegen ##
|
||||
* ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
|
||||
* ----------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import type { ScenariosResponseDto } from "../data-contracts";
|
||||
import type { ApiRequestClient, RequestParams } from "../http-client";
|
||||
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags Testing
|
||||
* @name SimpleTestingScenarios
|
||||
* @summary List deterministic X-Demo-Scenario values
|
||||
* @request GET:/api/v1/testing/scenarios
|
||||
*/
|
||||
export const simpleTestingScenarios = (
|
||||
http: ApiRequestClient,
|
||||
requestParams: RequestParams = {},
|
||||
) =>
|
||||
http.request<ScenariosResponseDto, any>({
|
||||
path: `/api/v1/testing/scenarios`,
|
||||
method: "GET",
|
||||
format: "json",
|
||||
...requestParams,
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
|
||||
/*
|
||||
* ----------------------------------------------------------------------
|
||||
* ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
|
||||
* ## ##
|
||||
* ## Не редактируйте вручную: изменения будут перезаписаны. ##
|
||||
* ## Для изменений перегенерируйте клиент. ##
|
||||
* ## ##
|
||||
* ## Генератор: @gromlab/api-codegen ##
|
||||
* ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
|
||||
* ----------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import type {
|
||||
SimpleTestingSeedParams,
|
||||
TestingActionResponseDto,
|
||||
} from "../data-contracts";
|
||||
import type { ApiRequestClient, RequestParams } from "../http-client";
|
||||
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags Testing
|
||||
* @name SimpleTestingSeed
|
||||
* @summary Select a small or large deterministic dataset
|
||||
* @request POST:/api/v1/testing/seed/{preset}
|
||||
*/
|
||||
export const simpleTestingSeed = (
|
||||
http: ApiRequestClient,
|
||||
{ preset, ...query }: SimpleTestingSeedParams,
|
||||
requestParams: RequestParams = {},
|
||||
) =>
|
||||
http.request<TestingActionResponseDto, any>({
|
||||
path: `/api/v1/testing/seed/${preset}`,
|
||||
method: "POST",
|
||||
format: "json",
|
||||
...requestParams,
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
|
||||
/*
|
||||
* ----------------------------------------------------------------------
|
||||
* ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
|
||||
* ## ##
|
||||
* ## Не редактируйте вручную: изменения будут перезаписаны. ##
|
||||
* ## Для изменений перегенерируйте клиент. ##
|
||||
* ## ##
|
||||
* ## Генератор: @gromlab/api-codegen ##
|
||||
* ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
|
||||
* ----------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import type {
|
||||
ErrorResponseDto,
|
||||
SimpleUserResponseDto,
|
||||
} from "../data-contracts";
|
||||
import type { ApiRequestClient, RequestParams } from "../http-client";
|
||||
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags Users
|
||||
* @name SimpleUsersMe
|
||||
* @summary Get the authenticated user
|
||||
* @request GET:/api/v1/users/me
|
||||
* @secure
|
||||
*/
|
||||
export const simpleUsersMe = (
|
||||
http: ApiRequestClient,
|
||||
requestParams: RequestParams = {},
|
||||
) =>
|
||||
http.request<SimpleUserResponseDto, ErrorResponseDto>({
|
||||
path: `/api/v1/users/me`,
|
||||
method: "GET",
|
||||
secure: true,
|
||||
format: "json",
|
||||
...requestParams,
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
'use client'
|
||||
|
||||
export { getCategoryListKey, useGetCategoryList } from './use-get-category-list.hook'
|
||||
export { getOrderListKey, useGetOrderList } from './use-get-order-list.hook'
|
||||
export { getProductListKey, useGetProductList } from './use-get-product-list.hook'
|
||||
@@ -0,0 +1,20 @@
|
||||
type QueryValue = boolean | number | string | null | undefined
|
||||
|
||||
/**
|
||||
* Собирает стабильную query-строку для SWR cache key.
|
||||
*/
|
||||
export const createQueryString = (query: Record<string, QueryValue>): string => {
|
||||
const searchParams = new URLSearchParams()
|
||||
|
||||
Object.entries(query).forEach(([key, value]) => {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return
|
||||
}
|
||||
|
||||
searchParams.set(key, String(value))
|
||||
})
|
||||
|
||||
const search = searchParams.toString()
|
||||
|
||||
return search ? `?${search}` : ''
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import useSWR from 'swr'
|
||||
import type { SWRConfiguration } from 'swr'
|
||||
|
||||
import type { CategoriesResponseDto } from '../generated'
|
||||
import { simpleCategoriesList } from '../generated/operations/simple-categories-list'
|
||||
import { simpleHttpClient } from '../transport/client'
|
||||
|
||||
/**
|
||||
* Возвращает SWR-ключ списка категорий.
|
||||
*/
|
||||
export const getCategoryListKey = () => {
|
||||
return ['simple-rest-api', '/api/v1/categories'] as const
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает категории каталога с прозрачным SWR-кешированием.
|
||||
*/
|
||||
export const useGetCategoryList = (
|
||||
config?: SWRConfiguration<CategoriesResponseDto>
|
||||
) => {
|
||||
return useSWR<CategoriesResponseDto>(
|
||||
getCategoryListKey(),
|
||||
() => simpleCategoriesList(simpleHttpClient),
|
||||
config
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import useSWR from 'swr'
|
||||
import type { SWRConfiguration } from 'swr'
|
||||
|
||||
import type { OrdersResponseDto, SimpleOrdersListParams } from '../generated'
|
||||
import { simpleOrdersList } from '../generated/operations/simple-orders-list'
|
||||
import { simpleHttpClient } from '../transport/client'
|
||||
import type { GetOrdersParams } from '../types'
|
||||
import { createQueryString } from './lib/create-query-string'
|
||||
|
||||
/**
|
||||
* Возвращает SWR-ключ списка заказов с фактическим endpoint.
|
||||
*/
|
||||
export const getOrderListKey = (params: GetOrdersParams = {}) => {
|
||||
const query = createQueryString(params)
|
||||
|
||||
return ['simple-rest-api', `/api/v1/orders${query}`] as const
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает страницу доступных пользователю заказов.
|
||||
*/
|
||||
export const useGetOrderList = (
|
||||
params: GetOrdersParams = {},
|
||||
config?: SWRConfiguration<OrdersResponseDto>
|
||||
) => {
|
||||
const fetcher = () => {
|
||||
// Backend schema types page and limit as Object although the wire values are numbers.
|
||||
const generatedParams = params as unknown as SimpleOrdersListParams
|
||||
return simpleOrdersList(simpleHttpClient, generatedParams)
|
||||
}
|
||||
|
||||
return useSWR<OrdersResponseDto>(getOrderListKey(params), fetcher, config)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import useSWR from 'swr'
|
||||
import type { SWRConfiguration } from 'swr'
|
||||
|
||||
import type { ProductsResponseDto, SimpleProductsListParams } from '../generated'
|
||||
import { simpleProductsList } from '../generated/operations/simple-products-list'
|
||||
import { simpleHttpClient } from '../transport/client'
|
||||
import type { GetProductsParams } from '../types'
|
||||
import { createQueryString } from './lib/create-query-string'
|
||||
|
||||
/**
|
||||
* Возвращает SWR-ключ списка продуктов с фактическим endpoint.
|
||||
*/
|
||||
export const getProductListKey = (params: GetProductsParams = {}) => {
|
||||
const query = createQueryString(params)
|
||||
|
||||
return ['simple-rest-api', `/api/v1/products${query}`] as const
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает страницу продуктов с прозрачным SWR-кешированием.
|
||||
*/
|
||||
export const useGetProductList = (
|
||||
params: GetProductsParams = {},
|
||||
config?: SWRConfiguration<ProductsResponseDto>
|
||||
) => {
|
||||
const fetcher = () => {
|
||||
// Backend schema types page and limit as Object although the wire values are numbers.
|
||||
const generatedParams = params as unknown as SimpleProductsListParams
|
||||
return simpleProductsList(simpleHttpClient, generatedParams)
|
||||
}
|
||||
|
||||
return useSWR<ProductsResponseDto>(getProductListKey(params), fetcher, config)
|
||||
}
|
||||
10
examples/react-vite/src/infra/simple-rest-api/index.ts
Normal file
10
examples/react-vite/src/infra/simple-rest-api/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export { toSimpleRestApiError } from './errors'
|
||||
export { simpleRestApi } from './rest-api'
|
||||
export {
|
||||
clearSimpleRestApiTokens,
|
||||
getSimpleRestApiRefreshToken,
|
||||
hasSimpleRestApiRefreshToken,
|
||||
setSimpleRestApiTokens
|
||||
} from './session/simple-rest-api-credentials'
|
||||
export { subscribeSimpleRestApiSessionExpired } from './session/simple-rest-api-session-events'
|
||||
export * from './hooks'
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createApiClient, operationsTree } from './generated'
|
||||
import { simpleHttpClient } from './transport/client'
|
||||
|
||||
/** Полный bound-клиент Simple REST API для private source adapters. */
|
||||
export const simpleRestApi = createApiClient(simpleHttpClient, operationsTree)
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { JwtTokensDto } from '../generated'
|
||||
|
||||
const REFRESH_TOKEN_STORAGE_KEY = 'slm-store.refresh-token'
|
||||
|
||||
let accessToken: string | null = null
|
||||
|
||||
/**
|
||||
* Возвращает sessionStorage только в browser runtime.
|
||||
*/
|
||||
const getSessionStorage = (): Storage | null => {
|
||||
if (typeof window === 'undefined') {
|
||||
return null
|
||||
}
|
||||
|
||||
return window.sessionStorage
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохраняет новую пару JWT после входа или ротации refresh token.
|
||||
*/
|
||||
export const setSimpleRestApiTokens = (tokens: JwtTokensDto): void => {
|
||||
accessToken = tokens.accessToken
|
||||
getSessionStorage()?.setItem(REFRESH_TOKEN_STORAGE_KEY, tokens.refreshToken)
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает access token текущей browser-сессии.
|
||||
*/
|
||||
export const getSimpleRestApiAccessToken = (): string | null => {
|
||||
return accessToken
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает refresh token, переживающий перезагрузку текущей вкладки.
|
||||
*/
|
||||
export const getSimpleRestApiRefreshToken = (): string | null => {
|
||||
return getSessionStorage()?.getItem(REFRESH_TOKEN_STORAGE_KEY) ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, можно ли восстановить пользовательскую сессию.
|
||||
*/
|
||||
export const hasSimpleRestApiRefreshToken = (): boolean => {
|
||||
return getSimpleRestApiRefreshToken() !== null
|
||||
}
|
||||
|
||||
/**
|
||||
* Удаляет transport credentials при выходе или истечении сессии.
|
||||
*/
|
||||
export const clearSimpleRestApiTokens = (): void => {
|
||||
accessToken = null
|
||||
getSessionStorage()?.removeItem(REFRESH_TOKEN_STORAGE_KEY)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/** Обработчик окончательного истечения REST-сессии. */
|
||||
export type SimpleRestApiSessionExpiredListener = () => void
|
||||
|
||||
const listeners = new Set<SimpleRestApiSessionExpiredListener>()
|
||||
|
||||
/**
|
||||
* Подписывает владельца пользовательской сессии на потерю credentials.
|
||||
*/
|
||||
export const subscribeSimpleRestApiSessionExpired = (
|
||||
listener: SimpleRestApiSessionExpiredListener
|
||||
): (() => void) => {
|
||||
listeners.add(listener)
|
||||
|
||||
return () => {
|
||||
listeners.delete(listener)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Сообщает подписчикам, что refresh token больше нельзя использовать.
|
||||
*/
|
||||
export const notifySimpleRestApiSessionExpired = (): void => {
|
||||
listeners.forEach((listener) => listener())
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { simpleUsersMe } from '../generated/operations/simple-users-me'
|
||||
import {
|
||||
clearSimpleRestApiTokens,
|
||||
setSimpleRestApiTokens
|
||||
} from '../session/simple-rest-api-credentials'
|
||||
import { simpleHttpClient } from './client'
|
||||
|
||||
/**
|
||||
* Создаёт JSON response для transport-level проверки.
|
||||
*/
|
||||
const jsonResponse = (body: unknown, status = 200): Response => {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
clearSimpleRestApiTokens()
|
||||
})
|
||||
|
||||
describe('simpleHttpClient', () => {
|
||||
it('coalesces concurrent 401 responses into one refresh request', async () => {
|
||||
let releaseRefresh = (): void => undefined
|
||||
const refreshGate = new Promise<void>((resolve) => {
|
||||
releaseRefresh = resolve
|
||||
})
|
||||
let refreshRequestCount = 0
|
||||
let expiredRequestCount = 0
|
||||
|
||||
/**
|
||||
* Эмулирует истёкший access token и управляемую ротацию token pair.
|
||||
*/
|
||||
const fetchMock = vi.fn(
|
||||
async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
||||
const url = new URL(String(input))
|
||||
const authorization = new Headers(init?.headers).get('Authorization')
|
||||
|
||||
if (url.pathname === '/api/v1/auth/refresh') {
|
||||
refreshRequestCount += 1
|
||||
await refreshGate
|
||||
return jsonResponse({
|
||||
data: {
|
||||
tokens: {
|
||||
accessToken: 'fresh-access',
|
||||
refreshToken: 'fresh-refresh',
|
||||
expiresIn: 60,
|
||||
tokenType: 'Bearer'
|
||||
},
|
||||
user: {
|
||||
id: 'user-admin',
|
||||
email: 'admin@demo.local',
|
||||
name: 'Demo Admin',
|
||||
role: 'admin',
|
||||
avatarUrl: null
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (authorization === 'Bearer expired-access') {
|
||||
expiredRequestCount += 1
|
||||
return jsonResponse({ code: 'JWT_INVALID', message: 'Expired' }, 401)
|
||||
}
|
||||
|
||||
return jsonResponse({
|
||||
data: {
|
||||
id: 'user-admin',
|
||||
email: 'admin@demo.local',
|
||||
name: 'Demo Admin',
|
||||
role: 'admin',
|
||||
avatarUrl: null
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
setSimpleRestApiTokens({
|
||||
accessToken: 'expired-access',
|
||||
refreshToken: 'refresh-token',
|
||||
expiresIn: 0,
|
||||
tokenType: 'Bearer'
|
||||
})
|
||||
|
||||
const requests = Promise.all([
|
||||
simpleUsersMe(simpleHttpClient),
|
||||
simpleUsersMe(simpleHttpClient)
|
||||
])
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(expiredRequestCount).toBe(2)
|
||||
expect(refreshRequestCount).toBe(1)
|
||||
})
|
||||
|
||||
releaseRefresh()
|
||||
|
||||
const responses = await requests
|
||||
|
||||
expect(responses).toHaveLength(2)
|
||||
expect(responses[0].data.email).toBe('admin@demo.local')
|
||||
expect(refreshRequestCount).toBe(1)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,84 @@
|
||||
import { ApiError, HttpClient } from '../generated'
|
||||
import { simpleAuthRefresh } from '../generated/operations/simple-auth-refresh'
|
||||
import {
|
||||
SIMPLE_REST_API_BASE_URL,
|
||||
SIMPLE_REST_API_TIMEOUT_MS
|
||||
} from '../config/simple-rest-api.config'
|
||||
import {
|
||||
clearSimpleRestApiTokens,
|
||||
getSimpleRestApiAccessToken,
|
||||
getSimpleRestApiRefreshToken,
|
||||
setSimpleRestApiTokens
|
||||
} from '../session/simple-rest-api-credentials'
|
||||
import { notifySimpleRestApiSessionExpired } from '../session/simple-rest-api-session-events'
|
||||
|
||||
const refreshHttpClient = new HttpClient({
|
||||
baseUrl: SIMPLE_REST_API_BASE_URL,
|
||||
timeout: SIMPLE_REST_API_TIMEOUT_MS
|
||||
})
|
||||
|
||||
let refreshPromise: Promise<void> | null = null
|
||||
|
||||
/**
|
||||
* Ротирует refresh token и объединяет конкурентные 401 в один запрос.
|
||||
*/
|
||||
const refreshSimpleRestApiSession = async (): Promise<void> => {
|
||||
const refreshToken = getSimpleRestApiRefreshToken()
|
||||
|
||||
if (!refreshToken) {
|
||||
throw new Error('Refresh token is missing')
|
||||
}
|
||||
|
||||
if (!refreshPromise) {
|
||||
refreshPromise = simpleAuthRefresh(refreshHttpClient, { refreshToken })
|
||||
.then((response) => {
|
||||
setSimpleRestApiTokens(response.data.tokens)
|
||||
})
|
||||
.finally(() => {
|
||||
refreshPromise = null
|
||||
})
|
||||
}
|
||||
|
||||
return refreshPromise
|
||||
}
|
||||
|
||||
/** Транспортный HTTP-клиент Simple REST API. */
|
||||
export const simpleHttpClient = new HttpClient({
|
||||
baseUrl: SIMPLE_REST_API_BASE_URL,
|
||||
timeout: SIMPLE_REST_API_TIMEOUT_MS,
|
||||
onRequest: (params) => {
|
||||
const token = getSimpleRestApiAccessToken()
|
||||
|
||||
if (!params.secure || !token) {
|
||||
return params
|
||||
}
|
||||
|
||||
const headers = new Headers(params.headers)
|
||||
|
||||
if (!headers.has('Authorization')) {
|
||||
headers.set('Authorization', `Bearer ${token}`)
|
||||
}
|
||||
|
||||
return { ...params, headers }
|
||||
},
|
||||
onError: async (error, context) => {
|
||||
const shouldRefresh =
|
||||
error instanceof ApiError &&
|
||||
error.status === 401 &&
|
||||
context.request.secure === true &&
|
||||
context.retryCount === 0
|
||||
|
||||
if (!shouldRefresh) {
|
||||
throw error
|
||||
}
|
||||
|
||||
try {
|
||||
await refreshSimpleRestApiSession()
|
||||
return context.retry()
|
||||
} catch (refreshError) {
|
||||
clearSimpleRestApiTokens()
|
||||
notifySimpleRestApiSessionExpired()
|
||||
throw refreshError
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Исправленные параметры списка заказов.
|
||||
*
|
||||
* OpenAPI backend ошибочно описывает page и limit через пустой Object schema.
|
||||
*/
|
||||
export type GetOrdersParams = {
|
||||
/** Номер страницы, начиная с единицы. */
|
||||
page?: number
|
||||
/** Число заказов на странице. */
|
||||
limit?: number
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { SortEnum } from '../generated'
|
||||
|
||||
/**
|
||||
* Исправленные параметры списка продуктов.
|
||||
*
|
||||
* OpenAPI backend ошибочно описывает page и limit через пустой Object schema.
|
||||
*/
|
||||
export type GetProductsParams = {
|
||||
/** Номер страницы, начиная с единицы. */
|
||||
page?: number
|
||||
/** Число продуктов на странице. */
|
||||
limit?: number
|
||||
/** Поиск по имени и описанию. */
|
||||
search?: string
|
||||
/** Фильтр по идентификатору категории. */
|
||||
categoryId?: string
|
||||
/** Порядок сортировки каталога. */
|
||||
sort?: SortEnum
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export type { GetOrdersParams } from './get-orders-params.type'
|
||||
export type { GetProductsParams } from './get-products-params.type'
|
||||
Reference in New Issue
Block a user