Перейти к основному содержимому

Anti-drift и кодген

Главный риск любого клиентского SDK — рассинхрон с бэком: бэкенд переименовал поле, поменял enum статуса, удалил эндпоинт, а фронт об этом узнаёт в проде. MineFlow Client SDK снимает этот риск одним принципом (ADR-0042 § Anti-drift contract):

Один источник истины на бэке → в SDK попадает кодогеном или re-export'ом, НИКОГДА копией → CI-гейт валит тот же PR, что вносит дрейф.

SDK живёт в том же монорепо и релизится из одного тега — нет version skew через сеть. Рассинхрон становится ошибкой компиляции в том же PR, а не рантайм-сюрпризом у пользователя.

Что значит «никогда копией»

Если ты видишь в коде SDK захардкоженный тип ответа, список статусов или регекс эндпоинта — это баг. Каждый такой артефакт должен либо генерироваться из live-OpenAPI, либо реэкспортироваться из исходной либы. Копия = форк = гарантированный дрейф.

Карта артефактов: что откуда берётся

Для фронтенд-разработчика важно понимать, что трогать руками нельзя — оно регенерируется и любая ручная правка затрётся:

АртефактИсточник истины (бэк)Доставка в SDKГейт от дрейфа
API-типы (@mineflow/api-client)NestJS controllers/DTOcodegen pnpm openapi:clientopenapi:diff + api-client drift gate (blocking) + typecheck
Zod-схемы форм (@mineflow/api-zod)*.schema.ts (pure-часть)codegen pnpm openapi:zodtypecheck
FSM-машины (@mineflow/api-schemas)libs/*/state-machine/*.machine.tsre-exportfsm:check (blocking) + typecheck
Роли (SystemRole)libs/auth SYSTEM_ROLESimport type (zero-runtime)typecheck
Error-кодыдомен *.errors.ts + filter (RFC 7807)нормализация по code в client-coretypecheck + контрактные тесты
Тривиальные read-хукиOpenAPI pathscodegen pnpm gen:hooksсверка пути с live-спекой (fail-loud)

Подробности по каждому пакету — в гайдах: api-client, api-zod, api-schemas, client-react, auth-web.

Откуда что: типы, схемы, FSM, роли

Все четыре доменных артефакта подтягиваются автоматически — ты просто импортируешь готовое:

import type { paths } from '@mineflow/api-client'; // типы REST (codegen, zero-runtime)
import { zCreateAssetDto } from '@mineflow/api-zod'; // Zod-схема формы (codegen)
import { assetMachine } from '@mineflow/api-schemas'; // FSM-машина (re-export)
import { useCan } from '@mineflow/client-react'; // RBAC поверх SystemRole (import type)
  • Типы (api-client) — генерируются из apps/api/openapi.json. Стираются при компиляции (zero-runtime), поэтому безопасны для браузера и Hermes. Если бэк сменил форму DTO — поменяется тип, и компиляция SDK упадёт в том же PR.
  • Zod-схемы (api-zod) — генерируются из той же OpenAPI. Бэк оборачивает схему в createZodDto, фронт получает чистую .strict()-версию для react-hook-form. См. рецепт по формам.
  • FSM (api-schemas) — реэкспорт xstate-машин из libs/*/state-machine. useAvailableActions(machine, status) показывает кнопки строго по машине бэка — см. рецепт по FSM.
  • Ролиimport type { SystemRole }: 7 канонических ролей напрямую из libs/auth, без копии. См. рецепт по RBAC.

Read-хуки генерятся (gen:hooks), fail-loud

Тривиальные read-хуки (list / detail / sub-коллекция) — useAssets, useAsset, usePersonnel, useShiftReports, useShiftReportEntries и т.д. — не пишутся руками. Это чистый boilerplate useApiQuery(key, c => unwrap(c.GET(path, …))), поэтому он генерируется из OpenAPI:

pnpm gen:hooks # standalone; также входит в pnpm openapi:client

Генератор (tools/scripts/gen-domain-hooks.ts) пишет файлы packages/client-react/src/domain-hooks/<area>.generated.ts (eam / hr / prd).

warning
Не редактируй *.generated.ts

Файлы с суффиксом .generated.ts помечены баннером AUTO-GENERATED. Любая ручная правка затрётся при следующем pnpm gen:hooks. Рукописные хуки (write / саги / offline) живут в соседнем <area>.ts и реэкспортят свой .generated.

