feat(openapi): улучшенный Swagger + TS-клиент через openapi-typescript (P1-19)

API:
• SwaggerGen с OpenAPI info (title/version/description),
  Bearer security-scheme (через OpenIddict JWT),
  стабильные operationId = Controller_VerbAction (HTTP-verb включён
  чтобы избежать коллизии когда ASP.NET стрипает Async-суффикс —
  WipeAll и WipeAllAsync ранее давали одинаковый operationId);
• CustomSchemaIds с префиксом из namespace (одноимённые nested
  record'ы в разных контроллерах больше не схлопываются — StockRow
  есть в Inventory_StockController и Reports_StockReportController).

UI:
• /swagger (UI) и /swagger/v1/swagger.json (документ) — только в Development.
  На prod не раскрываем (endpoint enumeration).

Web:
• Добавлен devDependency openapi-typescript@^7.5.2 + npm-script gen:api,
  читающий http://localhost:5081/swagger/v1/swagger.json.
• src/lib/api.generated.ts — сгенерированные типы (~7700 строк, все
  схемы и operations).
• src/lib/apiClient.ts — тонкая обёртка над axios api, использующая
  типы из generated. Подключена для пары контроллеров (Reports/Sales,
  Reports/ABC, Reports/Profit) как образец постепенной миграции.

docs/openapi.md — workflow генерации (live API или Swashbuckle CLI),
versioning, наставления для нового кода.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
nns 2026-05-28 11:39:22 +05:00
parent 96fa4bf990
commit dbd08f6fd2
6 changed files with 8099 additions and 13 deletions

59
docs/openapi.md Normal file
View file

