ceo-api/src/sagas/sagas.controller.ts
valentinbvro 765ec37c3a feat: Saga Manager (Sprint 4-6)
Motor generic pentru procese cu mai multi pasi care nu incap intr-o
tranzactie. Garantia nu e atomicitatea, ci compensarea in ordine inversa.

- saga-registry: definitii versionate in cod, backoff exponential plafonat
- saga-runner: corelator outbox->instanta, step runner, timeout, retry,
  compensare; revendicare cu FOR UPDATE SKIP LOCKED ca doua procese sa nu
  avanseze aceeasi saga simultan
- idempotenta la pornire prin index unic (saga, versiune, trigger_event),
  nu prin SELECT-apoi-INSERT care ar avea race
- compensarea are propriul retry: o compensare esuata lasa sistemul mai rau
  decat esecul original
- monitor /v1/sagas cu instante blocate si retry manual care NU sare pasi
- prima saga reala: imbogatire research brief, fara pas AI (un apel AI
  automat per brief ar schimba profilul de cost)
2026-07-29 15:02:56 +02:00

40 lines
1.3 KiB
TypeScript

import { Controller, Get, Param, ParseUUIDPipe, Post, Query } from '@nestjs/common';
import { CurrentSession } from '../auth/session.decorator';
import type { AuthenticatedSession } from '../auth/tenant.guard';
import { SagasService } from './sagas.service';
const STATUSES = [
'RUNNING',
'COMPLETED',
'COMPENSATING',
'COMPENSATED',
'FAILED',
'TIMED_OUT',
] as const;
@Controller('sagas')
export class SagasController {
constructor(private readonly sagas: SagasService) {}
@Get()
overview(@CurrentSession() session: AuthenticatedSession) {
return this.sagas.overview(session);
}
@Get('instances')
list(@CurrentSession() session: AuthenticatedSession, @Query('status') status?: string) {
// Allowlist, nu interpolare: statusul ajunge intr-o comparatie SQL.
const safe = STATUSES.includes(status as (typeof STATUSES)[number]) ? status : undefined;
return this.sagas.list(session, safe);
}
@Get('instances/:id')
detail(@CurrentSession() session: AuthenticatedSession, @Param('id', ParseUUIDPipe) id: string) {
return this.sagas.detail(session, id);
}
@Post('instances/:id/retry')
retry(@CurrentSession() session: AuthenticatedSession, @Param('id', ParseUUIDPipe) id: string) {
return this.sagas.retry(session, id);
}
}