Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/seikho/evtstore

Event Sourcing and CQRS with Node.js and TypeScript
https://github.com/seikho/evtstore

cqrs event-sourcing mongodb nodejs postgres typescript

Last synced: 5 days ago
JSON representation

Event Sourcing and CQRS with Node.js and TypeScript

Awesome Lists containing this project

README

        

# EvtStore

> Type-safe Event Sourcing and CQRS with Node.JS and TypeScript

- [Documentation](https://seikho.github.io/evtstore)
- [Supported Databases](https://seikho.github.io/evtstore/#/docs/providers)
- [API](https://seikho.github.io/evtstore/#/docs/api)
- [Event Handlers](https://seikho.github.io/evtstore/#/docs/event-handlers)
- [Command Handlers](https://seikho.github.io/evtstore/#/docs/commands)
- Examples
- [the example folder](https://github.com/Seikho/evtstore/tree/master/example)
- [My fullstack starter](https://github.com/Seikho/fullstack-starter)

**Note: `createDomain` will be migrating to `createDomainV2` in version 11.x**
The `createDomainV2` API solves circular reference issues when importing aggregates.
The original `createDomain` will be available as `createDomainV1` from 11.x onwards.

## Why

I reguarly use event sourcing and wanted to lower the barrier for entry and increase productivity for colleagues.
The design goals were:

- Provide as much type safety and inference as possible
- Make creating domains quick and intuitive
- Be easy to test
- Allow developers to focus on application/business problems instead of Event Sourcing and CQRS problems

To obtain these goals the design is highly opinionated, but still flexible.

## Supported Databases

See [Providers](https://seikho.github.io/evtstore/#/docs/providers) for more details and examples

- Postgres using [Postgres.js](https://www.npmjs.com/package/postgres)
- Postgres using [node-postgres](https://node-postgres.com)
- SQLite, MySQL, Postgres using [Knex](https://knexjs.org)
- In-memory
- MongoDB
- Neo4j v3.5
- Neo4j v4

## Aggregate Persistence

See [the documentation](https://seikho.github.io/evtstore/#/docs/api?id=aggregate-persistence) regarding information about aggregate persistence. This refers to persisting a copy of the aggregate on events for performant retrieval.

## Examples

EvtStore is type-driven to take advantage of type safety and auto completion. We front-load the creation of our `Event`, `Aggregate`, and `Command` types to avoid having to repeatedly import and pass them as generic argument. EvtStore makes use for TypeScript's [mapped types and conditional types](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html) to achieve this.

```ts
type UserEvt =
| { type: 'created', name: string }
| { type: 'disabled' }
| { type: 'enabled' }
type UserAgg = { name: string, enabled: boolean }
type UserCmd =
| { type: 'create': name: string }
| { type: 'enable' }
| { type: 'disable' }

type PostEvt =
| { type: 'postCreated', userId: string, content: string }
| { type: 'postArchived' }

type PostAgg = { userId: string, content: string, archived: boolean }
type PostCmd =
| { type: 'createPost', userId: string, content: string }
| { type: 'archivedPost', userId: string }

const user = createAggregate({
stream: 'users',
create: () => ({ name: '', enabled: false }),
fold: (evt) => {
switch (evt.type) {
case 'created':
return { name: evt.name, enabled: true }
case 'disabled':
return { enabled: false }
case 'enabled':
return { enabled: true }
}
}
})

const post = createAggregate({
stream: 'posts',
create: () => ({ content: '', userId: '', archived: false }),
fold: (evt) => {
switch (evt.type) {
case 'postCreated':
return { userId: evt.userId, content: evt.content }
case 'postArchived':
return { archived: true }
},
}
})

const provider = createProvider()

export const { domain, createHandler } = createDomain({ provider }, { user, post })

export const userCmd = createCommands(domain.user, {
async create(cmd, agg) { ... },
async disable(cmd, agg) { ... },
async enable(cmd, agg) { ... },
})

export const postCmd = createCommands(domain.post, {
async createPost(cmd, agg) {
if (agg.version) throw new CommandError('Post already exists')
const user = await domain.user.getAggregate(cmd.userId)
if (!user.version) throw new CommandError('Unauthorized')

return { type: 'postCreated', content: cmd.content, userId: cmd.userId }
},
async archivePost(cmd, agg) {
if (cmd.userId !== agg.userId) throw new CommandError('Not allowed')
if (agg.archived) return

return { type: 'postArchived' }
}
})

const postModel = createHandler('posts-model', ['posts'], {
// When the event handler is started for the first time, the handler will begin at the end of the stream(s) history
tailStream: false,

// Every time the event handler is started, the handler will begin at the end of the stream(s) history
alwaysTailStream: false,

// Skip events that throw an error when being handled
continueOnError: false,
})

postModel.handle('posts', 'postCreated', async (id, event, meta) => {
// Insert into database
})
postModel.start()

```

See [the example folder](https://github.com/Seikho/evtstore/tree/master/example)

## API

See [API](https://seikho.github.io/evtstore/#/docs/api)