# Yoonite Saga

> Orchestration de workflows transactionnels avec gestion de compensation (pattern Saga)

Yoonite Saga est une librairie TypeScript/Node.js permettant de définir et d'exécuter des workflows transactionnels complexes, inspirée du pattern Saga. Elle facilite la gestion d'enchaînements d'étapes, la validation, les conditions d'exécution, la compensation (rollback) en cas d'erreur, et l'injection de services. Idéale pour les architectures distribuées ou les processus métier nécessitant robustesse et traçabilité.

---

## Installation

```bash
npm i yoonite-saga
```

---

## Concepts clés

- **Step** : Une étape du workflow, pouvant contenir plusieurs actions (`invoke`), une validation, une condition d'exécution, et une compensation.
- **Invoke** : Une action à exécuter dans une étape. Peut être asynchrone et enrichir le contexte d'exécution.
- **Compensation** : Fonction de rollback exécutée si une erreur survient dans le workflow.
- **Condition** : Fonction permettant de conditionner l'exécution d'une étape ou d'une action.
- **Context** : Objet partagé et enrichi tout au long du workflow.
- **Services** : Objets injectés pour être utilisés dans les actions du workflow.

---

## Exemple simple

```typescript
import { SagaBuilder, SagaProcessor } from "yoonite-saga";

const builder = new SagaBuilder({ debug: true });

const workflow = builder
  .step("Initialisation")
  .invoke(() => ({ userId: "abc" }))

  .step("Traitement")
  .invoke(({ userId }) => {
    console.log(`Traitement pour l'utilisateur ${userId}`);
  })
  .build();

const processor = new SagaProcessor();
processor.add(workflow);
const response = await processor.start();
console.log(response);
```

---

## Utilisation avancée

### 1. Enrichir le contexte

Chaque `invoke` peut retourner un objet qui enrichit le contexte pour les étapes suivantes :

```typescript
.step("Créer le compte")
.invoke(() => ({ accountId: "123" }))

.step("Afficher le compte")
.invoke(({ accountId }) => {
  console.log(`Compte créé : ${accountId}`);
})
```

### 2. Actions asynchrones

```typescript
.step("Récupérer les infos")
.invoke(async ({ accountId }) => {
  const infos = await api.get(`/accounts/${accountId}`);
  return { infos };
})
```

### 3. Plusieurs invokes par étape

```typescript
.step("Récupérer les animaux")
.invoke("Chats", async ({ accountId }) => {
  const cats = await api.get(`/cats/${accountId}`);
  return { cats };
})
.invoke("Chiens", async ({ accountId }) => {
  const dogs = await api.get(`/dogs/${accountId}`);
  return { dogs };
});
```

### 4. Conditions sur les étapes

```typescript
.step("Vacciner le chien")
.condition(({ dogs }) => dogs && dogs.length > 0)
.invoke("Vaccins", async ({ dogs }) => {
  const { dogId } = dogs[0];
  const vaccines = await api.get(`/vaccines/${dogId}`);
  return { vaccines };
})
```

### 5. Conditions sur les invokes

```typescript
.step("Récupérer les animaux")
.invoke("Chats", {
  condition: ({ hasCats }) => hasCats,
  action: async ({ accountId }) => {
    const cats = await api.get(`/cats/${accountId}`);
    return { cats };
  }
})
.invoke("Chiens", {
  condition: ({ hasDogs }) => hasDogs,
  action: async ({ accountId }) => {
    const dogs = await api.get(`/dogs/${accountId}`);
    return { dogs };
  }
})
```

### 6. Gestion des erreurs et compensation

Si une erreur est levée, la saga s'arrête et exécute les compensations définies sur les étapes déjà passées :

```typescript
.step("Créer la commande")
.invoke(async () => {
  const orderId = await api.post(`/orders`, { amount: 100 });
  return { orderId };
})
.withCompensation(async ({ orderId }) => {
  await api.delete(`/orders/${orderId}`);
})

.step("Expédition")
.invoke(() => {
  throw new Error("Erreur d'expédition");
})
```

On peut aussi définir une compensation sur un `invoke` :

```typescript
.step("Créer la commande")
.invoke({
  action: async () => {
    const orderId = await api.post(`/orders`, { amount: 100 });
    return { orderId };
  },
  withCompensation: async ({ orderId }) => {
    await api.delete(`/orders/${orderId}`);
  }
})
```

### 7. Validation de données

Vous pouvez valider le contexte à chaque étape avec un DTO compatible `class-validator` :

```typescript
import { IsString } from "class-validator";

class MyDto {
  @IsString()
  accountId: string;
}

.step("Créer le compte")
.invoke(() => ({ accountId: "123" }))
.validate(MyDto)
```

---

## Résultat de l'exécution

Le résultat retourné par le processor contient :

```typescript
{
  state: "success" | "failed",
  context: { ... }, // Contexte final
  history: any[],   // Historique détaillé de l'exécution
  errors: Error[],  // Liste des erreurs rencontrées
}
```

---

## Injection de services

Vous pouvez injecter des services (ex: logger, clients API, etc.) à utiliser dans vos actions :

```typescript
const processor = new SagaProcessor({
  myLogger: customLogger,
  api: myApiClient,
});

.step("Log")
.invoke((ctx, { myLogger }) => {
  myLogger.log("Hello saga!");
})
```

---

## Pourquoi utiliser Yoonite Saga ?

- Orchestration simple et lisible de workflows transactionnels
- Gestion automatique des erreurs et rollback (compensation)
- Validation, conditions, et enrichissement du contexte
- Historique d'exécution pour audit/debug
- Injection de services pour actions personnalisées

---

## Licence

MIT
