Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions docs/en/v1/api/clean/chainedFunction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
---
outline: [2, 3]
description: "chainedFunction declares a typed aggregate of pure business actions that must run in order. The use case then orchestrates repositories around that aggregate."
prev:
text: "UseCase"
link: "/en/v1/api/clean/useCase"
next:
text: "Clean"
link: "/en/v1/api/clean/"
---

# chainedFunction

`chainedFunction` solves a Clean Architecture coordination problem: inside a use case, you sometimes need to associate operations that update different entities. The chained function represents the aggregate that makes those operations part of the same business consistency boundary.

It lets the domain explicitly declare that several pure business actions are linked. Functions passed to `chainedFunction` do not inject dependencies and do not call repositories: they only control entity lifecycle from the business point of view. The use case then orchestrates the aggregate, repositories, and technical effects.

## Interactive example

<MonacoTSEditor
src="/examples/v1/api/clean/chainedFunction/tryout.doc.ts"
majorVersion="v1"
height="1910px"
:foldLines="[2, 7, 13, 18, 22, 87]"
/>

## Why use it?

Use `chainedFunction` when a business aggregate is only consistent if several named operations happen together.

For example, publishing a comment can require:

- producing a valid comment entity;
- producing an updated article entity.

Persistence stays in the use case through the library repository system. The chained function only models the aggregate contract: "creating the comment" and "incrementing the article comment count" are linked business actions.

## Guarantees

`chainedFunction` provides these guarantees at the type level:

- links are exposed one after another, in declaration order;
- the implementation cannot access a later link before calling the current one;
- the success path must end with `chainEnd(...)`;
- a chained function can stop the flow by returning an `Either.Left`;
- the implementation can also return an `Either.Left` directly.
- chained functions remain pure domain functions.

## Syntax

### Signature

```typescript
function chainedFunction(
function1: [name: string, fn: Function],
function2: [name: string, fn: Function],
...functions: [name: string, fn: Function][]
): ChainedFunction
```

### Implementation

```typescript
const aggregate = chainedFunction(...functions);

const result = aggregate(function *(firstLink, { breakIfLeft }) {
const [value, nextLink] = yield *firstLink(({ functionName }) => functionName(...args));

return chainEnd(value);
});
```

## Parameters

- `function1`: first pure business function in the chain.
- `function2`: second pure business function in the chain.
- `functions`: additional pure business functions, executed in declaration order.
- `firstLink`: generated first link passed to the implementation callback.
- `breakIfLeft`: synchronous helper injected into the callback. It accepts a value that may contain an `Either.Left`, stops the chain when it is a `Left`, otherwise returns the discriminated non-`Left` value.

## Return value

`chainedFunction(...)` returns a chained aggregate function. Calling it returns:

- the raw value passed to `chainEnd(value)` on the success path;
- the `Either.Left` returned by a chained function;
- the `Either.Left` returned directly by the implementation.

## Error flow

When a chained function returns an `Either.Left`, the generator yields it and `chainedFunction` stops the implementation before the next links run. Business errors should be represented as `Either.Left`; thrown exceptions and rejected promises are not caught.

`breakIfLeft` follows the same rule from inside the callback: use it to explicitly short-circuit from an intermediate synchronous value (`value | Left`) before calling the next link.

## See also

- [`useCase`](/en/v1/api/clean/useCase) - Calls application logic with dependencies.
- [`repository`](/en/v1/api/clean/repository) - Declares a repository contract.
- [`Either`](/en/v1/api/either/) - Represents explicit success and error values.
3 changes: 3 additions & 0 deletions docs/en/v1/api/clean/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ Declares a repository (contract) and type-checks the implementation.
## [UseCase](/en/v1/api/clean/useCase)
Declares a use case with dependencies (repositories or other use cases).

## [chainedFunction](/en/v1/api/clean/chainedFunction)
Declares an aggregate of linked business actions that must run in order.

## Operations on primitives

