-
Notifications
You must be signed in to change notification settings - Fork 0
feat(74): add chained function #77
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Changes from 3 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
|
|
||
| ### Classic signature | ||
|
|
||
| ```typescript | ||
| function chainedFunction( | ||
| function1: [name: string, fn: Function], | ||
| function2: [name: string, fn: Function], | ||
| ...functions: [name: string, fn: Function][] | ||
| ): ChainedFunction | ||
| ``` | ||
|
|
||
| ### Implementation signature | ||
|
|
||
| ```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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
104 changes: 104 additions & 0 deletions
104
docs/examples/v1/api/clean/chainedFunction/tryout.doc.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" | ||
| >; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 classique | ||
|
|
||
| ```typescript | ||
| function chainedFunction( | ||
| function1: [name: string, fn: Function], | ||
| function2: [name: string, fn: Function], | ||
| ...functions: [name: string, fn: Function][] | ||
| ): ChainedFunction | ||
| ``` | ||
|
|
||
| ### Signature d'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. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.