All Projects → irontitan → paradox

irontitan / paradox

Licence: LGPL-3.0 license
Tools for event sourcing applications

Programming Languages

typescript
32286 projects

Projects that are alternatives of or similar to paradox

Eventhus
Go - CQRS / Event Sourcing made easy - Go
Stars: ✭ 350 (+1066.67%)
Mutual labels:  toolkit, event-sourcing
toolkit
Opera Web UI Toolkit
Stars: ✭ 20 (-33.33%)
Mutual labels:  toolkit
banku
Go event sourcing with Kafka, example project
Stars: ✭ 29 (-3.33%)
Mutual labels:  event-sourcing
eventuous
Minimalistic Event Sourcing library for .NET
Stars: ✭ 236 (+686.67%)
Mutual labels:  event-sourcing
awesome-talks
Awesome talks about event sourcing, cqrs, microservices, funcional programming ...
Stars: ✭ 23 (-23.33%)
Mutual labels:  event-sourcing
tiny-svg
A minimal toolbelt for builing fast SVG-based applications
Stars: ✭ 34 (+13.33%)
Mutual labels:  toolkit
com.xrtk.oculus
The Oculus platform components for the XRTK
Stars: ✭ 11 (-63.33%)
Mutual labels:  toolkit
java-toolkit
【Released】🛠Java常用的插件API整理以及基于JDK的一些方法封装库,能在不依赖大型框架下快速进行开发(亦可快速用于测试或者脚本类代码编写 - 含数据库相关)。
Stars: ✭ 13 (-56.67%)
Mutual labels:  toolkit
stem
Event sourcing framework based on ZIO and pluggable runtime (currently working with Akka cluster)
Stars: ✭ 22 (-26.67%)
Mutual labels:  event-sourcing
ipc-toolkit
A set of reusable functions to integrate IPC into an existing simulation.
Stars: ✭ 84 (+180%)
Mutual labels:  toolkit
eda-tutorial
Event-Driven Tutorial for Distributed Data with CQRS and Event Sourcing
Stars: ✭ 49 (+63.33%)
Mutual labels:  event-sourcing
axion
A toolkit for CTFs
Stars: ✭ 15 (-50%)
Mutual labels:  toolkit
antd-pro-toolkit
🐜ant design pro toolkit.
Stars: ✭ 13 (-56.67%)
Mutual labels:  toolkit
arcgis-runtime-toolkit-java
Runtime Java SE toolkit
Stars: ✭ 16 (-46.67%)
Mutual labels:  toolkit
spear
A sharp EventStoreDB v20+ client backed by Mint 😋
Stars: ✭ 51 (+70%)
Mutual labels:  event-sourcing
node-cqrs-saga
Node-cqrs-saga is a node.js module that helps to implement the sagas in cqrs. It can be very useful as domain component if you work with (d)ddd, cqrs, eventdenormalizer, host, etc.
Stars: ✭ 59 (+96.67%)
Mutual labels:  event-sourcing
akka-persistence-gcp-datastore
akka-persistence-gcp-datastore is a journal and snapshot store plugin for akka-persistence using google cloud firestore in datastore mode.
Stars: ✭ 18 (-40%)
Mutual labels:  event-sourcing
firebase-event-sourcing
Event Sourcing + CQRS + DDD for Firebase
Stars: ✭ 14 (-53.33%)
Mutual labels:  event-sourcing
GHOST
General meta-Heuristic Optimization Solving Toolkit
Stars: ✭ 28 (-6.67%)
Mutual labels:  toolkit
VehicleTracker
Vehicle Tracker with Microservice example
Stars: ✭ 70 (+133.33%)
Mutual labels:  event-sourcing


Toolkit to help developers implement the event sourcing architecture

Build Status GitHub license Javascript code Style Github All Releases GitHub package version Codacy Badge Known Vulnerabilities

Summary

Instalation

$ pnpm i @irontitan/paradox
$ npm i @irontitan/paradox
$ yarn add @irontitan/paradox