Fail-loud — главное свойство генератора. Каждый путь манифеста сверяется с живой OpenAPI-спекой на этапе генерации: метод существует, есть ли query/path-параметры, есть ли обязательные query. Если бэк переименовал или удалил эндпоинт — pnpm gen:hooks падает с понятной ошибкой и не пишет частичный файл, вместо того чтобы молча сгенерировать битый хук:

✗ gen-domain-hooks: путь /api/v1/eam/assets отсутствует в OpenAPI-спеке (дрейф бэка?).

То же касается обязательных query-параметров: для /hr/timesheet (требует dateFrom/dateTo) генератор выпускает хук без default-аргумента — забыть параметр станет ошибкой типа, а не пустым ответом в рантайме.

Добавить read-хуки нового агрегата

Это задача для мейнтейнеров SDK, но знать механику полезно. Чтобы добавить, например, scm/fuel:

  1. одна строка в манифесте HOOKS в tools/scripts/gen-domain-hooks.ts (hook / path / keyGroup / kind);
  2. группа ключей в packages/client-react/src/query-keys.ts;
  3. pnpm gen:hooks.

Код самого генератора править не нужно — area выводится из пути (/api/v1/<area>/...). Сложные хуки (write с инвалидацией, саги, offline) остаются рукописными на дженериках useDomainMutation / useSagaMutation — см. гайд client-react.

Команды кодгена

pnpm openapi:export # выгрузить live-спеку NestJS → apps/api/openapi.json
pnpm openapi:client # типы api-client + gen:hooks (запускает openapi:export внутри)
pnpm openapi:zod # Zod-схемы api-zod из той же openapi.json
pnpm gen:hooks # только read-хуки (требует свежий openapi.json)

Конвейер: openapi:client сам делает openapi:export, генерит @mineflow/api-client и в конце дёргает gen:hooks. То есть после изменения контроллера/DTO на бэке достаточно одного pnpm openapi:client, чтобы и типы, и read-хуки пришли в согласие со спекой. openapi:zod запускается отдельно (читает уже выгруженный apps/api/openapi.json).

Полный ресинк после изменений на бэке
pnpm openapi:client && pnpm openapi:zod && pnpm typecheck

typecheck через turbo накрывает все пакеты @mineflow/client-* — breaking-изменение DTO ломает компиляцию SDK сразу, в том же месте.

CI-гейты от дрейфа

Anti-drift работает не на дисциплине, а на CI. Каждый источник дрейфа имеет блокирующий гейт:

  • pnpm openapi:diffoasdiff breaking-check live-спеки против baseline. Silent breaking-изменение API не проходит.
  • api-client drift gate (blocking) — «забыл перегенерить api-client» = красный PR. Раньше был advisory из-за флапа openapi:export без Redis — закрыто добавлением Redis в job.
  • pnpm typecheck (turbo) — SDK типится против сгенерированных типов; breaking-изменение DTO или ролей ломает компиляцию пакетов @mineflow/client-*.
  • pnpm fsm:check — ADR-0022 gate: сверяет xstate-машины с фикстурами. FSM в api-schemas не разойдётся с бэком.
  • pnpm gen:hooks — fail-loud при дрейфе путей (см. выше); в CI запускается как часть openapi:client.
Runtime-граница (деплой web vs API)

Кодген закрывает compile-time дрейф. На рантайме SDK и API всё равно разнесены по деплою: спасает префикс /api/v1 + expand–contract эволюция контракта + openapi:diff, который не пускает silent breaking. baseUrl — это origin без пути; пути из api-client уже содержат /api/v1.

Что делать, если что-то всё-таки разъехалось

Симптомы и лечение:

  • pnpm gen:hooks падает с «путь … отсутствует в OpenAPI-спеке» — бэк переименовал/удалил эндпоинт. Сначала pnpm openapi:export, потом скорректируй строку в манифесте HOOKS под новый путь.
  • typecheck ругается на поле/тип после pull'а — устарел api-client. Перегенери: pnpm openapi:client.
  • Форма валидируется не теми правилами — устарел api-zod. pnpm openapi:zod.
  • useAvailableActions показывает не те кнопки — машина в api-schemas могла разойтись; запусти pnpm fsm:check и пересобери.
  • Ошибки гейтишь по тексту, а не по code — это и есть дрейф: error.code стабилен (ASSET_INVALID_STATUS_TRANSITION), текст — нет. См. рецепт по ошибкам.

Полный обзор слоёв SDK и быстрый старт — в обзорном гайде. Полная карта anti-drift контракта — ADR-0042. REST-референс того же OpenAPI — /rest/.