### [equal](/en/v1/api/clean/primitives/operators/equal)
Expand Down
4 changes: 2 additions & 2 deletions docs/en/v1/api/clean/useCase.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ prev:
text: "Repository"
link: "/en/v1/api/clean/repository"
next:
text: "Clean"
link: "/en/v1/api/clean/"
text: "chainedFunction"
link: "/en/v1/api/clean/chainedFunction"
---

# UseCase
Expand Down
104 changes: 104 additions & 0 deletions docs/examples/v1/api/clean/chainedFunction/tryout.doc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { C, E, type ExpectType } from "@duplojs/utils";

interface CommentDraft {
articleId: number;
content: string;
}

interface Comment {
id: number;
articleId: number;
content: string;
}

interface Article {
id: number;
commentCount: number;
}

interface CommentRepository {
save(comment: Comment): Comment | E.Fail;
}

interface ArticleRepository {
findById(articleId: number): Article | E.Fail;
save(article: Article): Article | E.Fail;
}

const CommentRepository = C.createRepository<CommentRepository>();
const ArticleRepository = C.createRepository<ArticleRepository>();

const CommentPublicationAggregate = C.chainedFunction(
[
"createComment",
(draft: CommentDraft) => draft.content.trim()
? {
id: 1,
articleId: draft.articleId,
content: draft.content.trim(),
}
: E.fail(),
],
[
"incrementArticleCommentCount",
(article: Article): Article => ({
...article,
commentCount: article.commentCount + 1,
}),
],
);

const PublishCommentUseCase = C.createUseCase(
{
CommentRepository,
ArticleRepository,
},
({
commentRepository,
articleRepository,
}) => (draft: CommentDraft) => CommentPublicationAggregate(function *(link1, { breakIfLeft }) {
const [comment, link2] = yield *link1(({ createComment }) => createComment(draft));

const savedComment = yield *breakIfLeft(commentRepository.save(comment));

const article = yield *breakIfLeft(articleRepository.findById(savedComment.articleId));

const [updatedArticle, chainEnd] = yield *link2(
({ incrementArticleCommentCount }) => incrementArticleCommentCount(article),
);

const savedArticle = yield *breakIfLeft(articleRepository.save(updatedArticle));

return chainEnd({
comment: savedComment,
article: savedArticle,
});
}),
);

const publishComment = PublishCommentUseCase.getUseCase({
commentRepository: CommentRepository.createImplementation({
save: (comment) => comment,
}),
articleRepository: ArticleRepository.createImplementation({
findById: (articleId) => ({
id: articleId,
commentCount: 11,
}),
save: (article) => article,
}),
});

const publishedComment = publishComment({
articleId: 12,
content: " New comment ",
});

type CheckPublishedComment = ExpectType<
typeof publishedComment,
{
comment: Comment;
article: Article;
} | E.Fail,
"strict"
>;
100 changes: 100 additions & 0 deletions docs/fr/v1/api/clean/chainedFunction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
---
outline: [2, 3]
description: "chainedFunction déclare un agrégat typé d'actions métier pures qui doivent s'exécuter dans l'ordre. Le use case orchestre ensuite les repositories autour de cet agrégat."
prev:
text: "UseCase"
link: "/fr/v1/api/clean/useCase"
next:
text: "Clean"
link: "/fr/v1/api/clean/"
---

# chainedFunction

`chainedFunction` répond à un problème de coordination en Clean Architecture : dans un use case, on doit parfois associer des opérations qui mettent à jour des entités différentes. La chained function représente alors l'agrégat qui rend ces opérations solidaires dans une même frontière de cohérence métier.

Elle permet au domaine de déclarer explicitement que plusieurs actions métier pures sont liées. Les fonctions passées à `chainedFunction` ne font pas d'injection de dépendance et n'appellent pas de repository : elles contrôlent uniquement le cycle de vie des entités. Le use case orchestre ensuite l'agrégat, les repositories et les effets techniques.

## Exemple interactif

<MonacoTSEditor
src="/examples/v1/api/clean/chainedFunction/tryout.doc.ts"
majorVersion="v1"
height="1910px"
:foldLines="[2, 7, 13, 18, 22, 87]"
/>

## Pourquoi l'utiliser ?