Example

PersonWasCreated.ts event

Event that will create the Person class

import { Event } from '@irontitan/paradox'
import { Person } from './classes/Person'
import ObjectId from 'bson-objectid'

interface IPersonCreationParams {
  id?: ObjectId
  name: string
  email: string
}

class PersonWasCreated extends Event<IPersonCreationParams> {
  static readonly eventName: string = 'person-was-created'
  user: string

  constructor(data: IPersonCreationParams, user: string) {
    super(PersonWasCreated.eventName, data)
    this.user = user
  }

  static commit(state: Person, event: PersonWasCreated) {
    state.id = event.data.id
    state.name = event.data.name
    state.email = event.data.email
    state.updatedAt = event.timestamp
    state.updatedBy = event.user

    return state
  }
}

PersonEmailChanged.ts event

Triggered when a Person's email changes

import { Event } from '@irontitan/paradox'
import { Person } from './classes/Person'
import ObjectId from 'bson-objectid'

interface IPersonEmailChangeParams {
  newEmail: string
}

class PersonEmailChanged extends Event<IPersonEmailChangeParams> {
  static readonly eventName: string = 'person-email-changed'
  user: string

  constructor(data: IPersonEmailChangeParams, user: string) {
    super(PersonWasCreated.eventName, data)
    this.user = user
  }

  static commit(state: Person, event: PersonEmailChanged) {
    state.email = event.data.newEmail
    state.updatedAt = event.timestamp
    state.updatedBy = event.user

    return state
  }
}

Important

  • The commit method is in the event class in this example, but it can be at any place in the code
  • The eventName property is required

Person.ts class

The main Person entity.

Since version 2.9.0, EventEntity's constructor receives, as a second parameter, the Entity class itself. This is used to update the state internally when adding new events. For now, this second parameter is optional. Not passing it, though, is considered deprecated and will stop being supported on the future

import ObjectId from 'bson-objectid'
import { EventEntity } from '@irontitan/paradox'
import { PersonWasCreated } from './events/PersonWasCreated'
import { PersonEmailChanged } from './events/PersonEmailChanged'

export class Person extends EventEntity<Person> {
  name: string | null = null
  email: string | null = null
  updatedAt: Date | null = null
  updatedBy: string | null = null
  static readonly collection: string = 'people'

  constructor() {
    super({
      [ PersonWasCreated.eventName ]: PersonWasCreated.commit
    }, Person)
  }

  static create (email: string, name: string, user: string): Person { // Method to create a person
    const id = new ObjectId()
    const person = new Person()
    person.pushNewEvents([ new PersonWasCreated({id, name, email}, user) ]) // Includes a new event on creation
    return person // Returns new instance
  }

  changeEmail (newEmail: string, user: string) {
    this.pushNewEvents([ new PersonEmailChanged({ newEmail }, user) ])
    return this
  }

  get state() {
    const currentState = this.reducer.reduce(new Person, [
      ...this.persistedEvents,
      ...this.pendingEvents
    ])

    return {
      id: currentState.id,
      name: currentState.name,
      email: currentState.email
    }
  }
}

Putting it all together

import { Db, MongoClient } from 'mongodb'
import { MongodbEventRepository } from '@irontitan/paradox'
import { Person } from './classes/Person'

class PersonRepository extends MongodbEventRepository<Person> {
  constructor(connection: Db) {
    super(connection.collection(Person.collection), Person)
  }

  async search (filters: { name: string }, page: number = 1, size: number = 50) {
    const query = filters.name
      ? { 'state.name': filters.name }
      : { }

    const { documents, count, range, total } = await this._runPaginatedQuery(query, page, size)
    const entities = documents.map(({ events }) => new Person().setPersistedEvents(events))

    return { entities, count, range, total }
  }
}

