From 3e164ed8ee63b082359453b897a2779941e03d79 Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Sun, 11 Feb 2024 14:53:33 +0100 Subject: [PATCH] Added API docs for commands, command handling and event store --- README.md | 4 +- docs/api-docs.md | 82 +++++++++++++++++++ docs/getting-started.md | 4 +- docs/snippets/api/command.ts | 21 +++++ src/commandHandling/handleCommand.ts | 2 + .../handleCommandWithDecider.ts | 2 + src/eventStore/eventStore.ts | 2 + 7 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 docs/snippets/api/command.ts diff --git a/README.md b/README.md index a309d998..18a96ea8 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![](./docs/public/logo.png) -# Emmett - vent Sourcing development made simple +# Emmett - Event Sourcing development made simple Nowadays, storage is cheap, but the information is priceless. @@ -26,7 +26,7 @@ Maybe. I like its minimalistic approach and flexibility, plus TypeScript is an excellent language with its shapeshifter capabilities. Plus, I've been asked if I could deliver such a store for Node.js. -### Why Emmeett? +### Why Emmett? [Because](https://en.m.wikipedia.org/wiki/Emmett_Brown). diff --git a/docs/api-docs.md b/docs/api-docs.md index 9fa10963..3d17e330 100644 --- a/docs/api-docs.md +++ b/docs/api-docs.md @@ -23,3 +23,85 @@ The type is a simple wrapper to ensure the structure's correctness. It defines: See more context in [getting started guide](./getting-started.md#events) <<< @./../src/typing/event.ts + +## Command + +**Commands represent intention to perform business operation.** It targets a specific _audience_. It can be an application service and request with intention to “add user” or “change the order status to confirmed”. So the sender of the command must know the recipient and expects the request to be executed. Of course, the recipient may refuse to do it by not passing us the salt or throwing an exception during the request handling. + +Command type helps to keep the command definition aligned. It's not a must, but it helps to ensure that it has a type name defined (e.g. `AddProductItemToShoppingCart`) and read-only payload data. + +You can use it as follows + +<<< @/snippets/api/command.ts#command-type + +The type is a simple wrapper to ensure the structure's correctness. It defines: + +- **type** - command type name, +- **data** - represents the business data the command contains. It has to be a record structure; primitives are not allowed, +- **metadata** - represents the generic data command contains. It can represent telemetry, user id, tenant id, timestamps and other information that can be useful for running infrastructure. It has to be a record structure; primitives are not allowed. + +See more context in [getting started guide](./getting-started.md#commands) + +<<< @./../src/typing/command.ts + +## Event Store + +Emmett assumes the following event store structure: + +<<< @./../src/eventStore/eventStore.ts#event-store + +## Command Handler + +Emmett provides the composition around the business logic. + +Using simple functions: + +<<< @./../src/commandHandling/handleCommand.ts#command-handler + +Using decider: + +<<< @./../src/commandHandling/handleCommandWithDecider.ts#command-handler + +You can define it for you code as: + +```typescript +const handleCommand = CommandHandler< + ShoppingCart, + ShoppingCartCommand, + ShoppingCartEvent +>(getEventStore, toShoppingCartStreamId, decider); +``` + +And call it as (using [Express.js](https://expressjs.com/) api): + +```typescript +router.post( + '/clients/:clientId/shopping-carts/:shoppingCartId/product-items', + on(async (request: AddProductItemToShoppingCartRequest, handle) => { + const shoppingCartId = assertNotEmptyString(request.params.shoppingCartId); + + const productId = assertNotEmptyString(request.body.productId); + const quantity = assertPositiveNumber(request.body.quantity); + + const price = await getProductPrice(productId); + + return handle(shoppingCartId, { + type: 'AddProductItemToShoppingCart', + data: { + shoppingCartId, + productItem: { + productId, + quantity, + price, + }, + }, + }); + }), +); + +type AddProductItemToShoppingCartRequest = Request< + Partial<{ shoppingCartId: string }>, + unknown, + Partial<{ productId: number; quantity: number }> +>; +``` diff --git a/docs/getting-started.md b/docs/getting-started.md index d0e4f702..f802ece5 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,5 +1,7 @@ # Getting Started +![](/logo.png) + ## Event Sourcing **Event Sourcing keeps all the facts that happened in our system, and that's powerful!** Facts are stored as events that can be used to make decisions, fine-tune read models, integrate our systems, and enhance our analytics and tracking. All in one package, wash and go! @@ -114,7 +116,7 @@ Now let's define the `evolve` function that will evolve our state based on event One of the mentioned benefits is testing, which Emmett helps to do out of the box. -::: info For Event Sourcing, the testing pattern looks like this: +::: tip For Event Sourcing, the testing pattern looks like this: - **GIVEN** set of events recorded for the entity, - **WHEN** we run the command on the state built from events, diff --git a/docs/snippets/api/command.ts b/docs/snippets/api/command.ts new file mode 100644 index 00000000..39c06eff --- /dev/null +++ b/docs/snippets/api/command.ts @@ -0,0 +1,21 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +// #region command-type +import type { Command } from '@event-driven-io/emmett'; + +type AddProductItemToShoppingCart = Command< + 'AddProductItemToShoppingCart', + { + shoppingCartId: string; + productItem: PricedProductItem; + } +>; +// #endregion command-type + +export interface ProductItem { + productId: string; + quantity: number; +} + +export type PricedProductItem = ProductItem & { + price: number; +}; diff --git a/src/commandHandling/handleCommand.ts b/src/commandHandling/handleCommand.ts index 01f82c87..7187005a 100644 --- a/src/commandHandling/handleCommand.ts +++ b/src/commandHandling/handleCommand.ts @@ -1,6 +1,7 @@ import type { EventStore } from '../eventStore'; import type { Event } from '../typing'; +// #region command-handler export const CommandHandler = ( evolve: (state: State, event: StreamEvent) => State, @@ -25,3 +26,4 @@ export const CommandHandler = return eventStore.appendToStream(streamName, ...result); else return eventStore.appendToStream(streamName, result); }; +// #endregion command-handler diff --git a/src/commandHandling/handleCommandWithDecider.ts b/src/commandHandling/handleCommandWithDecider.ts index 28287ceb..a9cb394b 100644 --- a/src/commandHandling/handleCommandWithDecider.ts +++ b/src/commandHandling/handleCommandWithDecider.ts @@ -2,6 +2,7 @@ import type { EventStore } from '../eventStore'; import type { Command, Event } from '../typing'; import type { Decider } from '../typing/decider'; +// #region command-handler export const DeciderCommandHandler = ( { @@ -25,3 +26,4 @@ export const DeciderCommandHandler = return eventStore.appendToStream(streamName, ...result); else return eventStore.appendToStream(streamName, result); }; +// #endregion command-handler diff --git a/src/eventStore/eventStore.ts b/src/eventStore/eventStore.ts index 9cc7101d..2afab070 100644 --- a/src/eventStore/eventStore.ts +++ b/src/eventStore/eventStore.ts @@ -1,5 +1,6 @@ import type { Event } from '../typing'; +// #region event-store export interface EventStore { aggregateStream( streamName: string, @@ -16,3 +17,4 @@ export interface EventStore { ...events: E[] ): Promise; } +// #endregion event-store