@mineflow/contracts
@mineflow/contracts — единый источник правды (SSoT) для доменных событий MineFlow: Zod-схемы payload каждого события, реестр всех событий (eventCatalog) и схема обще го конверта (eventEnvelopeSchema). Тот же пакет, из которого backend публикует события в outbox/Redis Streams, фронтенд использует для типизации event-нагрузок (интеграционные/backend-консьюмеры).
В слоистой архитектуре SDK (ADR-0042) это shared-пакет уровня контракта: он не делает запросов и не рендерит UI — он описывает форму данных, которые ходят по событийной шине. Для REST-формы данных есть отдельный слой L0 (@mineflow/api-client / @mineflow/api-zod); contracts отвечает строго за события, не за HTTP-ответы.
Источник истины — именно Zod-схемы в этом пакете. Документ AsyncAPI 3.1.0 (docs/asyncapi.json) генерируется из eventCatalog + eventEnvelopeSchema через z.toJSONSchema() — это машинно-читаемая проекция контракта, а не параллельный источник. См. функцию buildAsyncApiDocument и ADR-0013.
Установка
pnpm add @mineflow/contracts
# peer-зависимость (используется как обычный import { z } из схем):
pnpm add zod
В монорепо MineFlow пакет резолвится как workspace:*. Рантайм-зависимость у пакета одна — zod.
Схемы (eventCatalog, *Schema) — это runtime-значения Zod. Если фронту нужна только типизация event-нагрузки, импортируй type-символы (EventName, EventDataByName, ShiftReportApproved, …) — это zero-runtime и ничего не тянет в бандл. Полноценные схемы держи на серверной стороне (валидация при publish/consume).
Конверт события (envelope)
Все события MineFlow едут в одном конверте — eventEnvelopeSchema (ADR-0013). Полезная нагрузка лежит в поле data, метаданные — снаружи:
import type { EventEnvelope } from '@mineflow/contracts';
// EventEnvelope:
// {
// id: string; // UUID события
// type: string; // "<context>.<entity>.<verb-past>", напр. "eam.asset.transferred"
// version: string; // "major.minor" схемы data, напр. "1.0"
// occurredAt: string; // ISO 8601
// producedBy: string; // имя продюсера
// correlationId?: string; // UUID, опционально
// causationId?: string; // UUID, опционально
// organizationId: string; // владелец события (ADR-0020 multi-tenancy)
// data: unknown; // payload — типизируется по type через eventCatalog
// }
data в самом конверте типизирован как unknown намеренно: конкретная форма зависит от type. Сузить её до конкретного события помогает eventCatalog (см. ниже).
Поле organizationId есть в каждом событии (ADR-0020), но фронт его не задаёт и не фильтрует руками — multi-tenancy изоляцию делает backend по JWT. На клиенте конверт приходит уже отскоупленным под организацию пользователя.
Каталог событий
eventCatalog — это as const-реестр всех известных событий: ключ — имя события (<context>.<entity>.<verb-past>), значение — { version, schema }. Из него выводятся два полезных типа:
EventName— объединение всех имён событий ('eam.asset.transferred' | 'prd.shift-report.approved' | …);EventDataByName<N>— типdataдля конкретного имени (черезz.inferего схемы).
import type { EventName, EventDataByName } from '@mineflow/contracts';
type AssetTransferred = EventDataByName<'eam.asset.transferred'>;
// { assetId: string; fromObjectId: string; toObjectId: string; performedBy: string; reason?: string }
type ApprovedData = EventDataByName<'prd.shift-report.approved'>;
// { reportId; organizationId; productionObjectId; shiftDate; shiftType; approvedBy;
// approvedAt; approveSagaId; actorId; summary: { drilledMeters; blastedBlocks; … } }
type IamProvisioningRequested =
EventDataByName<'hr.user-account.iam-provisioning-requested'>;
// { userAccountId; personnelId; login; actorId }
Конвенция имён жёсткая: <context>.<entity>.<verb-past> — eam.asset.created, prd.shift-report.rejected-after-approve, scm.fuel.* и т.д. Полный перечень имён — в API-референсе (тип EventName) либо прямо в eventCatalog.
Использование на фронтенде: типизация event-нагрузок
Контракты дают форму data каждого доменного события без догадок — сузив EventEnvelope по type, получаешь точный тип полей:
import type { EventEnvelope, EventName, EventDataByName } from '@mineflow/contracts';
// Узкоспециализированный конверт под конкретное имя события
type TypedEnvelope<N extends EventName> = Omit<EventEnvelope, 'type' | 'data'> & {
type: N;
data: EventDataByName<N>;
};
function handle(evt: EventEnvelope): void {
if (evt.type === 'prd.shift-report.approved') {
const data = evt.data as EventDataByName<'prd.shift-report.approved'>;
console.log('утверждён рапорт', data.reportId, data.summary.drilledMeters);
}
}
Это пригодится, когда фронт/BFF/edge принимает доменные события из интеграционного канала (см. раздел ниже) — форма та же, что у backend-консьюмеров.
Для live-уведомлений в UI не нужно работать с EventEnvelope вручную: их доставляет Centrifugo, а @mineflow/client-react даёт готовые хуки (useRealtimeNotifications / useOnNotification / usePresence) с уже типизированным Notification. Сырые контракты событий нужны для другого — типизации backend/интеграционных консьюмеров доменных событий. Подробности realtime — рецепт «Realtime».
hr.user-account.iam-provisioning-requested публикуется retry endpoint-ом
POST /hr/user-accounts/{id}/iam-provisioning/retry. Его consumer в IAM worker
идемпотентно создаёт или переиспользует local Keycloak user, синхронизирует
MineFlow realm roles и пишет JWT-attributes. Фронтенд обычно работает с этим
через useRetryIamProvisioning; contracts нужны интеграционным consumer-ам и
тестам событий.
Использование: типизация интеграций
Если фронт (или BFF/edge-функция) принимает события из внешнего интеграционного канала, contracts даёт ровно тот же контракт, что и у backend-консьюмеров — без дублирования форм руками. Сузив EventEnvelope по type, можно строить дискриминируемые обработчики:
import type { EventEnvelope, EventDataByName } from '@mineflow/contracts';
function handle(evt: EventEnvelope): void {
switch (evt.type) {
case 'eam.asset.created':
onAssetCreated(evt.data as EventDataByName<'eam.asset.created'>);
break;
case 'prd.shift-report.rejected':
onReportRejected(evt.data as EventDataByName<'prd.shift-report.rejected'>);
break;
default:
// неизвестное/неинтересное событие — игнор
break;
}
}