@ -0,0 +1,59 @@
# OpenAPI / Swagger
API публикует OpenAPI-документ через `Swashbuckle.AspNetCore`. Описание
включает security-scheme `Bearer` (OpenIddict JWT), стабильные
`operationId = Controller_Action`, уникальные `schemaId` с префиксом из
неймспейса (одноимённые nested record'ы в разных контроллерах не схлопываются).
## Эндпоинты
| URL | Когда |
|---|---|
| `/swagger` | UI, только Development |
| `/swagger/v1/swagger.json` | JSON-документ, только Development |
На stage/prod swagger отключён — отдельный endpoint enumeration
не должен раскрываться неавторизованным клиентам. Если нужно — поднимать
локальный API из той же ветки.
## TypeScript-клиент
В `src/food-market.web` подключён `openapi-typescript` (devDependency).
Команда:
```bash
# Терминал 1: поднять API
ASPNETCORE_ENVIRONMENT=Development dotnet run --project src/food-market.api
# Терминал 2: сгенерировать types
cd src/food-market.web
pnpm run gen:api # читает http://localhost:5081/swagger/v1/swagger.json
# → src/lib/api.generated.ts
```
Альтернативно (без живого API) — через `Swashbuckle.AspNetCore.Cli` (версия
должна совпадать с `Swashbuckle.AspNetCore`, у нас 6.9.0):
```bash
dotnet tool install --global Swashbuckle.AspNetCore.Cli --version 6.9.0
dotnet build src/food-market.api
swagger tofile --output /tmp/swagger.json \
src/food-market.api/bin/Debug/net8.0/foodmarket.Api.dll v1
cd src/food-market.web
pnpm exec openapi-typescript /tmp/swagger.json -o src/lib/api.generated.ts
```
## Использование
Тонкая обёртка в `src/food-market.web/src/lib/apiClient.ts` экспортирует
типизированные хелперы для отчётов (Reports/Sales, Reports/ABC,
Reports/Profit) — образец постепенной миграции с ручных типов в
`types.ts`. В новом коде использовать обёртку и переэкспортированные
типы; старые страницы переписывать по мере правок.
## Версионирование
Document `v1` — единственный. Если будут breaking changes — поднимаем
`v2` рядом, не ломая `v1`. У `operationId` стабильное имя
`Controller_Action` — переименование контроллера ломает TS-клиент,
относиться как к public API.

View file

@ -158,7 +158,58 @@
o.Filters.AddService<foodmarket.Api.Infrastructure.Tenancy.SuperAdminEditAuditFilter>();
});
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddSwaggerGen(opts =>
{
opts.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo
{
Title = "food-market API",
Version = "v1",
Description = "Multi-tenant POS/inventory backend. Все запросы " +
"ограничены организацией текущего JWT (claim `org_id`).",
});
// Bearer JWT через OpenIddict. Swagger UI «Authorize» подставит в Authorization.
var bearer = new Microsoft.OpenApi.Models.OpenApiSecurityScheme
{
Type = Microsoft.OpenApi.Models.SecuritySchemeType.Http,
Scheme = "bearer",
BearerFormat = "JWT",
In = Microsoft.OpenApi.Models.ParameterLocation.Header,
Description = "Access token, полученный через POST /connect/token.",
};
opts.AddSecurityDefinition("Bearer", bearer);
opts.AddSecurityRequirement(new Microsoft.OpenApi.Models.OpenApiSecurityRequirement
{
[new Microsoft.OpenApi.Models.OpenApiSecurityScheme
{
Reference = new Microsoft.OpenApi.Models.OpenApiReference
{
Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme,
Id = "Bearer",
},
}] = Array.Empty<string>(),
});
// Стабильные operationId для генерации TS-клиентов:
// <Controller>_<Verb><Action>. Verb включён чтобы избежать коллизии
// когда ASP.NET стрипает Async-суффикс и два метода (WipeAll, WipeAllAsync)
// получают одинаковое имя action → одинаковый operationId → duplicate.
opts.CustomOperationIds(api =>
{
var ctrl = api.ActionDescriptor.RouteValues["controller"];
var action = api.ActionDescriptor.RouteValues["action"];
var verb = api.HttpMethod is { Length: > 0 } m ? char.ToUpper(m[0]) + m[1..].ToLowerInvariant() : "";
return $"{ctrl}_{verb}{action}";
});
// У нас есть одноимённые nested record'ы в разных контроллерах
// (например, StockRow в StockController и StockReportController).
// Включаем имя контроллера в schemaId через FullName-suffix чтобы не
// словить duplicate schemaId — Swashbuckle падает на этом по умолчанию.
opts.CustomSchemaIds(t => t.FullName!
.Replace("foodmarket.Api.Controllers.", "")
.Replace("foodmarket.Application.", "")
.Replace("foodmarket.Domain.", "")
.Replace("+", "_")
.Replace(".", "_"));
});
// MoySklad import integration. Auto-decompress gzip responses from MoySklad's edge.
// BaseAddress берётся из конфигурации (MoySklad:BaseUrl) с дефолтом на боевой
@ -237,8 +288,15 @@
if (app.Environment.IsDevelopment())
{
// Swagger/OpenAPI: только в Development. /swagger/v1/swagger.json — JSON-документ,
// /swagger — UI. На prod не раскрываем (sensitive endpoint enumeration), на dev
// используется фронтом для генерации TS-клиента (`pnpm run gen:api`).
app.UseSwagger();
app.UseSwaggerUI();
app.UseSwaggerUI(opts =>
{
opts.DocumentTitle = "food-market API";
opts.RoutePrefix = "swagger";
});
}
app.MapControllers();

View file

