UNPKG

evtstore

Version:

Event Sourcing with Node.JS

147 lines (113 loc) 5.11 kB
# 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<UserEvt, UserAgg, 'users'>({ 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<PostEvt, PostAgg, 'posts'>({ 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<UserEvt, UserEvt, UserCmd>(domain.user, { async create(cmd, agg) { ... }, async disable(cmd, agg) { ... }, async enable(cmd, agg) { ... }, }) export const postCmd = createCommands<PostEvt, PostAgg, PostCmd>(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)