Skip to content

Commit

Permalink
Added API docs for commands, command handling and event store
Browse files Browse the repository at this point in the history
  • Loading branch information
oskardudycz committed Feb 11, 2024
1 parent 9a7ddd4 commit 3e164ed
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 3 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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).

Expand Down
82 changes: 82 additions & 0 deletions docs/api-docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 }>
>;
```
4 changes: 3 additions & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
@@ -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!
Expand Down Expand Up @@ -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,
Expand Down
21 changes: 21 additions & 0 deletions docs/snippets/api/command.ts
Original file line number Diff line number Diff line change
@@ -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;
};
2 changes: 2 additions & 0 deletions src/commandHandling/handleCommand.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { EventStore } from '../eventStore';
import type { Event } from '../typing';

// #region command-handler
export const CommandHandler =
<State, StreamEvent extends Event>(
evolve: (state: State, event: StreamEvent) => State,
Expand All @@ -25,3 +26,4 @@ export const CommandHandler =
return eventStore.appendToStream(streamName, ...result);
else return eventStore.appendToStream(streamName, result);
};
// #endregion command-handler
2 changes: 2 additions & 0 deletions src/commandHandling/handleCommandWithDecider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
<State, CommandType extends Command, StreamEvent extends Event>(
{
Expand All @@ -25,3 +26,4 @@ export const DeciderCommandHandler =
return eventStore.appendToStream(streamName, ...result);
else return eventStore.appendToStream(streamName, result);
};
// #endregion command-handler
2 changes: 2 additions & 0 deletions src/eventStore/eventStore.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Event } from '../typing';

// #region event-store
export interface EventStore {
aggregateStream<Entity, E extends Event>(
streamName: string,
Expand All @@ -16,3 +17,4 @@ export interface EventStore {
...events: E[]
): Promise<NextExpectedVersion>;
}
// #endregion event-store

0 comments on commit 3e164ed

Please sign in to comment.