@ -7,7 +7,8 @@
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
"preview": "vite preview",
"gen:api": "openapi-typescript http://localhost:5081/swagger/v1/swagger.json -o src/lib/api.generated.ts"
},
"dependencies": {
"@hookform/resolvers": "^5.2.2",
@ -40,6 +41,7 @@
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.5.0",
"openapi-typescript": "^7.5.2",
"postcss": "^8.5.10",
"tailwindcss": "^4.2.3",
"typescript": "~6.0.2",

View file

@ -93,6 +93,9 @@ importers:
globals:
specifier: ^17.5.0
version: 17.5.0
openapi-typescript:
specifier: ^7.5.2
version: 7.13.0(typescript@6.0.3)
postcss:
specifier: ^8.5.10
version: 8.5.10
@ -296,6 +299,16 @@ packages:
'@oxc-project/types@0.126.0':
resolution: {integrity: sha512-oGfVtjAgwQVVpfBrbtk4e1XDyWHRFta6BS3GWVzrF8xYBT2VGQAk39yJS/wFSMrZqoiCU4oghT3Ch0HaHGIHcQ==}
'@redocly/ajv@8.11.2':
resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==}
'@redocly/config@0.22.0':
resolution: {integrity: sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ==}
'@redocly/openapi-core@1.34.15':
resolution: {integrity: sha512-HAwCnNyKcs5XGQqms+9t7OdAPM/5TDstmhF+0i7tdCFato2QKuYIlyWETwkXd8c5zbltr1oB+6y9NTeQLr2d6Q==}
engines: {node: '>=18.17.0', npm: '>=9.5.0'}
'@reduxjs/toolkit@2.11.2':
resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==}
peerDependencies:
@ -657,9 +670,17 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
agent-base@7.1.4:
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
engines: {node: '>= 14'}
ajv@6.14.0:
resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==}
ansi-colors@4.1.3:
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
engines: {node: '>=6'}
ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
@ -695,6 +716,9 @@ packages:
brace-expansion@1.1.14:
resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==}
brace-expansion@2.1.1:
resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==}
brace-expansion@5.0.5:
resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==}
engines: {node: 18 || 20 || >=22}
@ -719,6 +743,9 @@ packages:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
change-case@5.4.4:
resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==}
class-variance-authority@0.7.1:
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
@ -733,6 +760,9 @@ packages:
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
colorette@1.4.0:
resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
@ -1032,6 +1062,10 @@ packages:
hermes-parser@0.25.1:
resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==}
https-proxy-agent@7.0.6:
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
engines: {node: '>= 14'}
ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
@ -1054,6 +1088,10 @@ packages:
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
engines: {node: '>=0.8.19'}
index-to-position@1.2.0:
resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==}
engines: {node: '>=18'}
internmap@2.0.3:
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
engines: {node: '>=12'}
@ -1073,6 +1111,10 @@ packages:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true
js-levenshtein@1.1.6:
resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==}
engines: {node: '>=0.10.0'}
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@ -1091,6 +1133,9 @@ packages:
json-schema-traverse@0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
json-schema-traverse@1.0.0:
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
json-stable-stringify-without-jsonify@1.0.1:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
@ -1217,6 +1262,10 @@ packages:
minimatch@3.1.5:
resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
minimatch@5.1.9:
resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==}
engines: {node: '>=10'}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@ -1231,6 +1280,12 @@ packages:
node-releases@2.0.37:
resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==}
openapi-typescript@7.13.0:
resolution: {integrity: sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ==}
hasBin: true
peerDependencies:
typescript: ^5.x
optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
@ -1247,6 +1302,10 @@ packages:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
parse-json@8.3.0:
resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==}
engines: {node: '>=18'}
path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
@ -1262,6 +1321,10 @@ packages:
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
engines: {node: '>=12'}
pluralize@8.0.0:
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
engines: {node: '>=4'}
postcss-value-parser@4.2.0:
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
@ -1354,6 +1417,10 @@ packages:
redux@5.0.1:
resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==}
require-from-string@2.0.2:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
reselect@5.1.1:
resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
@ -1397,6 +1464,10 @@ packages:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
supports-color@10.2.2:
resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==}
engines: {node: '>=18'}
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
@ -1439,6 +1510,10 @@ packages:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
type-fest@4.41.0:
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
engines: {node: '>=16'}
typescript-eslint@8.59.0:
resolution: {integrity: sha512-BU3ONW9X+v90EcCH9ZS6LMackcVtxRLlI3XrYyqZIwVSHIk7Qf7bFw1z0M9Q0IUxhTMZCf8piY9hTYaNEIASrw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@ -1460,6 +1535,9 @@ packages:
peerDependencies:
browserslist: '>= 4.21.0'
uri-js-replace@1.0.1:
resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==}
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
@ -1526,6 +1604,13 @@ packages:
yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
yaml-ast-parser@0.0.43:
resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==}
yargs-parser@21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'}
yocto-queue@0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
@ -1562,7 +1647,7 @@ snapshots:
'@babel/types': 7.29.0
'@jridgewell/remapping': 2.3.5
convert-source-map: 2.0.0
debug: 4.4.3
debug: 4.4.3(supports-color@10.2.2)
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 6.3.1
@ -1632,7 +1717,7 @@ snapshots:
'@babel/parser': 7.29.2
'@babel/template': 7.28.6
'@babel/types': 7.29.0
debug: 4.4.3
debug: 4.4.3(supports-color@10.2.2)
transitivePeerDependencies:
- supports-color
@ -1667,7 +1752,7 @@ snapshots:
'@eslint/config-array@0.21.2':
dependencies:
'@eslint/object-schema': 2.1.7
debug: 4.4.3
debug: 4.4.3(supports-color@10.2.2)
minimatch: 3.1.5
transitivePeerDependencies:
- supports-color
@ -1683,7 +1768,7 @@ snapshots:
'@eslint/eslintrc@3.3.5':
dependencies:
ajv: 6.14.0
debug: 4.4.3
debug: 4.4.3(supports-color@10.2.2)
espree: 10.4.0
globals: 14.0.0
ignore: 5.3.2
@ -1777,6 +1862,29 @@ snapshots:
'@oxc-project/types@0.126.0': {}
'@redocly/ajv@8.11.2':
dependencies:
fast-deep-equal: 3.1.3
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
uri-js-replace: 1.0.1
'@redocly/config@0.22.0': {}
'@redocly/openapi-core@1.34.15(supports-color@10.2.2)':
dependencies:
'@redocly/ajv': 8.11.2
'@redocly/config': 0.22.0
colorette: 1.4.0
https-proxy-agent: 7.0.6(supports-color@10.2.2)
js-levenshtein: 1.1.6
js-yaml: 4.1.1
minimatch: 5.1.9
pluralize: 8.0.0
yaml-ast-parser: 0.0.43
transitivePeerDependencies:
- supports-color
'@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.5)(redux@5.0.1))(react@19.2.5)':
dependencies:
'@standard-schema/spec': 1.1.0
@ -1998,7 +2106,7 @@ snapshots:
'@typescript-eslint/types': 8.59.0
'@typescript-eslint/typescript-estree': 8.59.0(typescript@6.0.3)
'@typescript-eslint/visitor-keys': 8.59.0
debug: 4.4.3
debug: 4.4.3(supports-color@10.2.2)
eslint: 9.39.4(jiti@2.6.1)
typescript: 6.0.3
transitivePeerDependencies:
@ -2008,7 +2116,7 @@ snapshots:
dependencies:
'@typescript-eslint/tsconfig-utils': 8.59.0(typescript@6.0.3)
'@typescript-eslint/types': 8.59.0
debug: 4.4.3
debug: 4.4.3(supports-color@10.2.2)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
@ -2027,7 +2135,7 @@ snapshots:
'@typescript-eslint/types': 8.59.0
'@typescript-eslint/typescript-estree': 8.59.0(typescript@6.0.3)
'@typescript-eslint/utils': 8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3)
debug: 4.4.3
debug: 4.4.3(supports-color@10.2.2)
eslint: 9.39.4(jiti@2.6.1)
ts-api-utils: 2.5.0(typescript@6.0.3)
typescript: 6.0.3
@ -2042,7 +2150,7 @@ snapshots:
'@typescript-eslint/tsconfig-utils': 8.59.0(typescript@6.0.3)
'@typescript-eslint/types': 8.59.0
'@typescript-eslint/visitor-keys': 8.59.0
debug: 4.4.3
debug: 4.4.3(supports-color@10.2.2)
minimatch: 10.2.5
semver: 7.7.4
tinyglobby: 0.2.16
@ -2078,6 +2186,8 @@ snapshots:
acorn@8.16.0: {}
agent-base@7.1.4: {}
ajv@6.14.0:
dependencies:
fast-deep-equal: 3.1.3
@ -2085,6 +2195,8 @@ snapshots:
json-schema-traverse: 0.4.1
uri-js: 4.4.1
ansi-colors@4.1.3: {}
ansi-styles@4.3.0:
dependencies:
color-convert: 2.0.1
@ -2121,6 +2233,10 @@ snapshots:
balanced-match: 1.0.2
concat-map: 0.0.1
brace-expansion@2.1.1:
dependencies:
balanced-match: 1.0.2
brace-expansion@5.0.5:
dependencies:
balanced-match: 4.0.4
@ -2147,6 +2263,8 @@ snapshots:
ansi-styles: 4.3.0
supports-color: 7.2.0
change-case@5.4.4: {}
class-variance-authority@0.7.1:
dependencies:
clsx: 2.1.1
@ -2159,6 +2277,8 @@ snapshots:
color-name@1.1.4: {}
colorette@1.4.0: {}
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
@ -2217,9 +2337,11 @@ snapshots:
date-fns@4.1.0: {}
debug@4.4.3:
debug@4.4.3(supports-color@10.2.2):
dependencies:
ms: 2.1.3
optionalDependencies:
supports-color: 10.2.2
decimal.js-light@2.5.1: {}
@ -2306,7 +2428,7 @@ snapshots:
ajv: 6.14.0
chalk: 4.1.2
cross-spawn: 7.0.6
debug: 4.4.3
debug: 4.4.3(supports-color@10.2.2)
escape-string-regexp: 4.0.0
eslint-scope: 8.4.0
eslint-visitor-keys: 4.2.1
@ -2443,6 +2565,13 @@ snapshots:
dependencies:
hermes-estree: 0.25.1
https-proxy-agent@7.0.6(supports-color@10.2.2):
dependencies:
agent-base: 7.1.4
debug: 4.4.3(supports-color@10.2.2)
transitivePeerDependencies:
- supports-color
ignore@5.3.2: {}
ignore@7.0.5: {}
@ -2458,6 +2587,8 @@ snapshots:
imurmurhash@0.1.4: {}
index-to-position@1.2.0: {}
internmap@2.0.3: {}
is-extglob@2.1.1: {}
@ -2470,6 +2601,8 @@ snapshots:
jiti@2.6.1: {}
js-levenshtein@1.1.6: {}
js-tokens@4.0.0: {}
js-yaml@4.1.1:
@ -2482,6 +2615,8 @@ snapshots:
json-schema-traverse@0.4.1: {}
json-schema-traverse@1.0.0: {}
json-stable-stringify-without-jsonify@1.0.1: {}
json5@2.2.3: {}
@ -2578,6 +2713,10 @@ snapshots:
dependencies:
brace-expansion: 1.1.14
minimatch@5.1.9:
dependencies:
brace-expansion: 2.1.1
ms@2.1.3: {}
nanoid@3.3.11: {}
@ -2586,6 +2725,16 @@ snapshots:
node-releases@2.0.37: {}
openapi-typescript@7.13.0(typescript@6.0.3):
dependencies:
'@redocly/openapi-core': 1.34.15(supports-color@10.2.2)
ansi-colors: 4.1.3
change-case: 5.4.4
parse-json: 8.3.0
supports-color: 10.2.2
typescript: 6.0.3
yargs-parser: 21.1.1
optionator@0.9.4:
dependencies:
deep-is: 0.1.4
@ -2607,6 +2756,12 @@ snapshots:
dependencies:
callsites: 3.1.0
parse-json@8.3.0:
dependencies:
'@babel/code-frame': 7.29.0
index-to-position: 1.2.0
type-fest: 4.41.0
path-exists@4.0.0: {}
path-key@3.1.1: {}
@ -2615,6 +2770,8 @@ snapshots:
picomatch@4.0.4: {}
pluralize@8.0.0: {}
postcss-value-parser@4.2.0: {}
postcss@8.5.10:
@ -2699,6 +2856,8 @@ snapshots:
redux@5.0.1: {}
require-from-string@2.0.2: {}
reselect@5.1.1: {}
resolve-from@4.0.0: {}
@ -2742,6 +2901,8 @@ snapshots:
strip-json-comments@3.1.1: {}
supports-color@10.2.2: {}
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0
@ -2776,6 +2937,8 @@ snapshots:
dependencies:
prelude-ls: 1.2.1
type-fest@4.41.0: {}
typescript-eslint@8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3):
dependencies:
'@typescript-eslint/eslint-plugin': 8.59.0(@typescript-eslint/parser@8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3))(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3)
@ -2797,6 +2960,8 @@ snapshots:
escalade: 3.2.0
picocolors: 1.1.1
uri-js-replace@1.0.1: {}
uri-js@4.4.1:
dependencies:
punycode: 2.3.1
@ -2842,6 +3007,10 @@ snapshots:
yallist@3.1.1: {}
yaml-ast-parser@0.0.43: {}
yargs-parser@21.1.1: {}
yocto-queue@0.1.0: {}
zod-validation-error@4.0.2(zod@4.3.6):

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,53 @@
/**
* Тонкая обёртка над axios `api`, использующая типы из api.generated.ts
* (сгенерированы из /swagger/v1/swagger.json через openapi-typescript).
*
* Регенерация:
* 1. dotnet run --project src/food-market.api (или просто запущенный API в dev)
* 2. cd src/food-market.web && pnpm run gen:api
* перезаписывает src/lib/api.generated.ts
*
* Эта обёртка демонстрирует подключение для пары контроллеров (reports/sales,
* reports/abc) как образец миграции с ручных типов. Остальные страницы
* пользуются явными типами из `types.ts` постепенная миграция в новом коде.
*
* Multi-tenant: контроллеры на API-стороне применяют tenant query-filter
* автоматически, обёртка ничего не делает специального только подставляет
* Authorization-токен (через axios interceptor в `api.ts`).
*/
import { api } from './api'
import type { components, paths } from './api.generated'
/** Типы DTO, переэкспорт из generated для удобства импорта на страницах. */
export type SalesReportRowDto = components['schemas']['Reports_SalesReportController_SalesRow']
export type AbcReportRowDto = components['schemas']['Reports_AbcReportController_AbcRow']
export type ProfitReportRowDto = components['schemas']['Reports_ProfitReportController_ProfitRow']
type SalesQuery = NonNullable<paths['/api/reports/sales']['get']['parameters']['query']>
type AbcQuery = NonNullable<paths['/api/reports/abc']['get']['parameters']['query']>
type ProfitQuery = NonNullable<paths['/api/reports/profit']['get']['parameters']['query']>
function qs(params: Record<string, unknown>): string {
const p = new URLSearchParams()
for (const [k, v] of Object.entries(params)) {
if (v == null || v === '') continue
p.set(k, v instanceof Date ? v.toISOString() : String(v))
}
return p.toString()
}
/** Тонкие хелперы, типизированные через generated types. */
export const reports = {
async sales(params: SalesQuery): Promise<SalesReportRowDto[]> {
const { data } = await api.get<SalesReportRowDto[]>(`/api/reports/sales?${qs(params)}`)
return data
},
async abc(params: AbcQuery): Promise<AbcReportRowDto[]> {
const { data } = await api.get<AbcReportRowDto[]>(`/api/reports/abc?${qs(params)}`)
return data
},
async profit(params: ProfitQuery): Promise<ProfitReportRowDto[]> {
const { data } = await api.get<ProfitReportRowDto[]>(`/api/reports/profit?${qs(params)}`)
return data
},
}