Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
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
- Host: GitHub
- URL: https://github.com/seikho/evtstore
- Owner: Seikho
- Created: 2019-10-11T15:07:26.000Z (about 5 years ago)
- Default Branch: master
- Last Pushed: 2023-09-27T20:53:29.000Z (about 1 year ago)
- Last Synced: 2024-09-19T13:50:52.569Z (about 2 months ago)
- Topics: cqrs, event-sourcing, mongodb, nodejs, postgres, typescript
- Language: TypeScript
- Homepage: https://seikho.github.io/evtstore
- Size: 645 KB
- Stars: 63
- Watchers: 5
- Forks: 5
- Open Issues: 5
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
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 problemsTo 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) returnreturn { 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)