@mineflow/client-react
Слой L2 клиентского SDK (ADR-0042): React-провайдер плюс набор х уков на TanStack Query поверх ядра @mineflow/client-core. Даёт чтение (concrete/refs/generic), мутации с авто-Idempotency-Key и инвалидацией кэша, write-lifecycle сменного рапорта с offline-first очередью, async-саги, live-уведомления + presence через Centrifugo, RBAC- и FSM-гейты для UI.
Это основной пакет для фронтенда: в 95% случаев приложение работает напрямую только с client-react (плюс @mineflow/api-zod для форм, @mineflow/api-schemas для FSM-машин и @mineflow/auth-web / @mineflow/auth-native для токенов). Пакет платформо-агностичен — те же хуки работают в web и React Native, всё платформо-специфичное инъектируется через провайдер.
Архитектура слоёв и сквозной end-to-end пример — в обзоре SDK и в гайдах нижних слоёв @mineflow/client-core, @mineflow/api-client.
Установка
pnpm add @mineflow/client-react @mineflow/client-core @mineflow/auth-web
# peer-зависимости приложения:
pnpm add react @tanstack/react-query xstate
# опционально под формы и FSM:
pnpm add @mineflow/api-zod @mineflow/api-schemas @hookform/resolvers react-hook-form
Peer-зависимости пакета (из package.json): react ^18 || ^19, @tanstack/react-query ^5, xstate ^5. В монорепо MineFlow внутренние пакеты резолвятся как workspace:*.
1. Провайдер
Оберни приложение в MineflowProvider один раз. Провайдер строит аутентифицированный fetch (Bearer) и REST-клиент из конфига, поднимает QueryClientProvider и кладёт всё в контекст.
import Keycloak from 'keycloak-js';
import { KeycloakTokenProvider } from '@mineflow/auth-web';
import { MineflowProvider } from '@mineflow/client-react';
const keycloak = new Keycloak({
url: 'https://auth.mineflow.local',
realm: 'mineflow',
clientId: 'mineflow-web',
});
await keycloak.init({ onLoad: 'check-sso', pkceMethod: 'S256' });
// Адаптер создаётся ОДИН раз вне рендера (сравнивается по ссылке).
const tokenProvider = new KeycloakTokenProvider({ keycloak });
function App() {
return (
<MineflowProvider
baseUrl={import.meta.env.VITE_API_BASE} // origin; пути уже включают /api/v1
tokenProvider={tokenProvider}
generateId={() => crypto.randomUUID()} // RN: react-native-get-random-values
roles={tokenProvider.getRoles()} // канонические SystemRole
>
{/* ... */}
</MineflowProvider>
);
}
Полный список пропсов (MineflowProviderProps):
| Проп | Тип | Назначение |
|---|---|---|
baseUrl | string | Origin API (без пути; /api/v1 уже зашит в путях из api-client). |
tokenProvider | TokenProvider | Auth-адаптер (web — KeycloakTokenProvider, RN — ReactNativeTokenProvider). |
generateId | IdGenerator | Генератор Idempotency-Key (web — crypto.randomUUID, RN — polyfill). |
roles | readonly SystemRole[] | Канонические роли текущего пользователя (для useCan). |
queryClient | QueryClient? | Внешний QueryClient; если не передан — создаётся свой. |
fetchImpl | typeof fetch? | Базовый fetch для REST-транспорта. По умолчанию глобальный fetch. |
Доступ к конте ксту — useMineflow() (вернёт { rest, authFetch, baseUrl, roles }) и useMineflowClient() (только REST-клиент). Вне провайдера оба бросают ошибку.
MineflowProvider НЕ поднимает live-доставку сам. Для уведомлений/presence через Centrifugo оберни поддерево в RealtimeProvider (внутри MineflowProvider) — см. §7 ниже и рецепт «Realtime».
Аутентифицированный fetch + REST-клиент пересобираются только при смене baseUrl, tokenProvider, generateId или fetchImpl. Смена roles НЕ влияет на транспорт (адресация realtime считается сервером по токену, каналы не зависят от UI-ролей). roles сравниваются по содержимому (rolesSignature), поэтому inline-литерал roles={['Foreman']} не вызывает лишних перестроений. А вот tokenProvider/generateId сравниваются по ссылке — создавай адаптер один раз вне рендера, иначе транспорт пересоберётся.
2. Чтение данных
Concrete-хуки (типобезопасные)
Тривиальные list/detail/sub-хуки генерятся из OpenAPI (см. «Кодген хуков») и типобезопасны по query-параметрам и ответу. Все возвращают стандартный UseQueryResult TanStack Query (data, isLoading, error, refetch, …).
import {
useAssets,
useAsset,
usePersonnel,
usePerson,
useBrigades,
useBrigade,
useBrigadeMembers,
useWatches,
useWatch,
useTimesheet,
useShiftReports,
useShiftReport,
useShiftReportEntries,
} from '@mineflow/client-react';
// EAM
const { data, isLoading, error } = useAssets({ status: 'operational', limit: 50 });
const { data: asset } = useAsset(id);
// HR
const { data: people } = usePersonnel({ limit: 100 });
const { data: person } = usePerson(personId); // не запустится при пустом id
const { data: members } = useBrigadeMembers(brigadeId);
const { data: timesheet } = useTimesheet({ dateFrom: '2026-06-01', dateTo: '2026-06-30' });
// PRD
const { data: reports } = useShiftReports({ shiftDateFrom: '2026-06-01', status: 'submitted' });
const { data: report } = useShiftReport(id); // parent: FSM-поля без line-items
const { data: entries } = useShiftReportEntries(id); // 7 коллекций line-items одним вызовом
Списки возвращают { items, nextCursor } (cursor-пагинация). Detail/sub-хуки рапорта, сотрудника, бри гады и вахты гейтятся enabled: id.length > 0 — не запускаются при пустом id.
useApiQueryAlluseTimesheet требует dateFrom/dateTo (обязательны на бэке). Серверный предел страницы — limit ≤ 200. Для пикеров на участке с >200 единицами есть useAssetsAll / usePersonnelAll — они сами гонят курсор до конца через useApiQueryAll, передавать нужно только фильтры (objectId/status), без cursor/limit.
Подробности cursor-пагинации — в рецепте «Пагинация».
Справочники (refs-kit)
Долгоживущие lookup'ы для picker'ов (ADR-0014), читаемые всеми ролями:
import { useProductionObjects, usePositions, useAssetClasses } from '@mineflow/client-react';
const { data: objects } = useProductionObjects(); // участки; activeOnly=true по умолчанию
const { data: positions } = usePositions(); // должности HR
const { data: classes } = useAssetClasses(); // классы техники EAM
Generic-хуки
Под любой эндпоинт, которого ещё нет в concrete-наборе. useApiQuery даёт механику (кэш, контекст), сам запрос типизируется через замыкание fn(client):
import { useApiQuery } from '@mineflow/client-react';
import { unwrap } from '@mineflow/client-core';
const q = useApiQuery(['eam', 'maintenance'], async (c) =>
unwrap(await c.GET('/api/v1/eam/maintenance', { params: { query: {} } })),
);
useApiQueryAll(queryKey, fetchPage, options?) — то же, но стягивает все страницы курсора в один массив (fetchPage(client, cursor) => { items, nextCursor }; maxPages — предохранитель от битого nextCursor).
3. Мутации (write)
Idempotency-Key проставляется автоматически на каждый write (его генерит fetch-слой из generateId) — руками передавать не нужно. Concrete-мутации на успехе инвалидируют связанный раздел кэша.
import {
useRegisterAsset,
useTransferAsset,
useDecommissionAsset,
useConserveAsset,
useReactivateAsset,
useCompleteMaintenance,
} from '@mineflow/client-react';
const register = useRegisterAsset();
register.mutate({ inventoryNumber: 'BG-001', name: '...', assetClassId, currentObjectId /* … */ });
const transfer = useTransferAsset();
transfer.mutate({ id, body: { toObjectId, reason } }); // reason — опционально
const decommission = useDecommissionAsset(); // синхронная (200), НЕ сага
decommission.mutate({ id, body: { coApprovedBy, reason } }); // double-approval
// FSM-переходы актива (ADR-0045): dedicated PATCH-эндпоинты, журнал движений пишется реактивно
useConserveAsset().mutate({ id, reason }); // operational → conserved (reason обязателен)
useReactivateAsset().mutate({ id }); // conserved → operational
useCompleteMaintenance().mutate({ id }); // maintenance → operational
HR user-accounts: local IAM onboarding
Создание HR-учётки теперь отделено от создания пользователя в Keycloak. useCreateUserAccount
синхронно создаёт HR.UserAccount и возвращает iamProvisioningStatus: 'pending'; worker
асинхронно создаёт local Keycloak user, назначает realm roles, пишет JWT-attributes и после
успеха переводит статус в provisioned.
import {
useCreateUserAccount,
useResetUserPassword,
useRetryIamProvisioning,
useUserAccount,
} from '@mineflow/client-react';
const create = useCreateUserAccount();
create.mutate({
personnelId,
login: 'ivanov',
systemRoleIds: [foremanRoleId],
// пароль на create не передаётся — первый пароль выдаётся reset-flow ниже.
});
const { data: account } = useUserAccount(userAccountId);
if (account?.iamProvisioningStatus === 'failed') {
useRetryIamProvisioning().mutate({ id: account.id });
}
if (account?.iamProvisioningStatus === 'provisioned') {
useResetUserPassword().mutate({ id: account.id });
}
useRetryIamProvisioning вызывает POST /hr/user-accounts/{id}/iam-provisioning/retry:
для pending/failed он очищает ошибку и повторно ставит provisioning в очередь, для
provisioned возвращает no-op, а для deactivated сервер отдаёт 422. useResetUserPassword
тоже отдаст понятный 422, если Keycloak user ещё не создан.
Generic-мутации
На дженериках построены все concrete write-хуки. useDomainMutation(invalidateKey, fn, options?) инвалидирует раздел на успехе:
import { useDomainMutation, queryKeys } from '@mineflow/client-react';
import { unwrap } from '@mineflow/client-core';
const m = useDomainMutation(queryKeys.personnel.all, async (c, vars: { id: string }) =>
unwrap(
await c.PATCH('/api/v1/hr/personnel/{id}/start-watch', {
params: { path: { id: vars.id }, header: { 'Idempotency-Key': '' } },
}),
),
);
Третий аргумент { mutationKey, scope } (DomainMutationOptions) делает запись offline-устойчивой — см. §5.
useApiMutation(fn) — то же без авто-инвалидации (когда сам управляешь кэ шем).
При write через generic-мутацию передавай header: { 'Idempotency-Key': '' } — это пустой плейсхолдер под required header-param OpenAPI; реальный ключ подставит fetch-слой (makeAuthenticatedFetch).
Карта кодов ошибок и гейтинг UI по error.code — в рецепте «Ошибки». Шаблон форм с Zod — в рецепте «Формы».
4. Сменные рапорты PRD: write-lifecycle
Полный цикл авторинга рапорта (ADR-0049): создать черновик → добавить строки 7 коллекций → подать/ отклонить. Все write-хуки рапорта offline-устойчивы (общий scope, стабильный Idempotency-Key, пауза вместо падения — детали в §5).
import {
useCreateShiftReport,
useAddShiftPersonnel,
useAddShiftAssetUsage,
useAddDrillingEntry,
useAddBlastingEntry,
useAddFuelEntry,
useAddDowntimeEvent,
useAddTmcUsage,
useSubmitShiftReport,
useRejectShiftReport,
} from '@mineflow/client-react';
// idempotencyKey г енерь при энкью и клади в vars — он переживёт рестарт/replay.
const create = useCreateShiftReport();
create.mutate({ body: { objectId, shiftDate, shift }, idempotencyKey });
const addPersonnel = useAddShiftPersonnel();
addPersonnel.mutate({ id: reportId, body: { personnelId, workedHours }, idempotencyKey });
const addAssetUsage = useAddShiftAssetUsage();
// возвращает id, на который ссылаются drilling/fuel (anchor)
const submit = useSubmitShiftReport(); // draft → submitted, тело пустое
submit.mutate({ id: reportId, idempotencyKey });
const reject = useRejectShiftReport(); // submitted → rejected (reason ≥5), без саги
reject.mutate({ id: reportId, body: { reason }, idempotencyKey });
Остальные add-хуки симметричны: useAddDrillingEntry, useAddBlastingEntry, useAddFuelEntry, useAddDowntimeEvent, useAddTmcUsage — все принимают { id, body, idempotencyKey } (AddEntryVars<B>).
approve и reject-after-approve — саги (поллят статус, требуют сети), см. §6.
drilling/fuel ссылаются на assetUsageId, возвращаемый useAddShiftAssetUsage, поэтому добавляй asset-usage до них. FIFO-replay offline-очереди сохраняет порядок энкью (см. §5).
5. Offline-first запись и outbox (ADR-0049)
Полевой пользователь работает без стабильной сети. Write-операции рапорта офлайн паузятся (не падают), очередь персистится и доигрывается строго по порядку при реконнекте/после рестарта. Вся механика собрана в объекте shiftReportDomain — инстанциация фабрики createOfflineDomain.
Три кита устойчивости:
- Стабильный
Idempotency-Keyвvars— переживает рестарт и переиспользуется на каждой попытке; бэк дедуплицирует повтор. - Общий
scope— TanStack сериализует мутации с однимscope.idстрого FIFO (create→add→submitне гоняются конкурентно). networkMode: 'online'(дефолт TanStack) — офлайн мутация паузится,mutationFnне вызывается; авто-resume поonlineManager.
Включение (внутри провайдера)
Нужен и QueryClient, и REST-клиент. registerDefaults восстанавливает mutation defaults, чтобы мутации, поднятые из персиста (у них только key + variables, без mutationFn), знали, как себя проиграть. Звать до resumePausedMutations:
import { shiftReportDomain, useMineflowClient } from '@mineflow/client-react';
import { useQueryClient } from '@tanstack/react-query';
import { useEffect } from 'react';
function OfflineGate({ children }) {
const queryClient = useQueryClient();
const client = useMineflowClient();
useEffect(() => {
shiftReportDomain.registerDefaults(queryClient, client);
}, [queryClient, client]);
// + persistQueryClient* — эталон в apps/mobile/src/app/providers/offline-gate.tsx
return children;
}
Индикатор и ретрай в UI
import { shiftReportDomain, useIsOnline } from '@mineflow/client-react';
function OutboxBanner() {
const online = useIsOnline();
const { paused, pending, error, total } = shiftReportDomain.useOutbox(); // сводка очереди
const retry = shiftReportDomain.useRetryFailed(); // повтор упавших (тот же Idempotency-Key)
if (online && total === 0) return null;
return <Banner onPress={error > 0 ? retry : undefined}>{/* … */}</Banner>;
}
useOutbox() возвращает OutboxStatus: { paused, pending, error, total }. useRetryFailed() re-execute'ит только упавшие (errored) мутации с прежними variables — resumePausedMutations поднимает только paused, поэтому errored нужен явный повтор.
createOfflineDomain({ scope, invalidateKey, mutationKeyPrefix, ops, mutationKeys }) вернёт такой же объект с registerDefaults / useOutbox / useRetryFailed плюс ops / mutationKeys / scope для live-хуков (как это сделано для рапортов поверх useDomainMutation).
Полный сценарий offline-очереди — в рецепте «Offline».
6. Async-саги
Часть POST'ов возвращает 202 + sagaId; хук поллит /sagas/{id}/status до терминального состояния и резолвится финальным SagaStatus (с steps[]).
import { useApproveShiftReport, useRejectShiftReportAfterApprove } from '@mineflow/client-react';
const approve = useApproveShiftReport(); // центральная 5-шаговая approve-сага
approve.mutate({ id });
// approve.isPending — пока идёт сага; approve.data — финальный SagaStatus
const reverse = useRejectShiftReportAfterApprove(); // approved → rejected (только CEO), reason ≥10
reverse.mutate({ id, body: { reason } });
onError, не onSuccessЕсли сага завершилась откатом (failed/compensated), это не успех. useSagaMutation бросает SagaFailedError (с полем .status: SagaStatus), поэтому сработает onError, а не onSuccess. Иначе UI показал бы «утверждено» по факту провалившейся саги. Чистый детектор — assertSagaSucceeded(status).
Своя сага
import { useSagaMutation } from '@mineflow/client-react';
import { unwrap } from '@mineflow/client-core';
const m = useSagaMutation(async (c, vars: { id: string }) => {
const res = unwrap(await c.POST('/api/v1/eam/sagas/asset-decommission', { /* ... */ }));
return { sagaId: res.sagaId };
});
Второй аргумент { pollOptions } пробрасывается в pollSaga (интервал/таймаут). Подробнее про polling и компенсацию — в рецепте «Саги».
7. Уведомления + presence (live через Centrifugo)
Live-доставку держит отдельный RealtimeProvider (одно WebSocket-соединение к Centrifugo на приложение), смонтированный внутри MineflowProvider:
import { RealtimeProvider } from '@mineflow/client-react';
<MineflowProvider {...props}>
<RealtimeProvider>
<App />
</RealtimeProvider>
</MineflowProvider>;
Дальше — хуки. useRealtimeNotifications префиксит входящие прямо в кэш useNotifications (дедуп по id), список перерисовывается сам:
import { useNotifications, useRealtimeNotifications, usePresence } from '@mineflow/client-react';
// backlog (REST, cursor-пагинация, новые сверху)
const { data } = useNotifications({ limit: 50 });
// live: новые префиксятся в кэш useNotifications (дедуп по id)
const { enabled } = useRealtimeNotifications({
onNotification: (n) => toast(`${n.severity}: ${n.title}`),
});
// presence: сколько коллег орг онлайн
const { online } = usePresence();
Тип уведомления (Notification) — { id, eventType, severity, title, body, targetRoles, targetUserId, resourceType, resourceId, createdAt }. Сервер сам фильтрует фид по организации и роли/пользователю (targeting — в именах каналов personal:/role:/presence:, клиент не выбирает каналы).
При enabled=false (CENTRIFUGO_ENABLED=0 на бэке или нет RealtimeProvider) хуки no-op'ят — live-доставки нет, лента живёт на useNotifications + pull-to-refresh. Завязывай live-индикаторы на возвращаемый enabled.
Полный сценарий realtime (подключение, каналы, presence, RN) — в рецепте «Realtime».
8. RBAC-гейт
import { useCan } from '@mineflow/client-react';
function ApproveButton() {
const canApprove = useCan(['CEO', 'Engineer']); // одна из ролей у пользователя?
return canApprove ? <button>Утвердить</button> : null;
}
useCan(required) проверяет, есть ли у текущего пользователя (roles из провайдера) хотя бы одна из требуемых ролей. Пустой массив = доступно любому аутентифицированному. Чистая (не-хук) версия — can(userRoles, required).
Бэк всё равно проверяет роли и object-scope и вернёт 403. useCan лишь прячет недоступные элементы интерфейса. Object-scope / multi-tenancy делает сервер по JWT — org_id в URL не передаётся.
Полная матрица ролей и паттерны object-scope — в рецепте «RBAC».
9. FSM-кнопки
import { useAvailableActions } from '@mineflow/client-react';
import { assetMachine } from '@mineflow/api-schemas';
const actions = useAvailableActions(assetMachine, asset.status);
// ['SEND_TO_MAINTENANCE', 'CONSERVE', 'DECOMMISSION'] — что показать кнопками
useAvailableActions(machine, stateValue) возвращает имена доступных переходов из текущего состояния без учёта guard'ов — UI должен предлагать действие, даже если оно требует доп. ввода (например DECOMMISSION с double-approval); бэк сам валидирует переход (409). Чистая версия — availableActions(machine, stateValue).
Машины (assetMachine и т.д.) — из @mineflow/api-schemas. Маппинг «событие FSM → write-хук» держат concrete-мутации (например CONSERVE → useConserveAsset), а не этот helper. Подробнее — в рецепте «FSM».
10. Формы
Валидация — Zod-схемами из @mineflow/api-zod + react-hook-form:
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { zCreateAssetDto } from '@mineflow/api-zod';
import { useRegisterAsset } from '@mineflow/client-react';
function RegisterAssetForm() {
const form = useForm({ resolver: zodResolver(zCreateAssetDto) });
const register = useRegisterAsset();
return <form onSubmit={form.handleSubmit((v) => register.mutate(v))}>{/* … */}</form>;
}
Полный шаблон форм (включая динамические entry-формы рапорта) — в рецепте «Формы».
11. Ключи кэша (queryKeys)
Единый источник ключей TanStack Query для всех хуков — для ручного invalidate/prefetch/setQueryData из приложения:
import { queryKeys } from '@mineflow/client-react';
queryKeys.assets.all; // ['eam','assets'] — префикс списка И детали
queryKeys.assets.list({ status: 'operational' });
queryKeys.assets.detail(id);
queryKeys.shiftReports.entries(id);
queryKeys.refs.assetClasses;
queryClient.invalidateQueries({ queryKey: queryKeys.assets.all }); // накрывает списки + детали
Доступные группы: assets, personnel, brigades, watches, timesheet, shiftReports, notifications, documents, refs. Инвариант (покрыт тестом): all — префикс list(...)/detail(...), поэтому инвалидация по all накрывает весь раздел. Тип — QueryKeys.
12. Документы и вложения (presigned)
Загрузка файлов (фото рапорта и т.д.) — трёхшаговый presigned-flow (ADR-0051): initiate → бинарный PUT напрямую в storage → confirm. Бинарный PUT платформо-специфичен (web — fetch+Blob, RN — expo-file-system), поэтому SDK даёт операции, а сам аплоад оркеструет приложение. Операции — documentOps.initiate / confirm / getDownloadUrl / listByEntity.
Для чтения есть готовые хуки:
import { useDocumentsByEntity, useDocumentDownloadUrl } from '@mineflow/client-react';
const { data: docs } = useDocumentsByEntity('shift-report', reportId); // кросс-девайс список
const { data } = useDocumentDownloadUrl(docId); // presigned GET URL для просмотра
Идемпотентность хэширует method:path:body, поэтому initiate и confirm (разные path) не могут делить один ключ — иначе второй вызов получит 409. documentOps генерят свежий ключ per-call. Полный сценарий аплоада — в рецепте «React Native».
Справочник ключевых экспортов
| Экспорт | Назначение |
|---|---|
MineflowProvider | корневой провайдер (строит REST + authFetch + QueryClient) |
useMineflow / useMineflowClient | доступ к контексту / REST-клиенту |
useApiQuery / useApiQueryAll | дженерик-чтение (одна страница / весь курсор) |
useApiMutation / useDomainMutation / useSagaMutation | дженерик-write / write+инвалидация / сага |
useAssets / useAsset / useAssetsAll | чтение EAM |
usePersonnel / usePerson / usePersonnelAll / useBrigades / useBrigade / useBrigadeMembers / useWatches / useWatch / useTimesheet | чтение HR |
useShiftReports / useShiftReport / useShiftReportEntries | чтение PRD |
useProductionObjects / usePositions / useAssetClasses | справочники (refs-kit) |
useRegisterAsset / useTransferAsset / useDecommissionAsset | мутации EAM |
useCreateUserAccount / useRetryIamProvisioning / useResetUserPassword | HR local onboarding: доменная учётка → async Keycloak provisioning → первый пароль |
useConserveAsset / useReactivateAsset / useCompleteMaintenance | FSM-переходы актива |
useCreateShiftReport / useSubmitShiftReport / useRejectShiftReport | write-lifecycle рапорта (offline-ready) |
useAddShiftPersonnel / useAddShiftAssetUsage / useAddDrillingEntry / useAddBlastingEntry / useAddFuelEntry / useAddDowntimeEvent / useAddTmcUsage | строки рапорта (offline-ready) |
useApproveShiftReport / useRejectShiftReportAfterApprove | approve / сторно-саги PRD |
shiftReportDomain / createOfflineDomain / useIsOnline | offline-домен (replay/outbox/retry) + сеть |
useNotifications (backlog) / RealtimeProvider · useRealtimeNotifications · useOnNotification · usePresence (live Centrifugo) | уведомления + presence |
useCan / can | RBAC-гейт |
useAvailableActions / availableActions | FSM-действия |
useDocumentsByEntity / useDocumentDownloadUrl / documentOps | документы/вложения (presigned) |
queryKeys | ключи кэша для ручного invalidate/prefetch |
SagaFailedError / assertSagaSucceeded | детектор терминального отказа саги |
Полный список (включая типы вроде Notification, CreateShiftReportBody, OutboxStatus, MineflowProviderProps) — в полном API-референсе из кода → /api/client-react/.
Кодген хуков (для мейнтейнеров)
Тривиальные read-хуки (list/detail/sub) не пишутся руками — генерятся из apps/api/openapi.json:
pnpm gen:hooks # standalone; также входит в pnpm openapi:client
Раскладка src/domain-hooks/:
<area>.generated.ts— AUTO-GENERATED тривиальные read'ы (eam/hr/prd);<area>.ts— рукописные write/саги/offline; ре-экспортит свой.generated;refs.ts— справочники (bespoke-ключи, рукописные);index.ts— barrel области.
Чтобы добавить read-хуки нового агрегата (например scm/fuel): строка в манифесте HOOKS в tools/scripts/gen-domain-hooks.ts, группа ключей в src/query-keys.ts, затем pnpm gen:hooks. Сложные хуки (write с инвалидацией, саги, offline) пишутся руками в <area>.ts на дженериках useDomainMutation/useSagaMutation.
Генератор сверяет каждый путь с живой OpenAPI-спекой — переименование/удаление эндпоинта на бэке приводит к падению pnpm gen:hooks (fail-loud, без частичной записи), а не молча выдаёт битый хук. Тот же PR валит CI-гейт.
Подробнее про генерацию типов и хуков — в рецепте «Codegen».
React Native
Хуки platform-agnostic — TanStack Query работает в RN без изменений. Платформо-специфичное инъектируется через провайдер:
tokenProvider—ReactNativeTokenProviderиз@mineflow/auth-native(react-native-app-auth);generateId— polyfillreact-native-get-random-values;fetchImpl— опционален; нужен только если хочешь подменить REST-fetch. Для realtime ничего не требуется — Centrifugo использует нативный WebSocket.
Эталон offline-оркестрации (registerDefaults + persistQueryClient*) — apps/mobile/src/app/providers/offline-gate.tsx. UI пишется на RN-примитивах. Полный гайд по RN-интеграции (включая бинарный аплоад документов) — в рецепте «React Native».
Ссылки
- Полный API-референс из кода → /api/client-react/
- REST-референс (OpenAPI/Redoc) → /rest/
- Нижние слои:
@mineflow/client-core·@mineflow/api-client·@mineflow/api-zod·@mineflow/api-schemas - Auth:
@mineflow/auth-web·@mineflow/auth-native - Рецепты: Ошибки · Саги · Realtime · Offline · RBAC · FSM · Формы · Пагинация · React Native · Codegen