(async function () {
  const connection = (await MongoClient.connect('mongodb://mongodburl')).db('crowd')
  const personRepository = new PersonRepository(connection)
  const johnDoe = Person.create('[email protected]', 'jdoe') // Will create a new event in the class
  await personRepository.save(johnDoe) // Will persist the data to the database
  const allJanes = await personRepository.search({ name: 'jane' }, 1, 10) // Will return an object implementing the IPaginatedQueryResultinterface

  // If you like, there's a possibility to update multiple classes at the same time
  johnDoe.changeEmail({ newEmail: '[email protected]' }, 'jdoe')
  const [ janeDoe ] = allJanes
  janeDoe.changeEmail({ newEmail: '[email protected]' }, 'janedoe')

  await personRepository.bulkUpdate([ johnDoe, janeDoe ]) // Updates both entities in the database using `bulkWrite`
})() // This IIFE is just to generate our async/await scope

What does this toolkit have?

  • EventEntity: Pre-made event-based class. It contains all the implementations to create a fully functional event sourcing entity
  • MongoDBEventRepository: MongoDB event-based repository (If you use another database, feel free to help us by writing a PR and adding it to the list :D)
  • Typing helpers
  • A bare export of the Tardis toolkit

API Summary

EventEntity

TL;DR: Represents an business entity with event sourcing properties. Must always be extended. Must always contain a state getter which returns the final state of the entity

Properties:

  • public persistedEvents: Array of events that were persisted to the database
  • public pendingEvents: Array of events that have not yet been persisted to the database
  • protected reducer: A reducer instance as described in the Tardis documentation
  • get state: Returns the final state of the entity (must be implemented)

Methods:

  • public setPersistedEvents(events: Array<{id, name, data, timestamp}>): Sets the persistedEvents property with the events array
  • public pushNewEvents(events: Array<{id, name, data, timestamp}>): Pushes a new event to the pendingEvents array
  • public confirmEvents(): Transfers all the content from the pendingEvents array to the persistedEvents array

MongodbEventRepository

TL;DR: Represents a database that is fully suited to use event-based classes

Properties:

  • protected _collection: Collection name

Methods:

  • public save(entity: BusinessEntity, force: Boolean = false): Saves the current entity to the database by either pushing new events, or overriding the events array (Be VERY carefull with the last one)
  • public bulkUpdate(entities: EventEntity[], session): Updates multiple entities at once
  • public bulkInsert(entities: EventEntity[], session): Inserts multiple entities at once
  • public findById(id: string | ObjectId): Finds an entity by the provided ID
  • public withSession(session: ClientSession): Starts a MongoDB session and returns the available methods that can be used with the provided session
  • protected _runPaginatedQuery(query: {[key: string]: any}, page: number, size: number, sort?: {[field: string]: 1|-1}): Runs a query in the database and return the paginated results

EventEntity

An EventEntity is a business class which posesses the implementation of all events this class can have. All event-based entity must extends EventEntity class, since it is an abstract/generic class. Extending it will give your own class some cool functionalities out-of-the-box:

  • persistedEvents: An array of events which were already persistted to the database. It follows the {id, name, data, timestamp} format
  • pendingEvents: An array of events which were not yet saved to the database

When created, the new entity will receive (as a parameter) an object, of which the keys must be the name of an event and its value must be the commit function, which can be located anywhere, but, in our little example above, we created it as a static method inside the event entity itself. Since v2.9.0, it also receives the entity class itself, to be used for internal purposes.

This procedure is the same for all the events that entity might have, this is due to the fact that the EventEntity, when instantiated, will create a Reducer instance in the property this.reducer and it'll pass on all these known events to it so it can be possible to manage all events inside the same class, without the need to instantiate something new.

This class must also have a getter caled state. This getter exists in the parent class (EventEntity) as a "Non implemented method", which will throw an error if used as default. This way, it becomes necessary for the child class to overwrite the parent class' method, implementing in it the responsability to reduce the previous state to the current state and returning it.

Refer to the Person.ts class for more information