Utilisez `chainedFunction` quand un agrégat métier n'est cohérent que si plusieurs opérations nommées se produisent ensemble.

Par exemple, publier un commentaire peut demander :

- de créer l'entité commentaire ;
- de produire une entité commentaire valide ;
- de produire une entité article mise à jour.

La persistance reste dans le use case via le système de repository de la librairie. La chained function modélise seulement le contrat d'agrégat : "créer le commentaire" et "incrémenter le nombre de commentaires de l'article" sont des actions métier liées.

## Garanties

`chainedFunction` apporte ces garanties au niveau du typage :

- les liens sont exposés un par un, dans l'ordre de déclaration ;
- l'implémentation ne peut pas accéder à un lien suivant avant d'avoir appelé le lien courant ;
- le chemin de succès doit terminer avec `chainEnd(...)` ;
- une fonction chaînée peut arrêter le flux en retournant un `Either.Left` ;
- l'implémentation peut aussi retourner directement un `Either.Left`.
- les fonctions chaînées restent des fonctions de domaine pures.

## Syntaxe

### Signature

```typescript
function chainedFunction(
function1: [name: string, fn: Function],
function2: [name: string, fn: Function],
...functions: [name: string, fn: Function][]
): ChainedFunction
```

### Implémentation

```typescript
const aggregate = chainedFunction(...functions);

const result = aggregate(function *(firstLink, { breakIfLeft }) {
const [value, nextLink] = yield *firstLink(({ functionName }) => functionName(...args));

return chainEnd(value);
});
```

## Paramètres

- `function1` : première fonction métier pure de la chaîne.
- `function2` : deuxième fonction métier pure de la chaîne.
- `functions` : fonctions métier pures supplémentaires, exécutées dans l'ordre de déclaration.
- `firstLink` : premier link généré et passé au callback d'implémentation.
- `breakIfLeft` : helper synchrone injecté dans le callback. Il accepte une valeur potentiellement `Either.Left`, stoppe la chaîne si c'est un `Left`, sinon retourne la valeur discriminée sans le `Left`.

## Valeur de retour

`chainedFunction(...)` retourne une fonction d'agrégat chaîné. Son appel retourne :

- la valeur brute passée à `chainEnd(value)` sur le chemin de succès ;
- le `Either.Left` retourné par une fonction chaînée ;
- le `Either.Left` retourné directement par l'implémentation.

## Flux d'erreur

Quand une fonction chaînée retourne un `Either.Left`, le générateur le yield et `chainedFunction` arrête l'implémentation avant d'exécuter les links suivants. Les erreurs métier doivent être représentées avec `Either.Left` ; les exceptions lancées et les promesses rejetées ne sont pas interceptées.

`breakIfLeft` suit la même règle, mais côté callback : il sert à court-circuiter explicitement le flux à partir d'une valeur intermédiaire synchrone (`value | Left`) avant d'appeler le link suivant.

## Voir aussi

- [`useCase`](/fr/v1/api/clean/useCase) - Appelle une logique applicative avec dépendances.
- [`repository`](/fr/v1/api/clean/repository) - Déclare un contrat de repository.
- [`Either`](/fr/v1/api/either/) - Représente des valeurs explicites de succès et d'erreur.
3 changes: 3 additions & 0 deletions docs/fr/v1/api/clean/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ Déclare un repository (contrat) et type-check l'implémentation.
## [UseCase](/fr/v1/api/clean/useCase)
Déclare un use case avec des dépendances (repositories ou autres use cases).

## [chainedFunction](/fr/v1/api/clean/chainedFunction)
Déclare un agrégat d'actions métier liées qui doivent s'exécuter dans l'ordre.

## Opérations sur primitives

### [equal](/fr/v1/api/clean/primitives/operators/equal)
Expand Down
4 changes: 2 additions & 2 deletions docs/fr/v1/api/clean/useCase.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ prev:
text: "Repository"
link: "/fr/v1/api/clean/repository"
next:
text: "Clean"
link: "/fr/v1/api/clean/"
text: "chainedFunction"
link: "/fr/v1/api/clean/chainedFunction"
---

# UseCase
Expand Down
Loading
Loading