Besides state, the EventEntity class will disclose several other methods such:

  • setPersistedEvents: Which will receive an array of events in the {id, name, data, timestamp} format, fetched from the database, and it'll include these events into the persistedEvents array. It'll be often used when loading a class for the first time from the database.
  • pushNewEvents: Will receive an event array following the same {id, name, data, timestamp} format, but instead of adding them to the persisted events array, it'll add the events to the pendingEvents array and thus, notifying that there are events which were not yet persisted to the database and are only available inside this instance.
  • confirmEvents: Will move all the items from the pendingEvents array to the persistedEvents array. This will confirm that all the events were successfuly saved into the database. This will be often used after we save the last state of the entity to the database.

All of the three methods above call the private method updateState, which sets all properties from the current state back to the instance of the entity class. Which means that, when an event changes the value of a property, you don't need to recalculate the state altogether once again, it'll be automatically updated and available through this.propertyName

Repositories

Repositories are places where data resides, by default, we would not have to create an event-base class for them, but, in order to standardize all the events saved into the database, it was necessary to create such class.

Since different databases have different event sourcing implementations, for now, we only have the ones listed below.

Note that different repository classes might behave differently depending on who created the class, please refer to the PR section or fill in an issue if you're experiencing trouble.

Interfaces

IPaginatedQueryResult

Represents a paginated query:

interface IPaginatedQueryResult<TDocument> { // TDocument is the type that represents the data which will be returned from the database (it is used internally)
  documents: TDocument[] // Documents in the current page
  count: number // Total results in the page
  range: {
    from: number, // Index of the first result
    to: number // Index of the last result
  }
  total: number // Query total
}

IEntityConstructor

Represents the constructor of an entity

interface IEntityConstructor<Entity> {
  new(events?: IEvent<any>[]): Entity
}

My repository is not included, what do I do?

Since this lib is open source and generic enough to be used by multiple repositories, there's no way to know which repositories the users are going to be using. So we added a way for you to create your own.

In order to create a repository, your class must extend the EventRepository class, which is fully abstract and is as follows:

export interface IEntityConstructor<Entity> {
  new(events?: IEvent<any>[]): Entity
}

export abstract class EventRepository<TEntity extends IEventEntity> {

  protected readonly _Entity: IEntityConstructor<TEntity>

  constructor (Entity: IEntityConstructor<TEntity>) {
    this._Entity = Entity
  }

  abstract async save (entity: TEntity): Promise<TEntity>

  abstract async findById (id: any): Promise<TEntity | null>

  abstract async runPaginatedQuery (query: { [key: string]: any }, page: number, size: number, sort: { [field: string]: 1 | -1 }): Promise<IPaginatedQueryResult<{ events: IEvent<TEntity>[] }>>
}

In order to maintain consistency between implementations, the following methods must be implemented:

  • save: Should save the given entity to the database and return the entity
  • findById: Should find an entity by its ID in the database. It is important to notice that, once found, the returned value should be a newly created instance of that entity (this is where you're going to use the setPersistedEvents method)
  • runPaginatedQuery: Should return a paginated query from the database

Besides these methods, any class that extends EventRepository will inherit the _Entity property, which refers to the entity constructor. This will be used when returning the newly created entity from the database during the findById method and seting its persisted events on the newly instantiated class, like so:

async function findById (id) {
  /* finds the data */
  const instance = this._Entity() // Creates a new instance of <Entity>
  return instance.setPersistedEvents(yourEvents) // Sets the returned data into the instance
}

Those are the required implementations, any additional functionalities you'd like to include in the repository can be added at will.

For further explanation and examples, refer to the MongodbEventRepository file in the src folder

Adding your repository to the list

If you'd like to add your repository to the list of included repositories, please fill in a PR and don't forget to stick to some conventions:

  • All names are CamelCase
  • Private methods start their names with _
  • Do not forget to add the documentation to this repository in the docs/ folder (the file should be the same name as your class)
  • Do not forget to add your repository to the list in this README along with the link to its own docs

Thank you for your contribution :D

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].